diff --git a/.github/workflows/build_cbsdk.yml b/.github/workflows/build_cbsdk.yml index 529f7060..0c2b546c 100644 --- a/.github/workflows/build_cbsdk.yml +++ b/.github/workflows/build_cbsdk.yml @@ -9,6 +9,7 @@ on: pull_request: branches: - master + - dev release: types: [published] @@ -27,20 +28,15 @@ jobs: fail-fast: false matrix: config: - - {name: "windows-x64", os: "windows-latest", cmake_extra: "-T v142,host=x86", arch: "AMD64"} - - {name: "windows-arm64", os: "windows-latest", cmake_extra: "-A ARM64", arch: "ARM64"} - - {name: "macOS-latest", os: "macOS-latest", arch: "auto"} - - {name: "jammy", os: "ubuntu-22.04", arch: "x86_64"} - - {name: "ubuntu-latest", os: "ubuntu-latest", arch: "x86_64"} - - {name: "bookworm-x64", os: "ubuntu-latest", container: "debian:bookworm", arch: "x86_64"} - - {name: "linux-arm64", os: "ubuntu-latest", arch: "aarch64"} - - container: ${{ matrix.config.container }} + - {name: "windows-x64", os: "windows-latest", cmake_extra: "", arch: "AMD64"} + - {name: "macOS-latest", os: "macOS-latest", cmake_extra: "", arch: "auto"} + - {name: "linux-x64", os: "ubuntu-latest", cmake_extra: "", arch: "x86_64"} + - {name: "linux-arm64", os: "ubuntu-latest", cmake_extra: "", arch: "aarch64"} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU for ARM emulation if: matrix.config.arch == 'aarch64' @@ -48,15 +44,6 @@ jobs: with: platforms: arm64 - - name: Install build dependencies (Debian container) - if: matrix.config.container - run: | - apt-get update - apt-get install -y \ - cmake g++ make git \ - python3 python3-pip python3-venv \ - file lsb-release dpkg-dev - - name: Install ARM64 cross-compilation tools if: matrix.config.arch == 'aarch64' run: | @@ -87,11 +74,8 @@ jobs: SHLIBDEPS_ARG="-DCPACK_DEBIAN_PACKAGE_SHLIBDEPS=OFF" fi cmake -B build -S . \ - -DCBSDK_BUILD_CBMEX=OFF \ - -DCBSDK_BUILD_CBOCT=OFF \ -DCBSDK_BUILD_TEST=OFF \ -DCBSDK_BUILD_SAMPLE=OFF \ - -DCBSDK_BUILD_TOOLS=OFF \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=${PWD}/install \ -DCPACK_PACKAGE_DIRECTORY=${PWD}/dist \ @@ -107,47 +91,6 @@ jobs: cmake --build build --target package --config Release -j fi - - name: Set up Python - if: "!matrix.config.container" - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install cibuildwheel - if: "!matrix.config.container" - run: python -m pip install cibuildwheel - - - name: Build wheels - if: "(!startsWith(matrix.config.os, 'ubuntu-') || matrix.config.os == 'ubuntu-latest' || matrix.config.name == 'linux-arm64') && !matrix.config.container" - run: python -m cibuildwheel bindings/Python --output-dir bindings/Python/dist - env: - CIBW_BUILD: cp311-* cp312-* cp313-* - CIBW_SKIP: "*-musllinux_* *-win32 *-manylinux_i686" - CIBW_BUILD_VERBOSITY: 1 - CIBW_ENVIRONMENT: CBSDK_INSTALL_PATH="${{ github.workspace }}/install" PIP_ONLY_BINARY=":all:" - CIBW_ARCHS: ${{ matrix.config.arch }} - - - name: Linux - Prefix package with release name - if: startswith(matrix.config.os, 'ubuntu-') && !matrix.config.container && matrix.config.arch != 'aarch64' - run: | - pushd dist - temp="$(lsb_release -cs)" - for name in * - do - mv "$name" "$(echo $temp)-$name" - done - popd - - - name: Debian - Prefix package with release name - if: matrix.config.container - run: | - pushd dist - for name in * - do - mv "$name" "bookworm-$name" - done - popd - - name: Upload C++ Packages uses: actions/upload-artifact@v4 with: @@ -155,18 +98,8 @@ jobs: path: dist/* if-no-files-found: ignore - - name: Upload Python Wheels - uses: actions/upload-artifact@v4 - if: "(!startsWith(matrix.config.os, 'ubuntu-') || matrix.config.os == 'ubuntu-latest' || matrix.config.name == 'linux-arm64') && !matrix.config.container" - with: - name: python-wheels-${{ matrix.config.name }} - path: bindings/Python/dist/*.whl - if-no-files-found: ignore - - name: Attach to Release uses: softprops/action-gh-release@v1 if: github.event_name == 'release' with: - files: | - dist/* - bindings/Python/dist/*.whl + files: dist/* diff --git a/.github/workflows/build_pycbsdk.yml b/.github/workflows/build_pycbsdk.yml new file mode 100644 index 00000000..1d02fe52 --- /dev/null +++ b/.github/workflows/build_pycbsdk.yml @@ -0,0 +1,151 @@ +name: Build pycbsdk Wheels + +on: + workflow_dispatch: + push: + branches: [master] + paths: + - 'src/**' + - 'pycbsdk/**' + - '.github/workflows/build_pycbsdk.yml' + pull_request: + branches: + - master + - dev + paths: + - 'src/**' + - 'pycbsdk/**' + release: + types: [ published ] + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + build-wheel: + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - name: windows-x64 + os: windows-latest + cmake_extra: "" + lib_pattern: "cbsdk.dll" + - name: linux-x64 + os: ubuntu-latest + cmake_extra: "" + lib_pattern: "libcbsdk.so" + - name: macos-universal + os: macos-latest + cmake_extra: "" + lib_pattern: "libcbsdk.dylib" + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 # Full history needed for git describe and setuptools-scm + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install Python build tools + run: pip install build setuptools wheel setuptools-scm + + - name: Build cbsdk shared library + run: | + cmake -B build -S . \ + -DCBSDK_BUILD_SHARED=ON \ + -DCBSDK_BUILD_TEST=OFF \ + -DCBSDK_BUILD_SAMPLE=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + ${{ matrix.config.cmake_extra }} + cmake --build build --target cbsdk_shared --config Release + + - name: Copy shared library to package + run: | + find build -name "${{ matrix.config.lib_pattern }}" -type f | head -1 | while read f; do + echo "Found: $f" + cp "$f" pycbsdk/src/pycbsdk/ + done + ls -la pycbsdk/src/pycbsdk/ + + - name: Build wheel + run: | + cd pycbsdk + python -m build --wheel + + - name: Repair wheel (Linux) + if: runner.os == 'Linux' + run: | + pip install auditwheel patchelf + # Detect the compatible manylinux tag for this toolchain + PLAT=$(auditwheel show pycbsdk/dist/*.whl 2>&1 | grep -oP 'manylinux_\d+_\d+_x86_64' | head -1) + if [ -z "$PLAT" ]; then + echo "auditwheel could not determine a manylinux tag, tagging as linux_x86_64" + PLAT="linux_x86_64" + fi + echo "Repairing wheel for platform: $PLAT" + auditwheel repair pycbsdk/dist/*.whl -w pycbsdk/wheelhouse/ --plat "$PLAT" + rm pycbsdk/dist/*.whl + mv pycbsdk/wheelhouse/*.whl pycbsdk/dist/ + + - name: Repair wheel (macOS) + if: runner.os == 'macOS' + run: | + pip install delocate + delocate-wheel -v pycbsdk/dist/*.whl + + - name: Upload wheel + uses: actions/upload-artifact@v4 + with: + name: pycbsdk-${{ matrix.config.name }} + path: pycbsdk/dist/*.whl + + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest + if: github.event_name == 'release' + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - run: pip install build setuptools-scm + - run: cd pycbsdk && python -m build --sdist + - uses: actions/upload-artifact@v4 + with: + name: pycbsdk-sdist + path: pycbsdk/dist/*.tar.gz + + publish: + name: Publish to PyPI + needs: [build-wheel, build-sdist] + runs-on: ubuntu-latest + if: github.event_name == 'release' + permissions: + id-token: write # Required for trusted publishing (OIDC) + environment: + name: pypi + url: https://pypi.org/p/pycbsdk + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist/ + merge-multiple: true + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml deleted file mode 100644 index 86d20e84..00000000 --- a/.github/workflows/publish-to-pypi.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Publish Python Package to PyPI - -on: - workflow_run: - workflows: ["Build and Release"] - types: - - completed - -permissions: - contents: read - id-token: write # Required for trusted publishing to PyPI - -jobs: - publish: - # Only run if the build workflow succeeded and was triggered by a published release. - # This filters out runs from push, pull_request, and workflow_dispatch triggers. - # Note: build_cbsdk.yml only responds to 'release: types: [published]', so - # if event == 'release', it must be a published release. - if: | - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'release' - runs-on: ubuntu-latest - - steps: - - name: Verify trigger event - run: | - echo "Workflow was triggered by: ${{ github.event.workflow_run.event }}" - echo "Workflow conclusion: ${{ github.event.workflow_run.conclusion }}" - echo "Workflow head branch: ${{ github.event.workflow_run.head_branch }}" - - - name: Download all wheel artifacts - uses: actions/download-artifact@v4 - with: - pattern: python-wheels-* - path: dist/ - merge-multiple: true - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: List downloaded wheels - run: | - echo "Downloaded wheels:" - ls -lh dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist/ \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea294aa8..f8000e1b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Configure CBSDK run: | @@ -46,4 +46,6 @@ jobs: run: cmake --build build --config Release -j - name: Run Tests - run: ctest --test-dir build --build-config Release --output-on-failure + run: > + ctest --test-dir build --build-config Release --output-on-failure + -E "^device\." diff --git a/.gitignore b/.gitignore index b4223a80..910daace 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,9 @@ Release/ **/cerelink/cbpyw.cpp **/cerelink/*.dll **/cerelink/__version__.py +*.ccf +!tests/ccf_raw.ccf +!tests/ccf_spk_1k.ccf -upstream \ No newline at end of file +upstream +*/uv.lock \ No newline at end of file diff --git a/BUILD.md b/BUILD.md index b3e0a280..6c06fbfc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,78 +1,55 @@ -# CBSDK Build Instructions +# CereLink Build Instructions -Most users will only need to download directly from the [releases page](https://github.com/CerebusOSS/CereLink/releases). +Pre-built packages are available on the [releases page](https://github.com/CerebusOSS/CereLink/releases). -| Platform | C++ Packages (.deb/.zip) | Python Wheels | -|---------------|--------------------------|---------------| -| windows-x64 | ✅ | ✅ | -| windows-arm64 | ✅ | ✅ | -| macOS-latest | ✅ | ✅ | -| jammy | ✅ | ❌ | -| ubuntu-latest | ✅ | ✅ (manylinux) | -| bookworm-x64 | ✅ | ❌ | -| linux-arm64 | ✅ | ✅ (aarch64) | +For GitHub Actions build scripts, see [.github/workflows/](./.github/workflows/). -Other users who just want the CLI commands, or if the below instructions aren't working for you, should check out the GitHub Actions [workflow scripts](https://github.com/CerebusOSS/CereLink/blob/master/.github/workflows/build_cbsdk.yml). +## Requirements -Continue here only if you want to build CereLink (Python cerelink) from source. +C++17 toolchain and CMake 3.16+. -## Requirements +## Build + +```bash +cmake -B build -S . -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release -j +``` -We assume you already have a build environment with an appropriate C++ toolchain and CMake installed. +### CMake Options -### Matlab (optional) +| Option | Default | Description | +|--------|---------|-------------| +| `CBSDK_BUILD_TEST` | ON (standalone) | Build unit and integration tests | +| `CBSDK_BUILD_SAMPLE` | ON (standalone) | Build example applications | +| `CBSDK_BUILD_SHARED` | OFF | Build `cbsdk_shared` (DLL/dylib/so) for pycbsdk | -If you want to build the Matlab wrappers then you will need to have Matlab development libraries available. In most cases, if you have Matlab installed in a default location, then cmake should be able to find it. +### Run Tests -## Cmake command line - Try me first +```bash +ctest --test-dir build --build-config Release --output-on-failure -E "^device\." +``` -Here are some cmake one-liners that work if your development environment happens to match perfectly. If not, then modify the cmake options according to the [CMake Options](#cmake-options) instructions below. +The `-E "^device\."` filter excludes tests that require a physical device or network. -* Windows: - * `cmake -B build -S . -G "Visual Studio 16 2019" -DBUILD_STATIC=ON -DCMAKE_INSTALL_PREFIX=../install -DCPACK_PACKAGE_DIRECTORY=../dist` -* MacOS - * `cmake -B build -S . -DCMAKE_INSTALL_PREFIX=${PWD}/install -DCPACK_PACKAGE_DIRECTORY=${PWD}/dist` - * If you are going to use the Xcode generator then you also need to use the old build system. Append: `-G Xcode -T buildsystem=1` -* Linux - * `cmake -B build -S . -DCMAKE_INSTALL_PREFIX=${PWD}/install -DCPACK_PACKAGE_DIRECTORY=${PWD}/dist` +### Build Shared Library (for pycbsdk) -Then follow that up with (append a processor # after the -j to use more cores): -* `cmake --build build --config Release -j` -* `cmake --build build --target=install --config Release -j` +```bash +cmake -B build -S . -DCBSDK_BUILD_SHARED=ON -DCBSDK_BUILD_TEST=OFF -DCMAKE_BUILD_TYPE=Release +cmake --build build --target cbsdk_shared --config Release +``` -And optionally, to build zips or debs: -* `cmake --build build --target package --config Release -j` +### Install -The build products should appear in the CereLink/install or CereLink/build/install directory. +```bash +cmake --build build --target install --config Release +``` -Note: This may generate an error related to the CLI builds. Please see further instructions in the [wrappers/cli/README.md](wrappers/cli/README.md). +### Package -### CMake Options +```bash +cmake --build build --target package --config Release +``` + +## Python (pycbsdk) -* `-G ` - * Call `cmake -G` to see a list of available generators. -* `-DCBSDK_BUILD_STATIC=ON` - * Whether to build cbsdk_static lib. This is required by the Python and Matlab wrappers. -* `-DCBSDK_BUILD_CBMEX=ON` - * To build Matlab binaries. Will only build if Matlab development libraries are found. -* `-DCBSDK_BUILD_CBOCT=ON` - * To build Octave binaries. Will only build if Octave development libraries are found. -* `-DCBSDK_BUILD_TEST=ON` -* `-DCBSDK_BUILD_SAMPLE=ON` -* `-DCBSDK_BUILD_TOOLS=ON` - * NSX to HDF5 tool should only build if HDF5 found. - * A few C++ applications in `tools/loop_tester` to test data pulling stability. -* `-DCBSDK_BUILD_CLI=ON` - * to build the C#/CLI bindings. Buggy. -* `-DCBPROTO_311=OFF` - * Set this to ON to compile CereLink to work with NSPs using protocol 3.11 (firmware 7.0x). - * Matlab and Python wrappers not supported in this mode. - * Cannot run alongside Central x86 version (if in "Program Files (x86)") because the Qt6 dependency restricts compilation targets to x64. But it works fine on other computers or if Central is not running. - -#### Extra Options for Matlab / cbmex - -* `-DMatlab_ROOT_DIR=` - * This should only be necessary if cmake cannot find Matlab automatically. - * e.g.: `-DMatlab_ROOT_DIR=/Applications/MATLAB_R2016a.app/` - * Alternatively, you could populate the ../Matlab directories with the Matlab include and lib files. -* `-DCBMEX_INSTALL_PREFIX` can be used to install cbmex to given directory. +See [pycbsdk/](./pycbsdk/) for building the Python wheel from source. diff --git a/CMakeLists.txt b/CMakeLists.txt index 532147a8..32fa8591 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,271 +1,124 @@ -# CBSDK CMake Build System -# Author: griffin.milsap@gmail.com -# chadwick.boulay@gmail.com +# CereLink Build System +# Author: chadwick.boulay@gmail.com +# +# Modular architecture: cbproto → cbshm → cbdev → cbsdk -cmake_minimum_required( VERSION 3.16 ) +cmake_minimum_required(VERSION 3.16) set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake" ${CMAKE_MODULE_PATH}) include(AppleSilicon) include(GetVersionFromGit) include(CheckAtomicNeeded) -# cmake_policy(SET CMP0091 NEW) # Get version from git tags get_version_from_git() project(CBSDK - DESCRIPTION "Blackrock Neurotech CereBus Software Development Kit" - LANGUAGES C CXX - VERSION ${GIT_VERSION_FULL} - ) + DESCRIPTION "Blackrock Neurotech CereBus Software Development Kit" + LANGUAGES C CXX + VERSION ${GIT_VERSION_FULL} +) # Common Configuration set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED On) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Required for static libs linked into shared lib (cbsdk_shared) ########################################################################################## # Optional Targets include(CMakeDependentOption) -# These options default to ON when building standalone, OFF when included as a subdirectory -cmake_dependent_option(CBSDK_BUILD_CBMEX "Build Matlab wrapper" OFF - "CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF) -cmake_dependent_option(CBSDK_BUILD_CBOCT "Build Octave wrapper" OFF - "CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF) +# Build options for new architecture cmake_dependent_option(CBSDK_BUILD_TEST "Build tests" ON "CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF) cmake_dependent_option(CBSDK_BUILD_SAMPLE "Build sample applications" ON "CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF) -cmake_dependent_option(CBSDK_BUILD_TOOLS "Build tools" ON - "CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF) - -# These options are always available regardless of build context -option(CBSDK_BUILD_STATIC "Build static cbsdk library" ON) -option(CBPROTO_311 "Build for protocol 3.11" OFF) -########################################################################################## -# Define target names -set( LIB_NAME cbsdk ) -set( INSTALL_TARGET_LIST ${LIB_NAME} ) -set( LIB_NAME_STATIC cbsdk_static ) -set( LIB_NAME_CBMEX cbmex ) -set( LIB_NAME_CBOCT cboct ) ########################################################################################## # Misc Configuration # -Make sure debug builds are recognized set(CMAKE_DEBUG_POSTFIX "d" CACHE STRING "Add a postfix, usually d on windows") + # -Force universal binary on macOS set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") -# -? -if(CBPROTO_311) - add_compile_definitions(CBPROTO_311) -endif() -if( WIN32 ) - # From cbhwlib/cbmex.vcproj: PreprocessorDefinitions="WIN32;_WINDOWS;NO_AFX;WINVER=0x0501;CBSDK_EXPORTS" +if(WIN32) + # Preprocessor definitions for Windows add_compile_definitions( WIN32 _WINDOWS NO_AFX NOMINMAX _CRT_SECURE_NO_WARNINGS - _WINSOCK_DEPRECATED_NO_WARNINGS # Not necessary if -DUNICODE + _WINSOCK_DEPRECATED_NO_WARNINGS ) -endif( WIN32 ) + + # Ensure Windows SDK architecture macros are defined. + # Required when consumed as a dependency (FetchContent) where the consuming + # project may not properly propagate architecture settings. + # Without this, winnt.h fails with "No Target Architecture" error. + if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + add_compile_definitions(_ARM64_) + elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) + add_compile_definitions(_AMD64_) + else() + add_compile_definitions(_X86_) + endif() +endif(WIN32) + set(CMAKE_CXX_VISIBILITY_PRESET hidden) -cmake_policy(SET CMP0063 NEW) # ENABLE CMP0063: Honor visibility properties for all target types. -cmake_policy(SET CMP0042 NEW) # ENABLE CMP0042: MACOSX_RPATH is enabled by default. +cmake_policy(SET CMP0063 NEW) # Honor visibility properties for all target types +cmake_policy(SET CMP0042 NEW) # MACOSX_RPATH is enabled by default ########################################################################################## # Standard installation directories include(GNUInstallDirs) ########################################################################################## -# Third party libraries -include(FetchContent) - -# -PugiXML -set(PUGIXML_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(PUGIXML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -set(PUGIXML_BUILD_SHARED_AND_STATIC OFF) -set(PUGIXML_INSTALL OFF CACHE BOOL "" FORCE) -FetchContent_Declare( - pugixml - GIT_REPOSITORY https://github.com/zeux/pugixml.git - GIT_TAG v1.15 - GIT_SHALLOW TRUE - EXCLUDE_FROM_ALL -) -FetchContent_MakeAvailable(pugixml) -if(TARGET pugixml AND NOT TARGET pugixml::pugixml) - add_library(pugixml::pugixml ALIAS pugixml) -endif() - -# Custom CCFUtils Lib -set( CCF_SOURCE - ${CMAKE_CURRENT_SOURCE_DIR}/src/ccfutils/CCFUtils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/ccfutils/CCFUtilsBinary.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/ccfutils/CCFUtilsConcurrent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/ccfutils/CCFUtilsXml.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/ccfutils/CCFUtilsXmlItems.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/ccfutils/XmlFile.cpp -) -add_library(CCFUtils OBJECT ${CCF_SOURCE}) -target_link_libraries(CCFUtils PRIVATE pugixml::pugixml) -target_include_directories(CCFUtils - PUBLIC - $ - INTERFACE # for unit tests - $ - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbproto -) -set_property(TARGET CCFUtils PROPERTY POSITION_INDEPENDENT_CODE ON) - -########################################################################################## -# Files/folders common to multiple targets -# (LIB_INCL_DIRS removed - now using target_include_directories) - -set( LIB_SOURCE - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbsdk/cbsdk.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbsdk/ContinuousData.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbsdk/EventData.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbhwlib/cbhwlib.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbhwlib/cbHwlibHi.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbhwlib/InstNetwork.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/central/Instrument.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/central/UDPsocket.cpp -) - +# New Modular Architecture +message(STATUS "Building modular architecture") +add_subdirectory(src/cbproto) +add_subdirectory(src/cbutil) +add_subdirectory(src/ccfutils) +add_subdirectory(src/cbshm) +add_subdirectory(src/cbdev) +add_subdirectory(src/cbsdk) ########################################################################################## -# Targets - -# cbsdk shared / dynamic -add_library(${LIB_NAME} SHARED ${LIB_SOURCE} $) -target_include_directories( ${LIB_NAME} - PUBLIC - $ - $ - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbhwlib - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbproto - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbsdk - ${CMAKE_CURRENT_SOURCE_DIR}/src/ccfutils - ${CMAKE_CURRENT_SOURCE_DIR}/src/central - ${CMAKE_CURRENT_SOURCE_DIR}/src/compat -) -target_link_libraries( ${LIB_NAME} - PRIVATE $) -if( WIN32 ) - target_link_libraries( ${LIB_NAME} PRIVATE wsock32 ws2_32 winmm ) - target_include_directories(${LIB_NAME} PUBLIC $) -else() - if(NOT APPLE) - target_link_libraries(${LIB_NAME} PRIVATE rt) - # Hide unexported symbols - target_link_options( ${LIB_NAME} PRIVATE "LINKER:--exclude-libs,ALL" ) - endif(NOT APPLE) -endif( WIN32 ) -set_target_properties( - ${LIB_NAME} - PROPERTIES - SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} -) -target_compile_definitions(${LIB_NAME} PRIVATE CBSDK_EXPORTS) - - -## -# cbsdk_static (optional) -if(CBSDK_BUILD_STATIC) - add_library( ${LIB_NAME_STATIC} STATIC ${LIB_SOURCE} $ ) - target_include_directories( ${LIB_NAME_STATIC} - PUBLIC - $ - $ - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbhwlib - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbproto - ${CMAKE_CURRENT_SOURCE_DIR}/src/cbsdk - ${CMAKE_CURRENT_SOURCE_DIR}/src/ccfutils - ${CMAKE_CURRENT_SOURCE_DIR}/src/central - ${CMAKE_CURRENT_SOURCE_DIR}/src/compat - ) - target_link_libraries( ${LIB_NAME_STATIC} - PRIVATE $) - if( WIN32 ) - target_link_libraries( ${LIB_NAME_STATIC} PRIVATE ws2_32 winmm ) - target_compile_definitions( ${LIB_NAME_STATIC} PUBLIC STATIC_CBSDK_LINK ) - # Note: If needed, set MSVC runtime library with: - # set_target_properties( ${LIB_NAME_STATIC} PROPERTIES - # MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" - # ) - else( WIN32 ) - if(NOT APPLE) - target_link_libraries(${LIB_NAME_STATIC} PRIVATE rt) - endif(NOT APPLE) - # Need relocatable static library - target_link_options( ${LIB_NAME_STATIC} PRIVATE "LINKER:--exclude-libs,ALL" ) - set_target_properties( ${LIB_NAME_STATIC} PROPERTIES - POSITION_INDEPENDENT_CODE ON) - endif( WIN32) - check_and_link_atomic(${LIB_NAME_STATIC}) - list(APPEND INSTALL_TARGET_LIST ${LIB_NAME_STATIC}) -endif(CBSDK_BUILD_STATIC) - -## -# Tests -# Note: We use enable_testing() instead of include(CTest) to avoid dashboard targets clutter -# The dashboard targets (Experimental*, Nightly*, Continuous*) are only created by include(CTest) +# Tests for Modular Architecture if(CBSDK_BUILD_TEST) enable_testing() - add_subdirectory(tests) + add_subdirectory(tests/unit) + add_subdirectory(tests/integration) endif(CBSDK_BUILD_TEST) -## -# Very Simple Sample Applications +########################################################################################## +# Validation Tools +add_subdirectory(tools/validate_clock_sync) + +########################################################################################## +# Sample Applications for New Architecture if(CBSDK_BUILD_SAMPLE) add_subdirectory(examples) endif(CBSDK_BUILD_SAMPLE) -## -# Language bindings (MATLAB, Octave, C++/CLI, Python) -add_subdirectory(bindings) - -## Misc Tools -add_subdirectory(tools) - - -######################################################################################### -# Install libraries, test executable, and headers -install( TARGETS ${INSTALL_TARGET_LIST} - EXPORT CBSDKTargets - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} -) -install( - DIRECTORY - ${CMAKE_CURRENT_SOURCE_DIR}/include/cerelink - DESTINATION - ${CMAKE_INSTALL_INCLUDEDIR} -) -if(WIN32) - install( - FILES - $ - TYPE BIN +########################################################################################## +# Installation +# The actual installation targets are defined in the subdirectories: +# - src/cbproto/CMakeLists.txt installs cbproto +# - src/cbshm/CMakeLists.txt installs cbshm +# - src/cbdev/CMakeLists.txt installs cbdev +# - src/cbsdk/CMakeLists.txt installs cbsdk + +# Install sample applications if built +if(CBSDK_BUILD_SAMPLE AND SAMPLE_TARGET_LIST) + install(TARGETS ${SAMPLE_TARGET_LIST} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) +endif() -elseif(APPLE) - install(CODE " - execute_process(COMMAND codesign --force --deep --sign - ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/lib${LIB_NAME}.dylib) - ") -endif(WIN32) - -######################################################################################### -# CMake Package Config -# Install the export set for use with find_package() +########################################################################################## +# CMake Package Config for new architecture include(CMakePackageConfigHelpers) -# Install the export targets +# Install the export targets (collected from subdirectories) install(EXPORT CBSDKTargets FILE CBSDKTargets.cmake NAMESPACE CBSDK:: @@ -293,10 +146,7 @@ install(FILES DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/CBSDK ) - -# --------- # -# Packaging # -# --------- # -# Child script handles most details. +########################################################################################## +# Packaging # Invoke with `cmake --build --target package` include(Packing) diff --git a/README.md b/README.md index ec90bd8e..25a7bb0d 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,78 @@ # CereLink -Cerebus Link for Blackrock Neurotech hardware +Software development kit for Blackrock Neurotech neural signal processing hardware (Cerebus, CerePlex, NSP, Gemini). -The software development kit for Blackrock Neurotech neural signal processing hardware includes: -* c++ library (cbsdk): cross-platform library for two-way communication with hardware -* Python binding (cerelink): Python binding for cbsdk to configure, pull data, and receive callbacks -* MATLAB/Octave binding (cbmex/cboct): MATLAB executable (mex) to configure and pull data using cbsdk -* C#/CLI binding -* File conversion utility (n2h5): Converts nsx and nev files to hdf5 format +## Components -Downloads are on the [releases page](https://github.com/CerebusOSS/CereLink/releases). +- **cbsdk** — C/C++ library for two-way communication with hardware +- **pycbsdk** — Python wrapper (cffi, no compiler needed at install time) -## Build +## Architecture -The [BUILD.md](./BUILD.md) document has the most up-to-date build instructions. +Modular library stack: -## Usage +| Module | Purpose | +|--------|---------| +| `cbproto` | Protocol definitions, packet types, version translation | +| `cbshm` | Shared memory (Central-compat and native layouts) | +| `cbdev` | Device transport (UDP sockets, handshake, clock sync) | +| `cbsdk` | SDK orchestration (device + shmem + callbacks + config) | +| `ccfutils` | CCF XML config file load/save | +| `pycbsdk` | Python package via cffi ABI mode | -You must be connected to a Blackrock Neurotech device (Cerebus, CerePlex, NSP, or Gemini) or a simulator (nPlayServer) to use cbsdk, cbmex, cboct, or cerebus.cbpy. +## Connection Modes -### Testing with nPlayServer +- **STANDALONE** — CereLink owns the device connection and shared memory +- **CLIENT** — Attach to another CereLink instance's shared memory +- **CENTRAL_COMPAT CLIENT** — Attach to Central's shared memory with on-the-fly protocol translation -On Windows, download and install the latest version of Cerebus Central Suite from the [Blackrock Neurotech support website (scroll down)](https://blackrockneurotech.com/support/). You may also wish to download some sample data from this same website. +## Build -nPlayServer for other platforms is available upon request. Post an issue in this repository with details about your system configuration, and we will try to help. +See [BUILD.md](./BUILD.md) for build instructions. -#### Testing with nPlayServer on localhost +## Python -After installing, navigate an explorer Window to `C:\Program Files\Blackrock Microsystems\Cerebus Central Suite\` and run `runNPlayAndCentral.bat`. This will run a device simulator (nPlayServer) and Central on the localhost loopback. cbsdk / cerebus / cbmex should be able to connect as a Slave (Central is the Master) to the nPlay instance. +``` +pip install pycbsdk +``` -#### Testing with nPlayServer on network +Or build from source — see [pycbsdk/](./pycbsdk/). -If you want to test using nPlayServer on a different computer to better emulate the device, you must first configure the IP addresses of the ethernet adapters of both the client computer with CereLink and the device-computer with nPlayServer. The client computer should be set to 192.168.137.198 or .199. The nPlayServer computer's IP address should be set to 192.168.137.128 to mimic a Cerebus NSP or 192.168.137.200 to mimic a Gemini Hub. +## Testing with nPlayServer -> Run `nPlayServer --help` to get a list of available options. +On Windows, download and install the latest version of Cerebus Central Suite from the [Blackrock Neurotech support website (scroll down)](https://blackrockneurotech.com/support/). You may also wish to download some sample data from this same website. -* Emulating Legacy NSP: `nPlayServer -L --network inst=192.168.137.128:51001 --network bcast=192.168.137.255:51002` -* Emulating Gemini Hub: `nPlayServer -L --network inst=192.168.137.200:51002 --network bcast=192.168.137.255:51002` +### Testing on localhost -### cerelink Python +Navigate to `C:\Program Files\Blackrock Microsystems\Cerebus Central Suite\` and run `runNPlayAndCentral.bat`. This runs a device simulator (nPlayServer) and Central on localhost loopback. -See [bindings/Python/README.md](./bindings/Python/README.md) for usage and build instructions. +### Testing on network -## Getting Help +Configure IP addresses: client at 192.168.137.198 or .199, device at 192.168.137.128 (NSP) or 192.168.137.200 (Gemini Hub). -First, read the frequently asked questions and answers in the [project wiki](https://github.com/CerebusOSS/CereLink/wiki). +Run `nPlayServer --help` for options: +* Legacy NSP: `nPlayServer -L --network inst=192.168.137.128:51001 --network bcast=192.168.137.255:51002` +* Gemini Hub: `nPlayServer -L --network inst=192.168.137.200:51002 --network bcast=192.168.137.255:51002` -Second, search the [issues on GitHub](https://github.com/CerebusOSS/CereLink/issues). +### Linux Network + +**Firewall:** +``` +sudo ufw allow in on enp6s0 from 192.168.137.0/24 to any port 51001:51005 proto udp +``` +Replace `enp6s0` with your ethernet adapter. + +**Socket Buffer Size:** +``` +echo "net.core.rmem_max=16777216" | sudo tee -a /etc/sysctl.d/99-cerebus.conf +echo "net.core.rmem_default=8388608" | sudo tee -a /etc/sysctl.d/99-cerebus.conf +``` +Then reboot. + +## Getting Help -Finally, open an issue. +1. Read the [project wiki](https://github.com/CerebusOSS/CereLink/wiki) +2. Search [issues on GitHub](https://github.com/CerebusOSS/CereLink/issues) +3. Open an issue This is a community project and is not officially supported by Blackrock Neurotech. diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt deleted file mode 100644 index 343b18e1..00000000 --- a/bindings/CMakeLists.txt +++ /dev/null @@ -1,167 +0,0 @@ -# Language Wrappers CMake Build System -# This file contains build configuration for all language wrappers: -# - MATLAB (cbmex) -# - Octave (cboct) -# - C++/CLI - -########################################################################################## -# Find language-specific dependencies - -# -Matlab -IF(${CBSDK_BUILD_CBMEX}) - # Try MATLAB locally first, then on MATLAB install - FIND_PATH( Matlab_INCLUDE_DIRS - "mex.h" - "${PROJECT_SOURCE_DIR}/wrappers/Matlab/include" - ) - IF ( Matlab_INCLUDE_DIRS ) - # Local Matlab mex libraries are stored in platform-specific paths - IF ( WIN32 ) - SET( PLATFORM_NAME "win" ) - ELSEIF ( APPLE ) - SET( PLATFORM_NAME "osx" ) - ELSE ( WIN32 ) - SET( PLATFORM_NAME "linux" ) - ENDIF ( WIN32 ) - IF( CMAKE_SIZEOF_VOID_P EQUAL 4 ) - SET( PLATFORM_NAME ${PLATFORM_NAME}32 ) - ELSE( CMAKE_SIZEOF_VOID_P EQUAL 4 ) - SET( PLATFORM_NAME ${PLATFORM_NAME}64 ) - ENDIF( CMAKE_SIZEOF_VOID_P EQUAL 4 ) - IF(MSVC) - SET( PLATFORM_NAME ${PLATFORM_NAME}/microsoft ) - ELSEIF(WIN32) - SET( PLATFORM_NAME ${PLATFORM_NAME}/mingw64 ) - ENDIF(MSVC) - - SET( MATLAB_ROOT "${PROJECT_SOURCE_DIR}/wrappers/Matlab" ) - MESSAGE ( STATUS "Search mex libraries at " ${Matlab_INCLUDE_DIRS}/../lib/${PLATFORM_NAME} ) - FILE( GLOB_RECURSE Matlab_LIBRARIES ${Matlab_INCLUDE_DIRS}/../lib/${PLATFORM_NAME}/libm*.* ) - IF( Matlab_LIBRARIES ) - SET( MATLAB_FOUND 1 ) - ENDIF( Matlab_LIBRARIES ) - ELSE ( Matlab_INCLUDE_DIRS ) - #SET( MATLAB_FIND_DEBUG 1 ) - FIND_PACKAGE( Matlab COMPONENTS MX_LIBRARY) - ENDIF ( Matlab_INCLUDE_DIRS ) -ENDIF() - -# -Octave -IF(${CBSDK_BUILD_CBOCT}) - FIND_PACKAGE( Octave ) -ENDIF() - -########################################################################################## -# Source for both cbmex and octave targets -SET( LIB_SOURCE_CBMEX - ${PROJECT_SOURCE_DIR}/wrappers/cbmex/cbmex.cpp -) -IF( MSVC ) - LIST ( APPEND LIB_SOURCE_CBMEX ${PROJECT_SOURCE_DIR}/wrappers/cbmex/cbMex.rc ) -ENDIF( MSVC ) - -########################################################################################## -# cbmex target -IF(${CBSDK_BUILD_CBMEX} AND MATLAB_FOUND ) - MESSAGE ( STATUS "Add cbmex build target using MATLAB libs at " ${Matlab_ROOT_DIR}) - ADD_LIBRARY( ${LIB_NAME_CBMEX} SHARED ${LIB_SOURCE_CBMEX} ) - - # Want package name to be cbmex without prefix - IF( WIN32 ) - # Do not output to Debug/Release directories on Windows. - # Commented out because it causes 'multiple rules generate cbmex.mexw64' error - # SET_TARGET_PROPERTIES( ${LIB_NAME_CBMEX} PROPERTIES PREFIX "../") - IF (MSVC) - # Manually export mexFunction because __declspec(dllexport) conflicts with its definition in mex.h - SET_TARGET_PROPERTIES( ${LIB_NAME_CBMEX} PROPERTIES - LINK_FLAGS "/EXPORT:mexFunction" - # MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" - ) - ENDIF(MSVC) - ELSEIF( APPLE ) - SET_TARGET_PROPERTIES( ${LIB_NAME_CBMEX} PROPERTIES PREFIX "" ) - # This is for normal users of MATLAB on OSX without homebrew - # so we try to use relative paths to be able to bundle shared libraries - SET_TARGET_PROPERTIES( ${LIB_NAME_CBMEX} PROPERTIES BUILD_WITH_INSTALL_RPATH 1 INSTALL_NAME_DIR "@rpath") - ELSE( WIN32 ) - SET_TARGET_PROPERTIES( ${LIB_NAME_CBMEX} PROPERTIES PREFIX "" ) - SET_TARGET_PROPERTIES( ${LIB_NAME_CBMEX} PROPERTIES LINK_FLAGS "-Wl,--exclude-libs,ALL" ) - ENDIF( WIN32 ) - - SET_TARGET_PROPERTIES( ${LIB_NAME_CBMEX} PROPERTIES SUFFIX .${Matlab_MEX_EXTENSION}) - IF( NOT CBMEX_INSTALL_PREFIX ) - SET( CBMEX_INSTALL_PREFIX .) - ENDIF( NOT CBMEX_INSTALL_PREFIX ) - # Use static library to build cbmex - ADD_DEPENDENCIES( ${LIB_NAME_CBMEX} ${LIB_NAME_STATIC} ) - TARGET_INCLUDE_DIRECTORIES( ${LIB_NAME_CBMEX} - PRIVATE ${Matlab_INCLUDE_DIRS} - ) - TARGET_LINK_LIBRARIES( ${LIB_NAME_CBMEX} - ${LIB_NAME_STATIC} - ${Matlab_LIBRARIES} - ) - INSTALL( TARGETS ${LIB_NAME_CBMEX} - RUNTIME DESTINATION ${CBMEX_INSTALL_PREFIX}/CereLink - LIBRARY DESTINATION ${CBMEX_INSTALL_PREFIX}/CereLink - ARCHIVE DESTINATION ${CBMEX_INSTALL_PREFIX}/CereLink - ) -ENDIF( ${CBSDK_BUILD_CBMEX} AND MATLAB_FOUND ) - -########################################################################################## -# octave target -IF( ${CBSDK_BUILD_CBOCT} AND OCTAVE_FOUND ) - MESSAGE ( STATUS "Add cbmex build target using Octave libs at " ${OCTAVE_OCT_LIB_DIR}) - ADD_LIBRARY( ${LIB_NAME_CBOCT} SHARED ${LIB_SOURCE_CBMEX} ) - - # Want package name to be cbmex without prefix - IF( WIN32 ) - # Do not output to Debug/Release directories on Windows - SET_TARGET_PROPERTIES( ${LIB_NAME_CBOCT} PROPERTIES PREFIX "../" ) - # Manually export mexFunction because __declspec(dllexport) conflicts with its definition in mex.h - SET_TARGET_PROPERTIES( ${LIB_NAME_CBOCT} PROPERTIES LINK_FLAGS "/EXPORT:mexFunction" ) - ELSEIF( APPLE ) - SET_TARGET_PROPERTIES( ${LIB_NAME_CBOCT} PROPERTIES PREFIX "" ) - # This is for normal users of MATLAB on OSX without homebrew - # so we try to use relative paths to be able to bundle shared libraries - SET_TARGET_PROPERTIES( ${LIB_NAME_CBOCT} PROPERTIES BUILD_WITH_INSTALL_RPATH 1 INSTALL_NAME_DIR "@rpath") - ELSE( WIN32 ) - SET_TARGET_PROPERTIES( ${LIB_NAME_CBOCT} PROPERTIES PREFIX "" ) - SET_TARGET_PROPERTIES( ${LIB_NAME_CBOCT} PROPERTIES LINK_FLAGS "-Wl,--exclude-libs,ALL" ) - ENDIF( WIN32 ) - - SET_TARGET_PROPERTIES( ${LIB_NAME_CBOCT} PROPERTIES SUFFIX .mex) - IF( NOT CBMEX_INSTALL_PREFIX ) - SET( CBMEX_INSTALL_PREFIX .) - ENDIF( NOT CBMEX_INSTALL_PREFIX ) - # Use static library to build cbmex - ADD_DEPENDENCIES( ${LIB_NAME_CBOCT} ${LIB_NAME_STATIC} ) - TARGET_INCLUDE_DIRECTORIES( ${LIB_NAME_CBOCT} - PRIVATE ${OCTAVE_INCLUDE_DIR} - ) - TARGET_LINK_LIBRARIES( ${LIB_NAME_CBOCT} ${LIB_NAME_STATIC} ${OCTAVE_LIBRARIES} ) - INSTALL( TARGETS ${LIB_NAME_CBOCT} - RUNTIME DESTINATION ${CBMEX_INSTALL_PREFIX}/CereLink - LIBRARY DESTINATION ${CBMEX_INSTALL_PREFIX}/CereLink - ARCHIVE DESTINATION ${CBMEX_INSTALL_PREFIX}/CereLink - ) -ENDIF( ${CBSDK_BUILD_CBOCT} AND OCTAVE_FOUND ) - -########################################################################################## -# C++ / CLI -add_subdirectory(cli) - - -## SWIG -- not working; segfault on import -#find_package(SWIG REQUIRED) -#include(${SWIG_USE_FILE}) -#find_package(Python REQUIRED COMPONENTS Development) -#set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/cbmex/cbmex.i PROPERTIES CPLUSPLUS ON) # for C++ types like bool -#set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/cbmex/cbmex.i PROPERTIES SWIG_FLAGS "-builtin") -#swig_add_library(cbsdk_swig -# LANGUAGE python -# SOURCES ${CMAKE_CURRENT_LIST_DIR}/cbmex/cbmex.i -#) -#set_target_properties(cbsdk_swig PROPERTIES SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON) -#set_target_properties(cbsdk_swig PROPERTIES COMPILE_FLAGS "-fvisibility=hidden -fvisibility-inlines-hidden") -#swig_link_libraries(cbsdk_swig PRIVATE cbsdk Python::Python) diff --git a/bindings/Matlab/ReadMe.txt b/bindings/Matlab/ReadMe.txt deleted file mode 100755 index 31f89ba7..00000000 --- a/bindings/Matlab/ReadMe.txt +++ /dev/null @@ -1,5 +0,0 @@ -This is the contents of the "Extern" directory -from the MATLAB installation. - -These are from version 6.1 of Matlab. -You can install a 30-day trial of MATLAB, then use the header and library files in the extern directory for development. diff --git a/bindings/Matlab/include/PlaceHolder.txt b/bindings/Matlab/include/PlaceHolder.txt deleted file mode 100755 index c401fec8..00000000 --- a/bindings/Matlab/include/PlaceHolder.txt +++ /dev/null @@ -1,30 +0,0 @@ -Place holder for these header files (some may be unnecessary): - -blas.h -blascompat32.h -boz.txt -emlrt.h -engine.h -fintrf.h -io64.h -lapack.h -libeng.def -libmat.def -libmex.def -libmwsglm.def -libmx.def -mat.h -matrix.h -mex.h -mexversion.rc -mwdebug.h -sgl.def -tmwtypes.h -_libeng.def -_libmat.def -_libmatlbmx.def -_libmex.def -_libmwsglm.def -_libmx.def -_mex.def -_sgl.def diff --git a/bindings/Python/README.md b/bindings/Python/README.md deleted file mode 100644 index d3bb177d..00000000 --- a/bindings/Python/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# cerelink - -This is a Python wrapper for the CereLink (cbsdk) library to configure, pull data, and receive callbacks from Blackrock Neurotech devices. - -> Note: If you do not require Central running on the same computer as your Python Blackrock device client, then consider instead using [pycbsdk](https://github.com/CerebusOSS/pycbsdk). - -## Getting Started - -* Download a wheel from the releases page or [build it yourself](#build-instructions). -* Activate a Python environment with pip, Cython, and numpy -* Install the wheel: `python -m pip install path\to\filename.whl` -* Test with `python -c "from cerelink import cbpy; cbpy.open(parameter=cbpy.defaultConParams())"` - * You might get `RuntimeError: -30, Instrument is offline.`. That's OK, depending on your device and network settings. -* See [cerebuswrapper](https://github.com/CerebusOSS/cerebuswrapper) for a tool that provides a simplified interface to cerelink. - -## Build Instructions - -* If you haven't already, build and install CereLink following the [main BUILD instructions](../../BUILD.md). - * Windows: If using Visual Studio then close it. -* Open a Terminal or Anaconda prompt and activate your Python environment. -* Your Python environment's Python interpreter (CPython, arch, version) must match that of the eventual machine that will run the package, and it must already have Cython, numpy, pip, setuptools, and wheel installed. - * We do not set these as explicit dependencies on the package to avoid bundling them so you must install manually. - * `python -m ensurepip` - * `python -m pip install --upgrade pip setuptools wheel cython numpy` -* Change to the CereLink directory. -* Install locally: `python -m pip install bindings/Python` -* or, if you are making a wheel to bring to another machine, - * activate an environment matching the target machine, - * `python -m pip wheel bindings/Python -w bindings/Python/dist` - * The wheels will be in the `bindings/Python/dist` folder. - * See the [Wiki](https://github.com/CerebusOSS/CereLink/wiki/cerelink) for more information. - -## NumPy Compatibility - -This project is built against **NumPy 2.x**. Users must have NumPy 2.0 or later installed. -If you are having trouble, try to run the following command and report your results: -`python -c "import numpy; print(f'NumPy: {numpy.__version__}'); from cerelink import cbpy; print('Success!')"` - -If you need to support older NumPy 1.x versions, you can build wheels with: - -```toml -[build-system] -requires = ["setuptools", "wheel", "cython", "oldest-supported-numpy", "setuptools-scm"] - -[project] -dependencies = [ - "numpy>=1.23.0,<2.0", -] -``` diff --git a/bindings/Python/cerelink/__init__.py b/bindings/Python/cerelink/__init__.py deleted file mode 100644 index 6870722f..00000000 --- a/bindings/Python/cerelink/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -try: - from cerelink.__version__ import version as __version__ -except ImportError: - # Version file not generated yet (e.g., in development before first build) - __version__ = "0.0.0.dev0" diff --git a/bindings/Python/cerelink/cbpy.pyx b/bindings/Python/cerelink/cbpy.pyx deleted file mode 100644 index af626de7..00000000 --- a/bindings/Python/cerelink/cbpy.pyx +++ /dev/null @@ -1,1157 +0,0 @@ -# distutils: language = c++ -# cython: language_level=2 -from cbsdk_cython cimport * -from libcpp cimport bool -from libc.stdlib cimport malloc, free -from libc.string cimport strncpy -cimport numpy as cnp -cimport cython - -import sys -import locale -import typing - -import numpy as np - -# Initialize numpy C API -cnp.import_array() - -# NumPy array flags for ownership -cdef extern from "numpy/arrayobject.h": - cdef int NPY_ARRAY_OWNDATA - - -# Determine the correct numpy dtype for PROCTIME based on its size -cdef object _proctime_dtype(): - if sizeof(PROCTIME) == 4: - return np.uint32 - elif sizeof(PROCTIME) == 8: - return np.uint64 - else: - raise RuntimeError("Unexpected PROCTIME size: %d" % sizeof(PROCTIME)) - -# Cache the dtype for performance -_PROCTIME_DTYPE = _proctime_dtype() - - -def version(int instance=0): - """Get library version - Inputs: - instance - (optional) library instance number - Outputs:" - dictionary with following keys - major - major API version - minor - minor API version - release - release API version - beta - beta API version (0 if a non-beta) - protocol_major - major protocol version - protocol_minor - minor protocol version - nsp_major - major NSP firmware version - nsp_minor - minor NSP firmware version - nsp_release - release NSP firmware version - nsp_beta - beta NSP firmware version (0 if non-beta)) - nsp_protocol_major - major NSP protocol version - nsp_protocol_minor - minor NSP protocol version - """ - - cdef cbSdkResult res - cdef cbSdkVersion ver - - res = cbSdkGetVersion(instance, &ver) - handle_result(res) - - ver_dict = {'major':ver.major, 'minor':ver.minor, 'release':ver.release, 'beta':ver.beta, - 'protocol_major':ver.majorp, 'protocol_minor':ver.majorp, - 'nsp_major':ver.nspmajor, 'nsp_minor':ver.nspminor, 'nsp_release':ver.nsprelease, 'nsp_beta':ver.nspbeta, - 'nsp_protocol_major':ver.nspmajorp, 'nsp_protocol_minor':ver.nspmajorp - } - return res, ver_dict - - -def defaultConParams(): - #Note: Defaulting to 255.255.255.255 assumes the client is connected to the NSP via a switch. - #A direct connection might require the client-addr to be "192.168.137.1" - con_parms = { - 'client-addr': str(cbNET_UDP_ADDR_BCAST.decode("utf-8"))\ - if ('linux' in sys.platform or 'linux2' in sys.platform) else '255.255.255.255', - 'client-port': cbNET_UDP_PORT_BCAST, - 'inst-addr': cbNET_UDP_ADDR_CNT.decode("utf-8"), - 'inst-port': cbNET_UDP_PORT_CNT, - 'receive-buffer-size': (8 * 1024 * 1024) if sys.platform == 'win32' else (6 * 1024 * 1024) - } - return con_parms - - -def open(int instance=0, connection: str = 'default', parameter: dict[str, str | int] | None = None) -> dict[str, str]: - """Open library. - Inputs: - connection - connection type, string can be one of the following - 'default': tries slave then master connection - 'master': tries master connection (UDP) - 'slave': tries slave connection (needs another master already open) - parameter - dictionary with following keys (all optional) - 'inst-addr': instrument IPv4 address. - 'inst-port': instrument port number. - 'client-addr': client IPv4 address. - 'client-port': client port number. - 'receive-buffer-size': override default network buffer size (low value may result in drops). - instance - (optional) library instance number - Outputs: - Same as "get_connection_type" command output - - Test with: -from cerelink import cbpy -cbpy.open(parameter=cbpy.defaultConParams()) - """ - cdef cbSdkResult res - - wconType = {'default': CBSDKCONNECTION_DEFAULT, 'slave': CBSDKCONNECTION_CENTRAL, 'master': CBSDKCONNECTION_UDP} - if not connection in wconType.keys(): - raise RuntimeError("invalid connection %s" % connection) - - cdef cbSdkConnectionType conType = wconType[connection] - cdef cbSdkConnection con - - parameter = parameter if parameter is not None else {} - cdef bytes szOutIP = parameter.get('inst-addr', cbNET_UDP_ADDR_CNT.decode("utf-8")).encode() - cdef bytes szInIP = parameter.get('client-addr', '').encode() - - con.szOutIP = szOutIP - con.nOutPort = parameter.get('inst-port', cbNET_UDP_PORT_CNT) - con.szInIP = szInIP - con.nInPort = parameter.get('client-port', cbNET_UDP_PORT_BCAST) - con.nRecBufSize = parameter.get('receive-buffer-size', 0) - - res = cbSdkOpen(instance, conType, con) - - handle_result(res) - - return get_connection_type(instance=instance) - - -def close(int instance=0): - """Close library. - Inputs: - instance - (optional) library instance number - """ - return handle_result(cbSdkClose(instance)) - - -def get_connection_type(int instance=0) -> dict[str, str]: - """ Get connection type - Inputs: - instance - (optional) library instance number - Outputs: - dictionary with following keys - 'connection': Final established connection; can be any of: - 'Default', 'Slave', 'Master', 'Closed', 'Unknown' - 'instrument': Instrument connected to; can be any of: - 'NSP', 'nPlay', 'Local NSP', 'Remote nPlay', 'Unknown') - """ - - cdef cbSdkResult res - cdef cbSdkConnectionType conType - cdef cbSdkInstrumentType instType - - res = cbSdkGetType(instance, &conType, &instType) - - handle_result(res) - - connections = ["Default", "Slave", "Master", "Closed", "Unknown"] - instruments = ["NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"] - - con_idx = conType - if con_idx < 0 or con_idx >= len(connections): - con_idx = len(connections) - 1 - inst_idx = instType - if inst_idx < 0 or inst_idx >= len(instruments): - inst_idx = len(instruments) - 1 - - return {"connection": connections[con_idx], "instrument": instruments[inst_idx]} - - -def trial_config( - int instance=0, - activate=True, - n_continuous: int = 0, - n_event: int = 0, - n_comment: int = 0, - n_tracking: int = 0, - begin_channel: int = 0, - begin_mask: int = 0, - begin_value: int = 0, - end_channel: int = 0, - end_mask: int = 0, - end_value: int = 0, - ) -> None: - """ - Configure trial settings. See cbSdkSetTrialConfig in cbsdk.h for details. - - Inputs: - activate - boolean, set True to flush data cache and start collecting data immediately, - set False to stop collecting data immediately or, if not already in a trial, - to rely on the begin/end channel mask|value to start/stop collecting data. - n_continuous - integer, number of continuous data samples to be cached. - 0 means no continuous data is cached. -1 will use cbSdk_CONTINUOUS_DATA_SAMPLES. - n_event - integer, number of events to be cached. - 0 means no events are captured. -1 will use cbSdk_EVENT_DATA_SAMPLES. - n_comment - integer, number of comments to be cached. - n_tracking - integer, number of video tracking events to be cached. Deprecated. - begin_channel - integer, channel to monitor to trigger trial start - Ignored if activate is True. - begin_mask - integer, mask to bitwise-and with data on begin_channel to look for trigger - Ignored if activate is True. - begin_value - value to trigger trial start - Ignored if activate is True. - end_channel - integer, channel to monitor to trigger trial stop - Ignored if activate is True. - end_mask - mask to bitwise-and with data on end_channel to evaluate trigger - Ignored if activate is True. - end_value - value to trigger trial stop - Ignored if activate is True. - instance - (optional) library instance number - """ - - cdef cbSdkResult res - cdef cbSdkConfigParam cfg_param - - # retrieve old values - res = cbsdk_get_trial_config(instance, &cfg_param) - handle_result(res) - - cfg_param.bActive = activate - - # Fill cfg_param with provided buffer_parameter values or default. - cfg_param.uWaveforms = 0 # waveforms do not work - cfg_param.uConts = n_continuous if n_continuous >= 0 else cbSdk_CONTINUOUS_DATA_SAMPLES - cfg_param.uEvents = n_event if n_event >= 0 else cbSdk_EVENT_DATA_SAMPLES - cfg_param.uComments = n_comment or 0 - cfg_param.uTrackings = n_tracking or 0 - - # Fill cfg_param mask-related parameters with provided range_parameter or default. - cfg_param.Begchan = begin_channel - cfg_param.Begmask = begin_mask - cfg_param.Begval = begin_value - cfg_param.Endchan = end_channel - cfg_param.Endmask = end_mask - cfg_param.Endval = end_value - - res = cbsdk_set_trial_config(instance, &cfg_param) - - handle_result(res) - - -def trial_event(int instance=0, bool seek=False, bool reset_clock=False): - """ Trial spike and event data. - Inputs: - instance - (optional) library instance number - seek - (optional) boolean - set False (default) to leave buffer intact -- peek only. - set True to clear all the data after it has been retrieved. - reset_clock - (optional) boolean - set False (default) to leave the trial clock alone. - set True to update the _next_ trial time to the current time. - This is overly complicated. Leave this as `False` unless you really know what you are doing. - Note: All timestamps are now in absolute device time. - Outputs: - list of arrays [channel, {'timestamps':[unit0_ts, ..., unitN_ts], 'events':digital_events}] - channel: integer, channel number (1-based) - digital_events: array, digital event values for channel (if a digital or serial channel) - unitN_ts: array, spike timestamps of unit N for channel (if an electrode channel)); - """ - - cdef cbSdkResult res - cdef cbSdkTrialEvent trialevent - - # get how many samples are available - res = cbsdk_init_trial_event(instance, reset_clock, &trialevent) - handle_result(res) - - if trialevent.num_events == 0: - return res, {'timestamps': np.array([], dtype=_PROCTIME_DTYPE), - 'channels': np.array([], dtype=np.uint16), - 'units': np.array([], dtype=np.uint16)} - - # Allocate flat arrays for event data - cdef cnp.ndarray timestamps = np.zeros(trialevent.num_events, dtype=_PROCTIME_DTYPE) - cdef cnp.ndarray channels = np.zeros(trialevent.num_events, dtype=np.uint16) - cdef cnp.ndarray units = np.zeros(trialevent.num_events, dtype=np.uint16) - - # Point C structure to our numpy arrays - trialevent.timestamps = cnp.PyArray_DATA(timestamps) - trialevent.channels = cnp.PyArray_DATA(channels) - trialevent.units = cnp.PyArray_DATA(units) - trialevent.waveforms = NULL # TODO: Add waveform support if needed - - # get the trial - res = cbsdk_get_trial_event(instance, seek, &trialevent) - handle_result(res) - - trial = { - 'timestamps': timestamps, - 'channels': channels, - 'units': units - } - - return res, trial - - -def trial_continuous( - int instance=0, - uint8_t group=0, - bool seek=True, - bool reset_clock=False, - timestamps=None, - samples=None, - uint32_t num_samples=0 -) -> dict[str, typing.Any]: - """ - Trial continuous data for a specific sample group. - - Inputs: - instance - (optional) library instance number - group - (optional) sample group to retrieve (0-6), default=0 will always return an empty result. - seek - (optional) boolean - set True (default) to advance read pointer after retrieval. - set False to peek -- data remains in buffer for next call. - reset_clock - (optional) boolean - Keep False (default) unless you really know what you are doing. - timestamps - (optional) pre-allocated numpy array for timestamps, shape=[num_samples], dtype=uint32 or uint64 - If None, function will allocate - samples - (optional) pre-allocated numpy array for samples, shape=[num_samples, num_channels], dtype=int16 - If None, function will allocate - num_samples - (optional) maximum samples to read. If 0 and arrays provided, inferred from array shape. - If arrays provided and this is specified, reads min(num_samples, array_size) - - Outputs: - data - dictionary with keys: - 'group': group number (0-6) - 'count': number of channels in this group - 'chan': array of channel IDs (1-based) - 'num_samples': actual number of samples read - 'trial_start_time': PROCTIME timestamp when the current **trial** started - 'timestamps': numpy array [num_samples] of PROCTIME timestamps (absolute device time) - 'samples': numpy array [num_samples, count] of int16 sample data - """ - cdef cbSdkResult res - cdef cbSdkTrialCont trialcont - cdef cnp.ndarray timestamps_array - cdef cnp.ndarray samples_array - cdef bool user_provided_arrays = timestamps is not None and samples is not None - - # Set the group we want to retrieve - trialcont.group = group - - # Initialize - get channel count and buffer info for this group - res = cbSdkInitTrialData(instance, reset_clock, NULL, &trialcont, NULL, NULL, 0) - handle_result(res) - - if trialcont.count == 0: - # No channels in this group - return { - "group": group, - "count": 0, - "chan": np.array([], dtype=np.uint16), - "num_samples": 0, - "trial_start_time": trialcont.trial_start_time, - "timestamps": np.array([], dtype=_PROCTIME_DTYPE), - "samples": np.array([], dtype=np.int16).reshape(0, 0) - } - - # Determine num_samples to request - if user_provided_arrays: - # Validate and use user arrays - if not isinstance(timestamps, np.ndarray) or not isinstance(samples, np.ndarray): - raise ValueError("timestamps and samples must both be numpy arrays") - - # Check dtypes - if timestamps.dtype not in [np.uint32, np.uint64]: - raise ValueError(f"timestamps must be dtype uint32 or uint64, got {timestamps.dtype}") - if samples.dtype != np.int16: - raise ValueError(f"samples must be dtype int16, got {samples.dtype}") - - # Check contiguity - if not timestamps.flags['C_CONTIGUOUS']: - raise ValueError("timestamps array must be C-contiguous") - if not samples.flags['C_CONTIGUOUS']: - raise ValueError("samples array must be C-contiguous") - - # Check shapes - if timestamps.ndim != 1: - raise ValueError(f"timestamps must be 1D array, got shape {timestamps.shape}") - if samples.ndim != 2: - raise ValueError(f"samples must be 2D array, got shape {samples.shape}") - - if timestamps.shape[0] != samples.shape[0]: - raise ValueError(f"timestamps and samples must have same length: {timestamps.shape[0]} != {samples.shape[0]}") - - if samples.shape[1] != trialcont.count: - raise ValueError(f"samples shape[1] must match channel count: {samples.shape[1]} != {trialcont.count} expected") - - # Determine num_samples from arrays if not specified - if num_samples == 0: - num_samples = timestamps.shape[0] - else: - # Use minimum of requested and available array size - num_samples = min(num_samples, timestamps.shape[0]) - - timestamps_array = timestamps - samples_array = samples - else: - # Allocate arrays - if num_samples == 0: - num_samples = trialcont.num_samples # Use what Init reported as available - - timestamps_array = np.empty(num_samples, dtype=_PROCTIME_DTYPE) - samples_array = np.empty((num_samples, trialcont.count), dtype=np.int16) - - # Set num_samples and point to array data - trialcont.num_samples = num_samples - trialcont.timestamps = cnp.PyArray_DATA(timestamps_array) - trialcont.samples = cnp.PyArray_DATA(samples_array) - - # Get the data - res = cbSdkGetTrialData(instance, seek, NULL, &trialcont, NULL, NULL) - handle_result(res) - - # Build channel array - cdef cnp.ndarray chan_array = np.empty(trialcont.count, dtype=np.uint16) - cdef uint16_t[::1] chan_view = chan_array - cdef int i - for i in range(trialcont.count): - chan_view[i] = trialcont.chan[i] - - # Prepare output - actual num_samples may be less than requested - cdef uint32_t actual_samples = trialcont.num_samples - - return { - 'group': group, - 'count': trialcont.count, - 'chan': chan_array, - 'num_samples': actual_samples, - 'trial_start_time': trialcont.trial_start_time, - 'timestamps': timestamps_array[:actual_samples], - 'samples': samples_array[:actual_samples, :] - } - - -def trial_data(int instance=0, bool seek=False, bool reset_clock=False, - bool do_event=True, bool do_cont=True, bool do_comment=False, unsigned long wait_for_comment_msec=250): - """ - - :param instance: (optional) library instance number - :param seek: (optional) boolean - set False (default) to peek only and leave buffer intact. - set True to advance the data pointer after retrieval. - :param reset_clock - (optional) boolean - set False (default) to leave the trial clock alone. - set True to update the _next_ trial time to the current time. - This is overly complicated. Leave this as `False` unless you really know what you are doing. - Note: All timestamps are now in absolute device time. - :param do_event: (optional) boolean. Set False to skip fetching events. - :param do_cont: (optional) boolean. Set False to skip fetching continuous data. - :param do_comment: (optional) boolean. Set to True to fetch comments. - :param wait_for_comment_msec: (optional) unsigned long. How long we should wait for new comments. - Default (0) will not wait and will only return comments that existed prior to calling this. - :return: (result, event_data, continuous_data, t_zero, comment_data) - res: (int) returned by cbsdk - event data: list of arrays [channel, {'timestamps':[unit0_ts, ..., unitN_ts], 'events':digital_events}] - channel: integer, channel number (1-based) - digital_events: array, digital event values for channel (if a digital or serial channel) - unitN_ts: array, spike timestamps of unit N for channel (if an electrode channel) - Note: timestamps are PROCTIME (uint32 or uint64), events are uint16 - continuous data: list of dictionaries, one per sample group (0-7) that has channels: - { - 'group': group number (0-7), - 'count': number of channels, - 'chan': numpy array of channel IDs (1-based), - 'sample_rate': sample rate (Hz), - 'num_samples': number of samples, - 'timestamps': numpy array [num_samples] of PROCTIME, - 'samples': numpy array [num_samples, count] of int16 - } - t_zero: deprecated, always 0 - comment_data: list of lists the form [timestamp, comment_str, charset, rgba] - timestamp: PROCTIME - comment_str: the comment in binary. - Use comment_str.decode('utf-16' if charset==1 else locale.getpreferredencoding()) - rgba: integer; the comment colour. 8 bits each for r, g, b, a - """ - - cdef cbSdkResult res - cdef cbSdkTrialEvent trialevent - cdef cbSdkTrialComment trialcomm - cdef cbSdkTrialCont trialcont_group - # cdef uint8_t ch_type - cdef uint32_t b_dig_in - cdef uint32_t b_serial - - cdef uint32_t tzero = 0 - cdef int comm_ix - cdef uint32_t num_samples - cdef int channel - cdef uint16_t ch - cdef int u - cdef int g, j - cdef uint32_t actual_samples_grp - - cdef cnp.ndarray mxa_proctime - cdef cnp.uint16_t[:] mxa_u16 - cdef cnp.uint8_t[:] mxa_u8 - cdef cnp.uint32_t[:] mxa_u32 - cdef cnp.ndarray timestamps_grp - cdef cnp.ndarray samples_grp - cdef cnp.ndarray chan_array_grp - cdef uint16_t[::1] chan_view_grp - - trial_event = [] - trial_cont = [] - trial_comment = [] - - # get how many samples are available - # Note: continuous data is now handled per-group below, so we don't init it here - res = cbsdk_init_trial_data(instance, reset_clock, &trialevent if do_event else NULL, - NULL, &trialcomm if do_comment else NULL, - wait_for_comment_msec) - handle_result(res) - - # Early return if none of the requested data are available. - # For continuous, we'll check per-group below - if (not do_event or (trialevent.num_events == 0)) \ - and (not do_comment or (trialcomm.num_samples == 0)) \ - and (not do_cont): - return res, trial_event, trial_cont, tzero, trial_comment - - # Events # - # ------ # - if do_event and trialevent.num_events > 0: - # Allocate flat arrays for event data - timestamps = np.zeros(trialevent.num_events, dtype=_PROCTIME_DTYPE) - channels = np.zeros(trialevent.num_events, dtype=np.uint16) - units = np.zeros(trialevent.num_events, dtype=np.uint16) - - # Point C structure to our numpy arrays - trialevent.timestamps = cnp.PyArray_DATA(timestamps) - trialevent.channels = cnp.PyArray_DATA(channels) - trialevent.units = cnp.PyArray_DATA(units) - trialevent.waveforms = NULL # TODO: Add waveform support if needed - - trial_event = { - 'timestamps': timestamps, - 'channels': channels, - 'units': units - } - - # Continuous # - # ---------- # - # With the new group-based API, we fetch each group (0-7) separately - if do_cont: - # Fetch all 8 groups - for g in range(8): # Groups 0-7 (cbMAXGROUPS) - trialcont_group.group = g - - # Initialize this group - res = cbSdkInitTrialData(instance, reset_clock, NULL, &trialcont_group, NULL, NULL, 0) - if res != CBSDKRESULT_SUCCESS or trialcont_group.count == 0: - continue # Skip groups with no channels - - # Allocate arrays for this group - num_samples = trialcont_group.num_samples - timestamps_grp = np.empty(num_samples, dtype=_PROCTIME_DTYPE) - samples_grp = np.empty((num_samples, trialcont_group.count), dtype=np.int16) - - # Point trialcont to arrays - trialcont_group.num_samples = num_samples - trialcont_group.timestamps = cnp.PyArray_DATA(timestamps_grp) - trialcont_group.samples = cnp.PyArray_DATA(samples_grp) - - # Get data for this group - res = cbSdkGetTrialData(instance, 0, NULL, &trialcont_group, NULL, NULL) - if res != CBSDKRESULT_SUCCESS: - continue - - # Build channel array - chan_array_grp = np.empty(trialcont_group.count, dtype=np.uint16) - chan_view_grp = chan_array_grp - for j in range(trialcont_group.count): - chan_view_grp[j] = trialcont_group.chan[j] - - actual_samples_grp = trialcont_group.num_samples - - # Append group data as dictionary - trial_cont.append({ - 'group': g, - 'count': trialcont_group.count, - 'chan': chan_array_grp, - 'sample_rate': trialcont_group.sample_rate, - 'num_samples': actual_samples_grp, - 'timestamps': timestamps_grp[:actual_samples_grp], - 'samples': samples_grp[:actual_samples_grp, :] - }) - - # Comments # - # -------- # - if do_comment and (trialcomm.num_samples > 0): - # Allocate memory - # For charsets; - mxa_u8 = np.zeros(trialcomm.num_samples, dtype=np.uint8) - trialcomm.charsets = &mxa_u8[0] - my_charsets = np.asarray(mxa_u8) - # For rgbas - mxa_u32 = np.zeros(trialcomm.num_samples, dtype=np.uint32) - trialcomm.rgbas = &mxa_u32[0] - my_rgbas = np.asarray(mxa_u32) - # For timestamps - mxa_proctime = np.zeros(trialcomm.num_samples, dtype=_PROCTIME_DTYPE) - trialcomm.timestamps = cnp.PyArray_DATA(mxa_proctime) - my_timestamps = mxa_proctime - # For comments - trialcomm.comments = malloc(trialcomm.num_samples * sizeof(uint8_t*)) - for comm_ix in range(trialcomm.num_samples): - trialcomm.comments[comm_ix] = malloc(256 * sizeof(uint8_t)) - trial_comment.append([my_timestamps[comm_ix], trialcomm.comments[comm_ix], - my_charsets[comm_ix], my_rgbas[comm_ix]]) - - # cbsdk get trial data - # Note: continuous data was already fetched per-group above - try: - if do_event or do_comment: - res = cbsdk_get_trial_data(instance, seek, - &trialevent if do_event else NULL, - NULL, # continuous handled per-group above - &trialcomm if do_comment else NULL) - handle_result(res) - finally: - if do_comment: - free(trialcomm.comments) - - # Note: tzero is no longer meaningful with group-based continuous data - return res, trial_event, trial_cont, tzero, trial_comment - - -def trial_comment(int instance=0, bool seek=False, bool clock_reset=False, unsigned long wait_for_comment_msec=250): - """ Trial comment data. - Inputs: - seek - (optional) boolean - set False (default) to peek only and leave buffer intact. - set True to clear all the data and advance the data pointer. - clock_reset - (optional) boolean - set False (default) to leave the trial clock alone. - set True to update the _next_ trial time to the current time. - instance - (optional) library instance number - Outputs: - list of lists the form [timestamp, comment_str, rgba] - timestamp: PROCTIME - comment_str: the comment as a py string - rgba: integer; the comment colour. 8 bits each for r, g, b, a - """ - - cdef cbSdkResult res - cdef cbSdkTrialComment trialcomm - - # get how many comments are available - res = cbsdk_init_trial_comment(instance, clock_reset, &trialcomm, wait_for_comment_msec) - handle_result(res) - - if trialcomm.num_samples == 0: - return res, [] - - # allocate memory - - # types - cdef cnp.uint8_t[:] mxa_u8_cs # charsets - cdef cnp.uint32_t[:] mxa_u32_rgbas - cdef cnp.ndarray mxa_proctime - - # For charsets; - mxa_u8_cs = np.zeros(trialcomm.num_samples, dtype=np.uint8) - trialcomm.charsets = &mxa_u8_cs[0] - my_charsets = np.asarray(mxa_u8_cs) - - # For rgbas - mxa_u32_rgbas = np.zeros(trialcomm.num_samples, dtype=np.uint32) - trialcomm.rgbas = &mxa_u32_rgbas[0] - my_rgbas = np.asarray(mxa_u32_rgbas) - - # For comments - trialcomm.comments = malloc(trialcomm.num_samples * sizeof(uint8_t*)) - cdef int comm_ix - for comm_ix in range(trialcomm.num_samples): - trialcomm.comments[comm_ix] = malloc(256 * sizeof(uint8_t)) - - # For timestamps - mxa_proctime = np.zeros(trialcomm.num_samples, dtype=_PROCTIME_DTYPE) - trialcomm.timestamps = cnp.PyArray_DATA(mxa_proctime) - my_timestamps = mxa_proctime - - trial = [] - try: - res = cbsdk_get_trial_comment(instance, seek, &trialcomm) - handle_result(res) - for comm_ix in range(trialcomm.num_samples): - # this_enc = 'utf-16' if my_charsets[comm_ix]==1 else locale.getpreferredencoding() - row = [my_timestamps[comm_ix], trialcomm.comments[comm_ix], my_rgbas[comm_ix]] # .decode(this_enc) - trial.append(row) - finally: - free(trialcomm.comments) - - return res, trial - - -def file_config(int instance=0, command='info', comment='', filename='', patient_info=None): - """ Configure remote file recording or get status of recording. - Inputs: - command - string, File configuration command, can be of of the following - 'info': (default) get File recording information - 'open': opens the File dialog if closed, ignoring other parameters - 'close': closes the File dialog if open - 'start': starts recording, opens dialog if closed - 'stop': stops recording - filename - (optional) string, file name to use for recording - comment - (optional) string, file comment to use for file recording - instance - (optional) library instance number - patient_info - (optional) dict carrying patient info. If provided then valid keys and value types: - 'ID': str - required, 'firstname': str - defaults to 'J', 'lastname': str - defaults to 'Doe', - 'DOBMonth': int - defaults to 01, 'DOBDay': int - defaults to 01, 'DOBYear': int - defaults to 1970 - Outputs: - Only if command is 'info' output is returned - A dictionary with following keys: - 'Recording': boolean, if recording is in progress - 'FileName': string, file name being recorded - 'UserName': Computer that is recording - """ - - - cdef cbSdkResult res - cdef char fname[256] - cdef char username[256] - cdef bool bRecording = False - - if command == 'info': - - res = cbSdkGetFileConfig(instance, fname, username, &bRecording) - handle_result(res) - info = {'Recording':bRecording, 'FileName':fname, 'UserName':username} - return res, info - - cdef int start = 0 - cdef unsigned int options = cbFILECFG_OPT_NONE - if command == 'open': - if filename or comment: - raise RuntimeError('filename and comment must not be specified for open') - options = cbFILECFG_OPT_OPEN - elif command == 'close': - options = cbFILECFG_OPT_CLOSE - elif command == 'start': - if not filename: - raise RuntimeError('filename must be specified for start') - start = 1 - elif command == 'stop': - if not filename: - raise RuntimeError('filename must be specified for stop') - start = 0 - else: - raise RuntimeError("invalid file config command %s" % command) - - cdef cbSdkResult patient_res - if start and patient_info is not None and 'ID' in patient_info: - default_patient_info = {'firstname': 'J', 'lastname': 'Doe', 'DOBMonth': 1, 'DOBDay': 1, 'DOBYear': 1970} - patient_info = {**default_patient_info, **patient_info} - for k,v in patient_info.items(): - if type(v) is str: - patient_info[k] = v.encode('UTF-8') - - patient_res = cbSdkSetPatientInfo(instance, - patient_info['ID'], - patient_info['firstname'], - patient_info['lastname'], - patient_info['DOBMonth'], - patient_info['DOBDay'], - patient_info['DOBYear']) - handle_result(patient_res) - - cdef int set_res - filename_string = filename.encode('UTF-8') - comment_string = comment.encode('UTF-8') - set_res = cbsdk_file_config(instance, filename_string, comment_string, start, options) - - - return set_res - - -def time(int instance=0, unit='samples'): - """Instrument time. - Inputs: - unit - time unit, string can be one the following - 'samples': (default) sample number integer - 'seconds' or 's': seconds calculated from samples - 'milliseconds' or 'ms': milliseconds calculated from samples - instance - (optional) library instance number - Outputs: - time - time passed since last reset - """ - - - cdef cbSdkResult res - - if unit == 'samples': - factor = 1 - elif unit in ['seconds', 's']: - raise NotImplementedError("Use time unit of samples for now") - elif unit in ['milliseconds', 'ms']: - raise NotImplementedError("Use time unit of samples for now") - else: - raise RuntimeError("Invalid time unit %s" % unit) - - cdef PROCTIME cbtime - res = cbSdkGetTime(instance, &cbtime) - handle_result(res) - - time = float(cbtime) / factor - - return res, time - - -def analog_out(channel_out, channel_mon, track_last=True, spike_only=False, int instance=0): - """ - Monitor a channel. - Inputs: - channel_out - integer, analog output channel number (1-based) - On NSP, should be >= MIN_CHANS_ANALOG_OUT (273) && <= MAX_CHANS_AUDIO (278) - channel_mon - integer, channel to monitor (1-based) - track_last - (optional) If True, track last channel clicked on in raster plot or hardware config window. - spike_only - (optional) If True, only play spikes. If False, play continuous. - """ - cdef cbSdkResult res - cdef cbSdkAoutMon mon - if channel_out < 273: - channel_out += 128 # Recent NSP firmware upgrade added 128 more analog channels. - if channel_mon is None: - res = cbSdkSetAnalogOutput(instance, channel_out, NULL, NULL) - else: - mon.chan = channel_mon - mon.bTrack = track_last - mon.bSpike = spike_only - res = cbSdkSetAnalogOutput(instance, channel_out, NULL, &mon) - handle_result(res) - return res - - -def digital_out(int channel, int instance=0, value='low'): - """Digital output command. - Inputs: - channel - integer, digital output channel number (1-based) - On NSP, 153 (dout1), 154 (dout2), 155 (dout3), 156 (dout4) - value - (optional), depends on the command - for command of 'set_value': - string, can be 'high' or 'low' (default) - instance - (optional) library instance number - """ - - values = ['low', 'high'] - if value not in values: - raise RuntimeError("Invalid value %s" % value) - - cdef cbSdkResult res - cdef uint16_t int_val = values.index(value) - res = cbSdkSetDigitalOutput(instance, channel, int_val) - handle_result(res) - return res - - -def get_channel_config(int channel, int instance=0, encoding="utf-8") -> dict[str, typing.Any]: - """ Get channel configuration. - Inputs: - - channel: 1-based channel number - - instance: (optional) library instance number - - encoding: (optional) encoding for string fields, default = 'utf-8' - Outputs: - -chaninfo = A Python dictionary with the following fields: - 'time': system clock timestamp, - 'chid': 0x8000, - 'type': cbPKTTYPE_AINP*, - 'dlen': cbPKT_DLENCHANINFO, - 'chan': actual channel id of the channel being configured, - 'proc': the address of the processor on which the channel resides, - 'bank': the address of the bank on which the channel resides, - 'term': the terminal number of the channel within it's bank, - 'chancaps': general channel capablities (given by cbCHAN_* flags), - 'doutcaps': digital output capablities (composed of cbDOUT_* flags), - 'dinpcaps': digital input capablities (composed of cbDINP_* flags), - 'aoutcaps': analog output capablities (composed of cbAOUT_* flags), - 'ainpcaps': analog input capablities (composed of cbAINP_* flags), - 'spkcaps': spike processing capabilities, - 'label': Label of the channel (null terminated if <16 characters), - 'userflags': User flags for the channel state, - 'doutopts': digital output options (composed of cbDOUT_* flags), - 'dinpopts': digital input options (composed of cbDINP_* flags), - 'aoutopts': analog output options, - 'eopchar': digital input capablities (given by cbDINP_* flags), - 'ainpopts': analog input options (composed of cbAINP* flags), - 'smpfilter': continuous-time pathway filter id, - 'smpgroup': continuous-time pathway sample group, - 'smpdispmin': continuous-time pathway display factor, - 'smpdispmax': continuous-time pathway display factor, - 'trigtype': trigger type (see cbDOUT_TRIGGER_*), - 'trigchan': trigger channel, - 'trigval': trigger value, - 'lncrate': line noise cancellation filter adaptation rate, - 'spkfilter': spike pathway filter id, - 'spkdispmax': spike pathway display factor, - 'lncdispmax': Line Noise pathway display factor, - 'spkopts': spike processing options, - 'spkthrlevel': spike threshold level, - 'spkthrlimit': , - 'spkgroup': NTrodeGroup this electrode belongs to - 0 is single unit, non-0 indicates a multi-trode grouping, - 'amplrejpos': Amplitude rejection positive value, - 'amplrejneg': Amplitude rejection negative value, - 'refelecchan': Software reference electrode channel, - """ - cdef cbSdkResult res - cdef cbPKT_CHANINFO cb_chaninfo - res = cbSdkGetChannelConfig(instance, channel, &cb_chaninfo) - handle_result(res) - if res != 0: - return {} - - chaninfo = { - 'time': cb_chaninfo.cbpkt_header.time, - 'chid': cb_chaninfo.cbpkt_header.chid, - 'type': cb_chaninfo.cbpkt_header.type, # cbPKTTYPE_AINP* - 'dlen': cb_chaninfo.cbpkt_header.dlen, # cbPKT_DLENCHANINFO - 'chan': cb_chaninfo.chan, - 'proc': cb_chaninfo.proc, - 'bank': cb_chaninfo.bank, - 'term': cb_chaninfo.term, - 'chancaps': cb_chaninfo.chancaps, - 'doutcaps': cb_chaninfo.doutcaps, - 'dinpcaps': cb_chaninfo.dinpcaps, - 'aoutcaps': cb_chaninfo.aoutcaps, - 'ainpcaps': cb_chaninfo.ainpcaps, - 'spkcaps': cb_chaninfo.spkcaps, - 'label': cb_chaninfo.label.decode(encoding), - 'userflags': cb_chaninfo.userflags, - 'doutopts': cb_chaninfo.doutopts, - 'dinpopts': cb_chaninfo.dinpopts, - 'moninst': cb_chaninfo.moninst, - 'monchan': cb_chaninfo.monchan, - 'outvalue': cb_chaninfo.outvalue, - 'aoutopts': cb_chaninfo.aoutopts, - 'eopchar': cb_chaninfo.eopchar, - 'ainpopts': cb_chaninfo.ainpopts, - 'smpfilter': cb_chaninfo.smpfilter, - 'smpgroup': cb_chaninfo.smpgroup, - 'smpdispmin': cb_chaninfo.smpdispmin, - 'smpdispmax': cb_chaninfo.smpdispmax, - 'trigtype': cb_chaninfo.trigtype, - 'trigchan': cb_chaninfo.trigchan, - 'lncrate': cb_chaninfo.lncrate, - 'spkfilter': cb_chaninfo.spkfilter, - 'spkdispmax': cb_chaninfo.spkdispmax, - 'lncdispmax': cb_chaninfo.lncdispmax, - 'spkopts': cb_chaninfo.spkopts, - 'spkthrlevel': cb_chaninfo.spkthrlevel, - 'spkthrlimit': cb_chaninfo.spkthrlimit, - 'spkgroup': cb_chaninfo.spkgroup, - 'amplrejpos': cb_chaninfo.amplrejpos, - 'amplrejneg': cb_chaninfo.amplrejneg, - 'refelecchan': cb_chaninfo.refelecchan - } - - # TODO: - #cbSCALING physcalin # physical channel scaling information - #cbFILTDESC phyfiltin # physical channel filter definition - #cbSCALING physcalout # physical channel scaling information - #cbFILTDESC phyfiltout # physical channel filter definition - #int32_t position[4] # reserved for future position information - #cbSCALING scalin # user-defined scaling information for AINP - #cbSCALING scalout # user-defined scaling information for AOUT - #uint16_t moninst - #uint16_t monchan - #int32_t outvalue # output value - #uint16_t lowsamples # address of channel to monitor - #uint16_t highsamples # address of channel to monitor - #int32_t offset - #cbMANUALUNITMAPPING unitmapping[cbMAXUNITS+0] # manual unit mapping - #cbHOOP spkhoops[cbMAXUNITS+0][cbMAXHOOPS+0] # spike hoop sorting set - - return chaninfo - - -def set_channel_config(int channel, chaninfo: dict[str, typing.Any] | None = None, int instance=0, encoding="utf-8") -> None: - """ Set channel configuration. - Inputs: - - channel: 1-based channel number - - chaninfo: A Python dict. See fields descriptions in get_channel_config. All fields are optional. - - instance: (optional) library instance number - - encoding: (optional) encoding for string fields, default = 'utf-8' - """ - cdef cbSdkResult res - cdef cbPKT_CHANINFO cb_chaninfo - res = cbSdkGetChannelConfig(instance, channel, &cb_chaninfo) - handle_result(res) - - if "label" in chaninfo: - new_label = chaninfo["label"] - if not isinstance(new_label, bytes): - new_label = new_label.encode(encoding) - strncpy(cb_chaninfo.label, new_label, sizeof(new_label)) - - if "smpgroup" in chaninfo: - cb_chaninfo.smpgroup = chaninfo["smpgroup"] - - if "spkthrlevel" in chaninfo: - cb_chaninfo.spkthrlevel = chaninfo["spkthrlevel"] - - if "ainpopts" in chaninfo: - cb_chaninfo.ainpopts = chaninfo["ainpopts"] - - res = cbSdkSetChannelConfig(instance, channel, &cb_chaninfo) - handle_result(res) - - -def get_sample_group(int group_ix, int instance=0, encoding="utf-8", int proc=1) -> list[dict[str, typing.Any]]: - """ - Get the channel infos for all channels in a sample group. - Inputs: - - group_ix: sample group index (1-6) - - instance: (optional) library instance number - - encoding: (optional) encoding for string fields, default = 'utf-8' - Outputs: - - channels_info: A list of dictionaries, one per channel in the group. Each dictionary has the following fields: - 'chid': 0x8000, - 'chan': actual channel id of the channel being configured, - 'proc': the address of the processor on which the channel resides, - 'bank': the address of the bank on which the channel resides, - 'term': the terminal number of the channel within its bank, - 'gain': the analog input gain (calculated from physical scaling), - 'label': Label of the channel (null terminated if <16 characters), - 'unit': physical unit of the channel, - """ - cdef cbSdkResult res - cdef uint32_t nChansInGroup - cdef uint16_t pGroupList[cbNUM_ANALOG_CHANS+0] - - res = cbSdkGetSampleGroupList(instance, proc, group_ix, &nChansInGroup, pGroupList) - if res: - handle_result(res) - return [] - - cdef cbPKT_CHANINFO chanInfo - channels_info = [] - for chan_ix in range(nChansInGroup): - chan_info = {} - res = cbSdkGetChannelConfig(instance, pGroupList[chan_ix], &chanInfo) - handle_result(res) - ana_range = chanInfo.physcalin.anamax - chanInfo.physcalin.anamin - dig_range = chanInfo.physcalin.digmax - chanInfo.physcalin.digmin - chan_info["chan"] = chanInfo.chan - chan_info["proc"] = chanInfo.proc - chan_info["bank"] = chanInfo.bank - chan_info["term"] = chanInfo.term - chan_info["gain"] = ana_range / dig_range - chan_info["label"] = chanInfo.label.decode(encoding) - chan_info["unit"] = chanInfo.physcalin.anaunit.decode(encoding) - channels_info.append(chan_info) - - return channels_info - - -def set_comment(comment_string, rgba_tuple=(0, 0, 0, 255), int instance=0): - """rgba_tuple is actually t_bgr: transparency, blue, green, red. Default: no-transparency & red.""" - cdef cbSdkResult res - cdef uint32_t t_bgr = (rgba_tuple[0] << 24) + (rgba_tuple[1] << 16) + (rgba_tuple[2] << 8) + rgba_tuple[3] - cdef uint8_t charset = 0 # Character set (0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI) - cdef bytes py_bytes = comment_string.encode() - cdef const char* comment = py_bytes - res = cbSdkSetComment( instance, t_bgr, charset, comment) - - -def get_sys_config(int instance=0): - cdef cbSdkResult res - cdef uint32_t spklength - cdef uint32_t spkpretrig - cdef uint32_t sysfreq - res = cbSdkGetSysConfig( instance, &spklength, &spkpretrig, &sysfreq) - handle_result(res) - return {'spklength': spklength, 'spkpretrig': spkpretrig, 'sysfreq': sysfreq} - - -def set_spike_config(int spklength=48, int spkpretrig=10, int instance=0): - cdef cbSdkResult res - res = cbSdkSetSpikeConfig( instance, spklength, spkpretrig) - handle_result(res) - - -cdef extern from "numpy/arrayobject.h": - void PyArray_ENABLEFLAGS(cnp.ndarray arr, int flags) - - -cdef class SpikeCache: - cdef readonly int inst, chan, n_samples, n_pretrig - cdef cbSPKCACHE *p_cache - cdef int last_valid - # cache - # .chid: ID of the Channel - # .pktcnt: number of packets that can be saved - # .pktsize: size of an individual packet - # .head: Where (0 based index) in the circular buffer to place the NEXT packet - # .valid: How many packets have come in since the last configuration - # .spkpkt: Circular buffer of the cached spikes - - def __cinit__(self, int channel=1, int instance=0): - self.inst = instance - self.chan = channel - cdef cbSPKCACHE ignoreme # Just so self.p_cache is not NULL... but this won't be used by anything - self.p_cache = &ignoreme # because cbSdkGetSpkCache changes what self.p_cache is pointing to. - self.reset_cache() - sys_config_dict = get_sys_config(instance) - self.n_samples = sys_config_dict['spklength'] - self.n_pretrig = sys_config_dict['spkpretrig'] - - def reset_cache(self): - cdef cbSdkResult res = cbSdkGetSpkCache(self.inst, self.chan, &self.p_cache) - handle_result(res) - self.last_valid = self.p_cache.valid - - # This function needs to be FAST! - @cython.boundscheck(False) # turn off bounds-checking for entire function - def get_new_waveforms(self): - # cdef everything! - cdef int new_valid = self.p_cache.valid - cdef int new_head = self.p_cache.head - cdef int n_new = min(max(new_valid - self.last_valid, 0), 400) - cdef cnp.ndarray[cnp.int16_t, ndim=2, mode="c"] np_waveforms = np.empty((n_new, self.n_samples), dtype=np.int16) - cdef cnp.ndarray[cnp.uint16_t, ndim=1] np_unit_ids = np.empty(n_new, dtype=np.uint16) - cdef int wf_ix, pkt_ix, samp_ix - for wf_ix in range(n_new): - pkt_ix = (new_head - 2 - n_new + wf_ix) % 400 - np_unit_ids[wf_ix] = self.p_cache.spkpkt[pkt_ix].cbpkt_header.type - # Instead of per-sample copy, we could copy the pointer for the whole wave to the buffer of a 1-d np array, - # then use memory view copying from 1-d array into our 2d matrix. But below is pure-C so should be fast too. - for samp_ix in range(self.n_samples): - np_waveforms[wf_ix, samp_ix] = self.p_cache.spkpkt[pkt_ix].wave[samp_ix] - #unit_ids_out = [unit_ids[wf_ix] for wf_ix in range(n_new)] - PyArray_ENABLEFLAGS(np_waveforms, NPY_ARRAY_OWNDATA) - self.last_valid = new_valid - return np_waveforms, np_unit_ids - - -cdef cbSdkResult handle_result(cbSdkResult res): - if res == CBSDKRESULT_WARNCLOSED: - print("Library is already closed.") - if res < 0: - errtext = "No associated error string. See cbsdk.h" - if res == CBSDKRESULT_ERROFFLINE: - errtext = "Instrument is offline." - elif res == CBSDKRESULT_CLOSED: - errtext = "Interface is closed; cannot do this operation." - elif res == CBSDKRESULT_ERRCONFIG: - errtext = "Trying to run an unconfigured method." - elif res == CBSDKRESULT_NULLPTR: - errtext = "Null pointer." - elif res == CBSDKRESULT_INVALIDCHANNEL: - errtext = "Invalid channel number." - - raise RuntimeError(("%d, " + errtext) % res) - return res diff --git a/bindings/Python/cerelink/cbsdk_cython.pxd b/bindings/Python/cerelink/cbsdk_cython.pxd deleted file mode 100644 index 8b9c47d9..00000000 --- a/bindings/Python/cerelink/cbsdk_cython.pxd +++ /dev/null @@ -1,458 +0,0 @@ -""" -@date March 9, 2014 - -@author: dashesy - -Purpose: Cython interface for cbsdk_small - -""" - -from libc.stdint cimport uint32_t, int32_t, uint16_t, int16_t, uint8_t -from libcpp cimport bool - -cdef extern from "stdint.h": - ctypedef unsigned long long uint64_t - -cdef extern from "cerelink/cbproto.h": - ctypedef uint64_t PROCTIME # Will be uint32_t or uint64_t depending on CBPROTO_311, but we detect size at runtime - - cdef char* cbNET_UDP_ADDR_INST "cbNET_UDP_ADDR_INST" # Cerebus default address - cdef char* cbNET_UDP_ADDR_CNT "cbNET_UDP_ADDR_CNT" # NSP default control address - cdef char* cbNET_UDP_ADDR_BCAST "cbNET_UDP_ADDR_BCAST" # NSP default broadcast address - cdef int cbNET_UDP_PORT_BCAST "cbNET_UDP_PORT_BCAST" # Neuroflow Data Port - cdef int cbNET_UDP_PORT_CNT "cbNET_UDP_PORT_CNT" # Neuroflow Control Port - - # Have to put constants that are used as array sizes in an enum, - # otherwise they are considered non-const and can't be used. - cdef enum cbhwlib_consts: - cbNUM_FE_CHANS = 256 - cbNUM_ANAIN_CHANS = 16 - cbNUM_ANALOG_CHANS = (cbNUM_FE_CHANS + cbNUM_ANAIN_CHANS) - cbNUM_ANAOUT_CHANS = 4 - cbNUM_AUDOUT_CHANS = 2 - cbNUM_ANALOGOUT_CHANS = (cbNUM_ANAOUT_CHANS + cbNUM_AUDOUT_CHANS) - cbNUM_DIGIN_CHANS = 1 - cbNUM_SERIAL_CHANS = 1 - cbNUM_DIGOUT_CHANS = 4 - cbMAXCHANS = (cbNUM_ANALOG_CHANS + cbNUM_ANALOGOUT_CHANS + cbNUM_DIGIN_CHANS + cbNUM_SERIAL_CHANS + cbNUM_DIGOUT_CHANS) - cbMAXUNITS = 5 - # MAX_CHANS_DIGITAL_IN = (cbNUM_ANALOG_CHANS + cbNUM_ANALOGOUT_CHANS + cbNUM_DIGIN_CHANS) - # MAX_CHANS_SERIAL = (MAX_CHANS_DIGITAL_IN + cbNUM_SERIAL_CHANS) - cbMAXTRACKOBJ = 20 # maximum number of trackable objects - cbMAXHOOPS = 4 - cbPKT_SPKCACHEPKTCNT = 400 - cbMAX_PNTS = 128 # make large enough to track longest possible spike width in samples - - cdef enum cbwlib_strconsts: - cbLEN_STR_UNIT = 8 - cbLEN_STR_LABEL = 16 - #cbLEN_STR_FILT_LABEL = 16 - cbLEN_STR_IDENT = 64 - - cdef enum cbStateCCF: - CCFSTATE_READ = 0 # Reading in progress - CCFSTATE_WRITE = 1 # Writing in progress - CCFSTATE_SEND = 2 # Sendign in progress - CCFSTATE_CONVERT = 3 # Conversion in progress - CCFSTATE_THREADREAD = 4 # Total threaded read progress - CCFSTATE_THREADWRITE = 5 # Total threaded write progress - CCFSTATE_UNKNOWN = 6 # (Always the last) unknown state - - # TODO: use cdef for each item instead? - cdef enum cbhwlib_cbFILECFG: - cbFILECFG_OPT_NONE = 0x00000000 - cbFILECFG_OPT_KEEPALIVE = 0x00000001 - cbFILECFG_OPT_REC = 0x00000002 - cbFILECFG_OPT_STOP = 0x00000003 - cbFILECFG_OPT_NMREC = 0x00000004 - cbFILECFG_OPT_CLOSE = 0x00000005 - cbFILECFG_OPT_SYNCH = 0x00000006 - cbFILECFG_OPT_OPEN = 0x00000007 - - cdef enum cbhwlib_cbCHANCAPS: - cbCHAN_EXISTS = 0x00000001 # Channel id is allocated - cbCHAN_CONNECTED = 0x00000002 # Channel is connected and mapped and ready to use - cbCHAN_ISOLATED = 0x00000004 # Channel is electrically isolated - cbCHAN_AINP = 0x00000100 # Channel has analog input capabilities - cbCHAN_AOUT = 0x00000200 # Channel has analog output capabilities - cbCHAN_DINP = 0x00000400 # Channel has digital input capabilities - cbCHAN_DOUT = 0x00000800 # Channel has digital output capabilities - - cdef enum cbhwlib_cbCHANTYPES: - cbCHANTYPE_ANAIN = 0x01 - cbCHANTYPE_ANAOUT = 0x02 - cbCHANTYPE_AUDOUT = 0x04 - cbCHANTYPE_DIGIN = 0x10 - cbCHANTYPE_SERIAL = 0x20 - cbCHANTYPE_DIGOUT = 0x40 - - ctypedef struct cbSCALING: - int16_t digmin # digital value that cooresponds with the anamin value - int16_t digmax # digital value that cooresponds with the anamax value - int32_t anamin # the minimum analog value present in the signal - int32_t anamax # the maximum analog value present in the signal - int32_t anagain # the gain applied to the default analog values to get the analog values - char anaunit[cbLEN_STR_UNIT+0] # the unit for the analog signal (eg, "uV" or "MPa") - - ctypedef struct cbFILTDESC: - char label[cbLEN_STR_LABEL+0] - uint32_t hpfreq # high-pass corner frequency in milliHertz - uint32_t hporder # high-pass filter order - uint32_t hptype # high-pass filter type - uint32_t lpfreq # low-pass frequency in milliHertz - uint32_t lporder # low-pass filter order - uint32_t lptype # low-pass filter type - - ctypedef struct cbMANUALUNITMAPPING: - int16_t nOverride - int16_t afOrigin[3] - int16_t afShape[3][3] - int16_t aPhi - uint32_t bValid # is this unit in use at this time? - - ctypedef struct cbHOOP: - uint16_t valid # 0=undefined, 1 for valid - int16_t time # time offset into spike window - int16_t min # minimum value for the hoop window - int16_t max # maximum value for the hoop window - - ctypedef struct cbPKT_HEADER: - PROCTIME time # system clock timestamp - uint16_t chid # channel identifier - uint16_t type # packet type - uint16_t dlen # length of data field in 32-bit chunks - uint8_t instrument # instrument number to transmit this packets - uint8_t reserved # reserved for future - - ctypedef struct cbPKT_CHANINFO: - cbPKT_HEADER cbpkt_header - uint32_t chan # actual channel id of the channel being configured - uint32_t proc # the address of the processor on which the channel resides - uint32_t bank # the address of the bank on which the channel resides - uint32_t term # the terminal number of the channel within it's bank - uint32_t chancaps # general channel capablities (given by cbCHAN_* flags) - uint32_t doutcaps # digital output capablities (composed of cbDOUT_* flags) - uint32_t dinpcaps # digital input capablities (composed of cbDINP_* flags) - uint32_t aoutcaps # analog output capablities (composed of cbAOUT_* flags) - uint32_t ainpcaps # analog input capablities (composed of cbAINP_* flags) - uint32_t spkcaps # spike processing capabilities - cbSCALING physcalin # physical channel scaling information - cbFILTDESC phyfiltin # physical channel filter definition - cbSCALING physcalout # physical channel scaling information - cbFILTDESC phyfiltout # physical channel filter definition - char label[cbLEN_STR_LABEL+0]# Label of the channel (null terminated if <16 characters) - uint32_t userflags # User flags for the channel state - int32_t position[4] # reserved for future position information - cbSCALING scalin # user-defined scaling information for AINP - cbSCALING scalout # user-defined scaling information for AOUT - uint32_t doutopts # digital output options (composed of cbDOUT_* flags) - uint32_t dinpopts # digital input options (composed of cbDINP_* flags) - uint32_t aoutopts # analog output options - uint32_t eopchar # digital input capablities (given by cbDINP_* flags) - uint16_t moninst - uint16_t monchan - int32_t outvalue # output value - # uint16_t lowsamples # address of channel to monitor - # uint16_t highsamples # address of channel to monitor - # int32_t offset - uint8_t trigtype # trigger type (see cbDOUT_TRIGGER_*) - uint8_t reserved[2] # 2 bytes reserved - uint8_t triginst # instrument of the trigger channel - uint16_t trigchan # trigger channel - uint16_t trigval # trigger value - uint32_t ainpopts # analog input options (composed of cbAINP* flags) - uint32_t lncrate # line noise cancellation filter adaptation rate - uint32_t smpfilter # continuous-time pathway filter id - uint32_t smpgroup # continuous-time pathway sample group - int32_t smpdispmin # continuous-time pathway display factor - int32_t smpdispmax # continuous-time pathway display factor - uint32_t spkfilter # spike pathway filter id - int32_t spkdispmax # spike pathway display factor - int32_t lncdispmax # Line Noise pathway display factor - uint32_t spkopts # spike processing options - int32_t spkthrlevel # spike threshold level - int32_t spkthrlimit - uint32_t spkgroup # NTrodeGroup this electrode belongs to - 0 is single unit, non-0 indicates a multi-trode grouping - int16_t amplrejpos # Amplitude rejection positive value - int16_t amplrejneg # Amplitude rejection negative value - uint32_t refelecchan # Software reference electrode channel - cbMANUALUNITMAPPING unitmapping[cbMAXUNITS+0] # manual unit mapping - cbHOOP spkhoops[cbMAXUNITS+0][cbMAXHOOPS+0] # spike hoop sorting set - - ctypedef struct cbFILTDESC: - char label[cbLEN_STR_LABEL+0] - uint32_t hpfreq # high-pass corner frequency in milliHertz - uint32_t hporder # high-pass filter order - uint32_t hptype # high-pass filter type - uint32_t lpfreq # low-pass frequency in milliHertz - uint32_t lporder # low-pass filter order - uint32_t lptype # low-pass filter type - - ctypedef struct cbPKT_SPK: - cbPKT_HEADER cbpkt_header - float fPattern[3] # values of the pattern space (Normal uses only 2, PCA uses third) - int16_t nPeak - int16_t nValley - int16_t wave[cbMAX_PNTS+0] # Room for all possible points collected - # wave must be the last item in the structure because it can be variable length to a max of cbMAX_PNTS - - ctypedef struct cbSPKCACHE: - uint32_t chid # ID of the Channel - uint32_t pktcnt # # of packets which can be saved - uint32_t pktsize # Size of an individual packet - uint32_t head # Where (0 based index) in the circular buffer to place the NEXT packet. - uint32_t valid # How many packets have come in since the last configuration - cbPKT_SPK spkpkt[cbPKT_SPKCACHEPKTCNT+0] # Circular buffer of the cached spikes - -cdef extern from "cerelink/cbsdk.h": - - cdef int cbSdk_CONTINUOUS_DATA_SAMPLES = 102400 - cdef int cbSdk_EVENT_DATA_SAMPLES = (2 * 8192) - cdef int cbSdk_MAX_UPOLOAD_SIZE = (1024 * 1024 * 1024) - cdef float cbSdk_TICKS_PER_SECOND = 30000.0 - cdef float cbSdk_SECONDS_PER_TICK = 1.0 / cbSdk_TICKS_PER_SECOND - - ctypedef enum cbSdkResult: - CBSDKRESULT_WARNCONVERT = 3 # If file conversion is needed - CBSDKRESULT_WARNCLOSED = 2 # Library is already closed - CBSDKRESULT_WARNOPEN = 1 # Library is already opened - CBSDKRESULT_SUCCESS = 0 # Successful operation - CBSDKRESULT_NOTIMPLEMENTED = -1 # Not implemented - CBSDKRESULT_UNKNOWN = -2 # Unknown error - CBSDKRESULT_INVALIDPARAM = -3 # Invalid parameter - CBSDKRESULT_CLOSED = -4 # Interface is closed cannot do this operation - CBSDKRESULT_OPEN = -5 # Interface is open cannot do this operation - CBSDKRESULT_NULLPTR = -6 # Null pointer - CBSDKRESULT_ERROPENCENTRAL = -7 # Unable to open Central interface - CBSDKRESULT_ERROPENUDP = -8 # Unable to open UDP interface (might happen if default) - CBSDKRESULT_ERROPENUDPPORT = -9 # Unable to open UDP port - CBSDKRESULT_ERRMEMORYTRIAL = -10 # Unable to allocate RAM for trial cache data - CBSDKRESULT_ERROPENUDPTHREAD = -11 # Unable to open UDP timer thread - CBSDKRESULT_ERROPENCENTRALTHREAD = -12 # Unable to open Central communication thread - CBSDKRESULT_INVALIDCHANNEL = -13 # Invalid channel number - CBSDKRESULT_INVALIDCOMMENT = -14 # Comment too long or invalid - CBSDKRESULT_INVALIDFILENAME = -15 # Filename too long or invalid - CBSDKRESULT_INVALIDCALLBACKTYPE = -16 # Invalid callback type - CBSDKRESULT_CALLBACKREGFAILED = -17 # Callback register/unregister failed - CBSDKRESULT_ERRCONFIG = -18 # Trying to run an unconfigured method - CBSDKRESULT_INVALIDTRACKABLE = -19 # Invalid trackable id, or trackable not present - CBSDKRESULT_INVALIDVIDEOSRC = -20 # Invalid video source id, or video source not present - CBSDKRESULT_ERROPENFILE = -21 # Cannot open file - CBSDKRESULT_ERRFORMATFILE = -22 # Wrong file format - CBSDKRESULT_OPTERRUDP = -23 # Socket option error (possibly permission issue) - CBSDKRESULT_MEMERRUDP = -24 # Socket memory assignment error - CBSDKRESULT_INVALIDINST = -25 # Invalid range or instrument address - CBSDKRESULT_ERRMEMORY = -26 # library memory allocation error - CBSDKRESULT_ERRINIT = -27 # Library initialization error - CBSDKRESULT_TIMEOUT = -28 # Conection timeout error - CBSDKRESULT_BUSY = -29 # Resource is busy - CBSDKRESULT_ERROFFLINE = -30 # Instrument is offline - CBSDKRESULT_INSTOUTDATED = -31 # The instrument runs an outdated protocol version - CBSDKRESULT_LIBOUTDATED = -32 # The library is outdated - - ctypedef enum cbSdkConnectionType: - CBSDKCONNECTION_DEFAULT = 0 # Try Central then UDP - CBSDKCONNECTION_CENTRAL = 1 # Use Central - CBSDKCONNECTION_UDP = 2 # Use UDP - CBSDKCONNECTION_CLOSED = 3 # Closed - - ctypedef enum cbSdkInstrumentType: - CBSDKINSTRUMENT_NSP = 0 # NSP - CBSDKINSTRUMENT_NPLAY = 1 # Local nPlay - CBSDKINSTRUMENT_LOCALNSP = 2 # Local NSP - CBSDKINSTRUMENT_REMOTENPLAY = 3 # Remote nPlay - - # cbSdkCCFEvent. Skipping because it depends on cbStateCCF - - ctypedef enum cbSdkTrialType: - CBSDKTRIAL_CONTINUOUS = 0 - CBSDKTRIAL_EVENTS = 1 - CBSDKTRIAL_COMMENTS = 2 - CBSDKTRIAL_TRACKING = 3 - - ctypedef enum cbSdkWaveformType: - cbSdkWaveform_NONE = 0 - cbSdkWaveform_PARAMETERS = 1 - cbSdkWaveform_SINE = 2 - cbSdkWaveform_COUNT = 3 - - ctypedef enum cbSdkWaveformTriggerType: - cbSdkWaveformTrigger_NONE = 0 # Instant software trigger - cbSdkWaveformTrigger_DINPREG = 1 # digital input rising edge trigger - cbSdkWaveformTrigger_DINPFEG = 2 # digital input falling edge trigger - cbSdkWaveformTrigger_SPIKEUNIT = 3 # spike unit - cbSdkWaveformTrigger_COMMENTCOLOR = 4 # custom colored event (e.g. NeuroMotive event) - cbSdkWaveformTrigger_SOFTRESET = 5 # Soft-reset trigger (e.g. file recording start) - cbSdkWaveformTrigger_EXTENSION = 6 # Extension trigger - cbSdkWaveformTrigger_COUNT = 7 # Always the last value - - ctypedef struct cbSdkVersion: - # Library version - uint32_t major - uint32_t minor - uint32_t release - uint32_t beta - # Protocol version - uint32_t majorp - uint32_t minorp - # NSP version - uint32_t nspmajor - uint32_t nspminor - uint32_t nsprelease - uint32_t nspbeta - # NSP protocol version - uint32_t nspmajorp - uint32_t nspminorp - - ctypedef struct cbSdkCCFEvent: - cbStateCCF state # CCF state - cbSdkResult result # Last result - const char * szFileName # CCF filename under operation - uint8_t progress # Progress (in percent) - - ctypedef struct cbSdkTrialEvent: - PROCTIME trial_start_time - uint32_t num_events - PROCTIME * timestamps # [num_events] - uint16_t * channels # [num_events] - uint16_t * units # [num_events] - void * waveforms # [num_events][cbMAX_PNTS] - - ctypedef struct cbSdkConnection: - int nInPort # Client port number - int nOutPort # Instrument port number - int nRecBufSize # Receive buffer size (0 to ignore altogether) - const char * szInIP # Client IPv4 address - const char * szOutIP # Instrument IPv4 address - cdef cbSdkConnection cbSdkConnection_DEFAULT = {cbNET_UDP_PORT_BCAST, cbNET_UDP_PORT_CNT, (4096 * 2048), "", ""} - - # Trial continuous data - ctypedef struct cbSdkTrialCont: - PROCTIME trial_start_time # Trial start time (device timestamp when trial started) - uint8_t group # Sample group to retrieve (0-7, set by user before Init) - uint16_t count # Number of valid channels in this group (output from Init) - uint16_t chan[cbNUM_ANALOG_CHANS+0] # channel numbers (1-based, output from Init) - uint16_t sample_rate # Sample rate for this group (Hz, output from Init) - uint32_t num_samples # Number of samples: input = max to read, output = actual read - void * samples # Pointer to contiguous [num_samples][count] array - PROCTIME * timestamps # Pointer to array of [num_samples] timestamps - - ctypedef struct cbSdkTrialComment: - PROCTIME trial_start_time # Trial start time (device timestamp when trial started) - uint16_t num_samples # Number of comments - uint8_t * charsets # Buffer to hold character sets - uint32_t * rgbas # Buffer to hold rgba values - uint8_t * * comments # Pointer to comments - PROCTIME * timestamps # Buffer to hold time stamps - - # Trial video tracking data - ctypedef struct cbSdkTrialTracking: - PROCTIME trial_start_time # Trial start time (device timestamp when trial started) - uint16_t count # Number of valid trackable objects (up to cbMAXTRACKOBJ) - uint16_t ids[cbMAXTRACKOBJ+0] # Node IDs (holds count elements) - uint16_t max_point_counts[cbMAXTRACKOBJ+0] # Maximum point counts (holds count elements) - uint16_t types[cbMAXTRACKOBJ+0] # Node types (can be cbTRACKOBJ_TYPE_* and determines coordinate counts) (holds count elements) - uint8_t names[cbMAXTRACKOBJ+0][16 + 1] # Node names (holds count elements) - uint16_t num_samples[cbMAXTRACKOBJ+0] # Number of samples - uint16_t * point_counts[cbMAXTRACKOBJ+0] # Buffer to hold number of valid points (up to max_point_counts) (holds count*num_samples elements) - void * * coords[cbMAXTRACKOBJ+0] # Buffer to hold tracking points (holds count*num_samples tarackables, each of max_point_counts points - uint32_t * synch_frame_numbers[cbMAXTRACKOBJ+0] # Buffer to hold synch frame numbers (holds count*num_samples elements) - uint32_t * synch_timestamps[cbMAXTRACKOBJ+0] # Buffer to hold synchronized tracking time stamps (in milliseconds) (holds count*num_samples elements) - PROCTIME * timestamps[cbMAXTRACKOBJ+0] # Buffer to hold tracking time stamps (holds count*num_samples elements) - - ctypedef struct cbSdkAoutMon: - uint16_t chan # (1-based) channel to monitor - bint bTrack # If should monitor last tracked channel - bint bSpike # If spike or continuous should be monitored - - ctypedef struct cbSdkWaveformData: - cbSdkWaveformType type - uint32_t repeats - cbSdkWaveformTriggerType trig - uint16_t trigChan - uint16_t trigValue - uint8_t trigNum - int16_t offset - uint16_t sineFrequency - int16_t sineAmplitude - uint16_t phases - uint16_t* duration - int16_t* amplitude - - cbSdkResult cbSdkGetVersion(uint32_t nInstance, cbSdkVersion * version) - # Read and write CCF requires a lot of baggage from cbhwlib.h. Skipping. - cbSdkResult cbSdkOpen(uint32_t nInstance, cbSdkConnectionType conType, cbSdkConnection con) nogil - cbSdkResult cbSdkGetType(uint32_t nInstance, cbSdkConnectionType * conType, cbSdkInstrumentType * instType) # Get connection and instrument type - cbSdkResult cbSdkClose(uint32_t nInstance) # Close the library - cbSdkResult cbSdkGetTime(uint32_t nInstance, PROCTIME * cbtime) # Get the instrument sample clock time - cbSdkResult cbSdkGetSpkCache(uint32_t nInstance, uint16_t channel, cbSPKCACHE **cache) - cbSdkResult cbSdkUnsetTrialConfig(uint32_t nInstance, cbSdkTrialType type) - cbSdkResult cbSdkGetChannelLabel(int nInstance, uint16_t channel, uint32_t * bValid, char * label, uint32_t * userflags, int32_t * position) - cbSdkResult cbSdkSetChannelLabel(uint32_t nInstance, uint16_t channel, const char * label, uint32_t userflags, int32_t * position) - # cbSdkResult cbSdkGetChannelType(uint32_t nInstance, uint16_t channel, uint8_t* ch_type) - # TODO: wrap Get channel capabilities section from cbsdk.h - cbSdkResult cbSdkIsChanAnyDigIn(uint32_t nInstance, uint16_t channel, uint32_t * bResult) - cbSdkResult cbSdkIsChanSerial(uint32_t nInstance, uint16_t channel, uint32_t * bResult); - # Retrieve data of a trial (NULL means ignore), user should allocate enough buffers beforehand, and trial should not be closed during this call - cbSdkResult cbSdkGetTrialData( uint32_t nInstance, - uint32_t seek, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking) - # Initialize the structures (and fill with information about active channels, comment pointers and samples in the buffer) - cbSdkResult cbSdkInitTrialData( uint32_t nInstance, uint32_t resetClock, - cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking, unsigned long wait_for_comment_msec) - cbSdkResult cbSdkSetFileConfig(uint32_t nInstance, const char * filename, const char * comment, uint32_t start, uint32_t options) # Also see cbsdk_file_config in helper - cbSdkResult cbSdkGetFileConfig(uint32_t nInstance, char * filename, char * username, bool * pbRecording) - cbSdkResult cbSdkSetPatientInfo(uint32_t nInstance, const char * ID, const char * firstname, const char * lastname, uint32_t DOBMonth, uint32_t DOBDay, uint32_t DOBYear) - cbSdkResult cbSdkInitiateImpedance(uint32_t nInstance) - #cbSdkResult cbSdkSendPoll(uint32_t nInstance, const char* appname, uint32_t mode, uint32_t flags, uint32_t extra) - cbSdkResult cbSdkSendPoll(uint32_t nInstance, void * ppckt) - #cbSdkSendPacket - #cbSdkSetSystemRunLevel - cbSdkResult cbSdkSetDigitalOutput(uint32_t nInstance, uint16_t channel, uint16_t value) - cbSdkResult cbSdkSetSynchOutput(uint32_t nInstance, uint16_t channel, uint32_t nFreq, uint32_t nRepeats) - #cbSdkExtDoCommand - cbSdkResult cbSdkSetAnalogOutput(uint32_t nInstance, uint16_t channel, cbSdkWaveformData* wf, cbSdkAoutMon* mon) - cbSdkResult cbSdkSetChannelMask(uint32_t nInstance, uint16_t channel, uint32_t bActive) - cbSdkResult cbSdkSetComment(uint32_t nInstance, uint32_t rgba, uint8_t charset, const char * comment) - cbSdkResult cbSdkSetChannelConfig(uint32_t nInstance, uint16_t channel, cbPKT_CHANINFO * chaninfo) - cbSdkResult cbSdkGetChannelConfig(uint32_t nInstance, uint16_t channel, cbPKT_CHANINFO * chaninfo) - cbSdkResult cbSdkGetFilterDesc(uint32_t nInstance, uint32_t proc, uint32_t filt, cbFILTDESC * filtdesc) - cbSdkResult cbSdkGetSampleGroupList(uint32_t nInstance, uint32_t proc, uint32_t group, uint32_t *length, uint16_t *list) - cbSdkResult cbSdkGetSampleGroupInfo(uint32_t nInstance, uint32_t proc, uint32_t group, char *label, uint32_t *period, uint32_t *length) - cbSdkResult cbSdkSetSpikeConfig(uint32_t nInstance, uint32_t spklength, uint32_t spkpretrig) - cbSdkResult cbSdkGetSysConfig(uint32_t nInstance, uint32_t * spklength, uint32_t * spkpretrig, uint32_t * sysfreq) - - -cdef extern from "cbsdk_helper.h": - - ctypedef struct cbSdkConfigParam: - uint32_t bActive - uint16_t Begchan - uint32_t Begmask - uint32_t Begval - uint16_t Endchan - uint32_t Endmask - uint32_t Endval - uint32_t uWaveforms - uint32_t uConts - uint32_t uEvents - uint32_t uComments - uint32_t uTrackings - - cbSdkResult cbsdk_get_trial_config(int nInstance, cbSdkConfigParam * pcfg_param) - cbSdkResult cbsdk_set_trial_config(int nInstance, const cbSdkConfigParam * pcfg_param) - - cbSdkResult cbsdk_init_trial_event(int nInstance, int clock_reset, cbSdkTrialEvent * trialevent) - cbSdkResult cbsdk_get_trial_event(int nInstance, int seek, cbSdkTrialEvent * trialevent) - - cbSdkResult cbsdk_init_trial_cont(int nInstance, int clock_reset, cbSdkTrialCont * trialcont) - cbSdkResult cbsdk_get_trial_cont(int nInstance, int seek, cbSdkTrialCont * trialcont) - - cbSdkResult cbsdk_init_trial_data(int nInstance, int clock_reset, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, cbSdkTrialComment * trialcomm, unsigned long wait_for_comment_msec) - cbSdkResult cbsdk_get_trial_data(int nInstance, int seek, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, cbSdkTrialComment * trialcomm) - - cbSdkResult cbsdk_init_trial_comment(int nInstance, int clock_reset, cbSdkTrialComment * trialcomm, unsigned long wait_for_comment_msec) - cbSdkResult cbsdk_get_trial_comment(int nInstance, int seek, cbSdkTrialComment * trialcomm) - - cbSdkResult cbsdk_file_config(int instance, const char * filename, const char * comment, int start, unsigned int options) diff --git a/bindings/Python/cerelink/cbsdk_helper.cpp b/bindings/Python/cerelink/cbsdk_helper.cpp deleted file mode 100644 index d6bec278..00000000 --- a/bindings/Python/cerelink/cbsdk_helper.cpp +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Implementation of helper API for easy Cython wrapping. - * - * @date March 9, 2014 - * @author: dashesy - */ - -#include - -#include "cbsdk_helper.h" - -cbSdkResult cbsdk_get_trial_config(const uint32_t nInstance, cbSdkConfigParam * pcfg_param) -{ - const cbSdkResult sdk_result = cbSdkGetTrialConfig(nInstance, &pcfg_param->bActive, - &pcfg_param->Begchan, &pcfg_param->Begmask, &pcfg_param->Begval, - &pcfg_param->Endchan, &pcfg_param->Endmask, &pcfg_param->Endval, - &pcfg_param->uWaveforms, - &pcfg_param->uConts, &pcfg_param->uEvents, &pcfg_param->uComments, - &pcfg_param->uTrackings); - - return sdk_result; -} - -cbSdkResult cbsdk_set_trial_config(const uint32_t nInstance, const cbSdkConfigParam * pcfg_param) -{ - const cbSdkResult sdk_result = cbSdkSetTrialConfig(nInstance, pcfg_param->bActive, - pcfg_param->Begchan,pcfg_param->Begmask, pcfg_param->Begval, - pcfg_param->Endchan, pcfg_param->Endmask, pcfg_param->Endval, - pcfg_param->uWaveforms, - pcfg_param->uConts, pcfg_param->uEvents, pcfg_param->uComments, - pcfg_param->uTrackings); - - return sdk_result; -} - - -cbSdkResult cbsdk_init_trial_event(const uint32_t nInstance, const int clock_reset, cbSdkTrialEvent * trialevent) -{ - memset(trialevent, 0, sizeof(*trialevent)); - const cbSdkResult sdk_result = cbSdkInitTrialData(nInstance, clock_reset, trialevent, nullptr, nullptr, nullptr); - - return sdk_result; -} - -cbSdkResult cbsdk_get_trial_event(const uint32_t nInstance, const int seek, cbSdkTrialEvent * trialevent) -{ - const cbSdkResult sdk_result = cbSdkGetTrialData(nInstance, seek, trialevent, nullptr, nullptr, nullptr); - - return sdk_result; -} - -cbSdkResult cbsdk_init_trial_cont(const uint32_t nInstance, const int clock_reset, cbSdkTrialCont * trialcont) -{ - memset(trialcont, 0, sizeof(*trialcont)); - const cbSdkResult sdk_result = cbSdkInitTrialData(nInstance, clock_reset, nullptr, trialcont, nullptr, nullptr); - - return sdk_result; -} - -cbSdkResult cbsdk_get_trial_cont(const uint32_t nInstance, const int seek, cbSdkTrialCont * trialcont) -{ - const cbSdkResult sdk_result = cbSdkGetTrialData(nInstance, seek, nullptr, trialcont, nullptr, nullptr); - - return sdk_result; -} - -cbSdkResult cbsdk_init_trial_data(const uint32_t nInstance, const int clock_reset, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, cbSdkTrialComment * trialcomm, unsigned long wait_for_comment_msec) -{ - if(trialevent) - { - memset(trialevent, 0, sizeof(*trialevent)); - } - if (trialcont) - { - memset(trialcont, 0, sizeof(*trialcont)); - } - if (trialcomm) - { - memset(trialcomm, 0, sizeof(*trialcomm)); - } - const cbSdkResult sdk_result = cbSdkInitTrialData(nInstance, clock_reset, trialevent, trialcont, trialcomm, nullptr, wait_for_comment_msec); - - return sdk_result; -} - -cbSdkResult cbsdk_get_trial_data(const uint32_t nInstance, const int seek, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, cbSdkTrialComment * trialcomm) -{ - const cbSdkResult sdk_result = cbSdkGetTrialData(nInstance, seek, trialevent, trialcont, trialcomm, nullptr); - - return sdk_result; -} - -cbSdkResult cbsdk_init_trial_comment(const uint32_t nInstance, const int clock_reset, cbSdkTrialComment * trialcomm, const unsigned long wait_for_comment_msec) -{ - memset(trialcomm, 0, sizeof(*trialcomm)); - const cbSdkResult sdk_result = cbSdkInitTrialData(nInstance, clock_reset, nullptr, nullptr, trialcomm, nullptr, wait_for_comment_msec); - - return sdk_result; -} - -cbSdkResult cbsdk_get_trial_comment(const uint32_t nInstance, const int seek, cbSdkTrialComment * trialcomm) -{ - const cbSdkResult sdk_result = cbSdkGetTrialData(nInstance, seek, nullptr, nullptr, trialcomm, nullptr); - - return sdk_result; -} - -cbSdkResult cbsdk_file_config(const uint32_t instance, const char * filename, const char * comment, const int start, const unsigned int options) -{ - const cbSdkResult sdk_result = cbSdkSetFileConfig(instance, filename == nullptr ? "" : filename, comment == nullptr ? "" : comment, start, options); - return sdk_result; -} diff --git a/bindings/Python/cerelink/cbsdk_helper.h b/bindings/Python/cerelink/cbsdk_helper.h deleted file mode 100644 index 661d1f41..00000000 --- a/bindings/Python/cerelink/cbsdk_helper.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * API to add to cbsdk. This wraps some main API functions in simpler (C-only) - * code. This in turn can be more easily wrapped (e.g. Cython) - * - * - * @date March 9, 2014 - * @author: dashesy - */ - -#ifndef CBHELPER_H -#define CBHELPER_H - -#include - -/* The following are already defined in cbsdk.h - // #define cbSdk_CONTINUOUS_DATA_SAMPLES 102400 // multiple of 4096 - /// The default number of events that will be stored per channel in the trial buffer - // #define cbSdk_EVENT_DATA_SAMPLES (2 * 8192) // multiple of 4096 - */ - -typedef struct _cbSdkConfigParam { - uint32_t bActive; - uint16_t Begchan; - uint32_t Begmask; - uint32_t Begval; - uint16_t Endchan; - uint32_t Endmask; - uint32_t Endval; - bool bDouble; - uint32_t uWaveforms; - uint32_t uConts; - uint32_t uEvents; - uint32_t uComments; - uint32_t uTrackings; -} cbSdkConfigParam; - -cbSdkResult cbsdk_get_trial_config(uint32_t nInstance, cbSdkConfigParam * pcfg_param); -cbSdkResult cbsdk_set_trial_config(uint32_t nInstance, const cbSdkConfigParam * pcfg_param); - -cbSdkResult cbsdk_init_trial_event(uint32_t nInstance, int clock_reset, cbSdkTrialEvent * trialevent); -cbSdkResult cbsdk_get_trial_event(uint32_t nInstance, int seek, cbSdkTrialEvent * trialevent); - -cbSdkResult cbsdk_init_trial_data(uint32_t nInstance, int clock_reset, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, cbSdkTrialComment * trialcomm, unsigned long wait_for_comment_msec = 250); -cbSdkResult cbsdk_get_trial_data(uint32_t nInstance, int seek, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, cbSdkTrialComment * trialcomm); - -cbSdkResult cbsdk_init_trial_cont(uint32_t nInstance, int clock_reset, cbSdkTrialCont * trialcont); -cbSdkResult cbsdk_get_trial_cont(uint32_t nInstance, int seek, cbSdkTrialCont * trialcont); - -cbSdkResult cbsdk_init_trial_comment(uint32_t nInstance, int clock_reset, cbSdkTrialComment * trialcomm, unsigned long wait_for_comment_msec = 250); -cbSdkResult cbsdk_get_trial_comment(uint32_t nInstance, int seek, cbSdkTrialComment * trialcomm); - -cbSdkResult cbsdk_file_config(uint32_t instance, const char * filename, const char * comment, int start, unsigned int options); - -#endif // CBHELPER_H diff --git a/bindings/Python/pyproject.toml b/bindings/Python/pyproject.toml deleted file mode 100644 index 654cb947..00000000 --- a/bindings/Python/pyproject.toml +++ /dev/null @@ -1,32 +0,0 @@ -[build-system] -requires = ["setuptools", "wheel", "cython", "numpy>=2.0.0", "setuptools-scm"] -build-backend = "setuptools.build_meta" - -[project] -name = "cerelink" # as it would appear on PyPI -dynamic = ["version", "readme"] -authors = [ - {name = "Chadwick Boulay", email = "chadwick.boulay@gmail.com"}, - {name = "Ehsan Azar"} -] -requires-python = ">=3.11" -dependencies = [ - "numpy>=2.0.0", -] -urls = {homepage = "https://github.com/CerebusOSS/CereLink"} - -[tool.setuptools.packages.find] -include = ["cerelink*"] # package names should match these glob patterns (["*"] by default) - -[tool.setuptools.package-data] -cerelink = ["*.dll"] - -[tool.setuptools.dynamic] -readme = {file = ["README.md"], content-type = "text/markdown"} - -[tool.setuptools_scm] -# Get version from git tags, looking in the root of the repository (not bindings/Python) -root = "../.." -version_file = "cerelink/__version__.py" -# Strip 'v' prefix from tags (v7.6.4 -> 7.6.4) -tag_regex = "^(?Pv)?(?P[^\\+]+)(?P.*)?$" diff --git a/bindings/Python/setup.py b/bindings/Python/setup.py deleted file mode 100644 index 4482f985..00000000 --- a/bindings/Python/setup.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -from pathlib import Path -import sys - -import numpy -from setuptools import setup, Extension -from Cython.Build import build_ext - - -def get_cbsdk_path(root): - # Check for environment variable first (for CI/CD builds) - env_path = os.environ.get("CBSDK_INSTALL_PATH") - if env_path and os.path.exists(env_path): - return env_path - - if "win32" in sys.platform: - vs_out = os.path.join(root, "out", "install", "x64-Release") - if os.path.exists(vs_out): - return vs_out - for build_dir in [".", "build", "cmake-build-release"]: - cbsdk_path = os.path.join(root, build_dir, "install") - if os.path.exists(cbsdk_path): - return cbsdk_path - - -def get_extras(): - # Find all the extra include files, libraries, and link arguments we need to install. - cur = os.path.dirname(os.path.abspath(__file__)) - root = os.path.abspath(os.path.join(cur, "..", "..")) - cbsdk_path = get_cbsdk_path(root) - - # Prepare return variables - x_includes = [] - x_libs = [] - x_link_args = [] - x_compile_args = [] - - # C++17 standard (matching CMake build configuration) - x_compile_args.append("/std:c++17" if "win32" in sys.platform else "-std=c++17") - - # CBSDK - x_includes.append(os.path.join(cbsdk_path, "include")) - if sys.platform == "darwin": - x_link_args += ["-L{path}".format(path=os.path.join(cbsdk_path, "lib"))] - elif "win32" in sys.platform: - x_link_args.append("/LIBPATH:" + str(Path(cbsdk_path) / f"lib")) - else: - x_link_args.append(f"-L{os.path.join(cbsdk_path, 'lib')}") - - # pugixml for CCF - for build_dir in [".", "build", "cmake-build-release"]: - pugixml_path = Path(cbsdk_path).parent / build_dir / "_deps" / "pugixml-build" - if (pugixml_path / "Release").exists(): - pugixml_path = pugixml_path / "Release" - if pugixml_path.exists(): - break - x_link_args.append( - f"/LIBPATH:{pugixml_path}" if "win32" in sys.platform else f"-L{pugixml_path}" - ) - x_libs.append("pugixml") - - # Other platform-specific libraries - if "win32" in sys.platform: - # Must include stdint (V2008 does not have it!) - x_includes.append(os.path.join(root, "compat")) - # add winsock, timer, and system libraries - x_libs += ["ws2_32", "winmm"] - x_libs += [ - "kernel32", - "user32", - "gdi32", - "winspool", - "shell32", - "ole32", - "oleaut32", - "uuid", - "comdlg32", - "advapi32", - ] - elif "darwin" not in sys.platform: - x_libs.append("rt") - - return x_includes, x_libs, x_link_args, x_compile_args - - -extra_includes, extra_libs, extra_link_args, extra_compile_args = get_extras() - - -setup( - ext_modules=[ - Extension( - "cerelink.cbpy", - sources=["cerelink/cbpy.pyx", "cerelink/cbsdk_helper.cpp"], - libraries=["cbsdk_static"] + extra_libs, - extra_compile_args=extra_compile_args, - extra_link_args=extra_link_args, - include_dirs=[numpy.get_include()] + extra_includes, - language="c++", - ) - ], - cmdclass={"build_ext": build_ext}, - # See pyproject.toml for rest of configuration. -) diff --git a/bindings/cbmex/Central.rc b/bindings/cbmex/Central.rc deleted file mode 100755 index 8f357bd4..00000000 --- a/bindings/cbmex/Central.rc +++ /dev/null @@ -1,389 +0,0 @@ -// =STS=> Central.rc[2460].aa00 closed SMID:1 -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "afxres.h" -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (U.S.) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -#ifdef _WIN32 -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) -#endif //_WIN32 - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""afxres.h""\r\0" -END - -3 TEXTINCLUDE -BEGIN - "#define _AFX_NO_SPLITTER_RESOURCES\r\n" - "#define _AFX_NO_OLE_RESOURCES\r\n" - "#define _AFX_NO_TRACKER_RESOURCES\r\n" - "#define _AFX_NO_PROPERTY_RESOURCES\r\n" - "\r\n" - "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\r\n" - "#ifdef _WIN32\r\n" - "LANGUAGE 9, 1\r\n" - "#pragma code_page(1252)\r\n" - "#endif //_WIN32\r\n" - "#include ""res\\Central.rc2"" // non-Microsoft Visual C++ edited resources\r\n" - "#define VERSION_DESCRIPTION ""Central Application""\r\n" - "#define VERSION_FILENAME ""Central.exe""\r\n" - "#include ""..\\cbhwlib\\CkiVersion.rc"" // non-Microsoft Visual C++ edited resources\r\n" - "#include ""afxres.rc"" // Standard components\r\n" - "#endif\r\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_CENTRAL ICON "res\\central.ico" -IDI_CONFIG ICON "res\\config.ico" -IDI_PANEL ICON "res\\panel.ico" -IDI_RASTER ICON "res\\raster.ico" -IDI_SINGLE ICON "res\\single.ico" -IDI_AMAP ICON "res\\amap.ico" -IDI_FILE ICON "res\\file.ico" -IDI_PLAY ICON "res\\play.ico" -IDI_PAUSE ICON "res\\pause.ico" - -///////////////////////////////////////////////////////////////////////////// -// -// Dialog -// - -IDD_ABOUT DIALOG 0, 0, 211, 57 -STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "About Central" -FONT 8, "MS Sans Serif" -BEGIN - ICON IDI_CENTRAL,IDC_STATIC,5,10,21,20,SS_SUNKEN - CTEXT "Central Application",IDC_STATIC_APP_VERSION,30,5,150,10,SS_NOPREFIX | SS_CENTERIMAGE - PUSHBUTTON "OK",IDOK,184,10,20,20 - CTEXT "built with Hardware Library",IDC_STATIC_LIB_VERSION,30,15,150,10,SS_NOPREFIX | SS_CENTERIMAGE - CTEXT "Copyright (C) 2003 - 2006 Cyberkinetics, Inc.",IDC_STATIC,30,25,150,10,SS_NOPREFIX | SS_CENTERIMAGE - CTEXT "NSP Firmware",IDC_STATIC_NSP_VERSION,30,45,150,10,SS_NOPREFIX | SS_CENTERIMAGE -END - -IDD_CENTRAL_DIALOG DIALOGEX 0, 0, 111, 294 -STYLE DS_SETFONT | DS_3DLOOK | WS_CAPTION | WS_SYSMENU -EXSTYLE WS_EX_APPWINDOW -CAPTION "Central" -MENU IDM_MENU -FONT 8, "MS Sans Serif", 0, 0, 0x1 -BEGIN - LTEXT "System Load",IDC_STATIC_load,7,100,43,8 - CTEXT "",IDC_STATIC_rate,51,99,22,10,SS_CENTERIMAGE | SS_SUNKEN - LTEXT "MByte/s",IDC_STATIC_mbs,76,100,28,8 - CONTROL "",IDC_STATIC_rateplot,"Static",SS_BITMAP | SS_CENTERIMAGE | SS_REALSIZEIMAGE,5,111,92,45,WS_EX_CLIENTEDGE - CTEXT "12",IDC_STATIC_12,98,110,8,8 - CTEXT "6",IDC_STATIC_6,98,129,8,8 - CTEXT "0",IDC_STATIC_0,98,149,8,8 - LTEXT "Initializing",IDC_STATIC_status,5,201,100,10,SS_SUNKEN - PUSHBUTTON "Reset",IDC_BUTTON_reset,60,186,45,10 - CONTROL "Hold",IDC_CHECK_hold,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | NOT WS_VISIBLE | WS_DISABLED | WS_TABSTOP,5,186,45,10 - CTEXT "Pkts Sent",IDC_STATIC_psent,5,161,50,10,SS_CENTERIMAGE - CTEXT "8888888888",IDC_STATIC_xmt,55,161,50,10,SS_SUNKEN - CTEXT "Pkts Received",IDC_STATIC_prec,5,171,50,10,SS_CENTERIMAGE - CTEXT "8888888888",IDC_STATIC_rec,55,171,50,10,SS_SUNKEN - PUSHBUTTON "Hardware Configuration",IDC_BUTTON_config,5,6,100,10,BS_CENTER | BS_VCENTER | BS_MULTILINE - PUSHBUTTON "Spike Panel",IDC_BUTTON_panel,5,16,100,10,BS_MULTILINE - PUSHBUTTON "Raster Plot",IDC_BUTTON_raster,5,26,100,10 - PUSHBUTTON "Single Neural Channel ",IDC_BUTTON_sort,5,36,100,10 - PUSHBUTTON "Activity Map",IDC_BUTTON_amap,5,46,100,10 - PUSHBUTTON "File Storage",IDC_BUTTON_file,5,56,100,10 - PUSHBUTTON "Signal to Noise Ratio",IDC_BUTTON_SNR,5,66,100,10 - PUSHBUTTON "Neural Modulation",IDC_BUTTON_MODULATION,5,76,100,10 - PUSHBUTTON "Thresholding",IDC_BUTTON_AUTOTHRESH,5,76,100,10,NOT WS_VISIBLE - PUSHBUTTON "Impedance Tester",IDC_BUTTON_IMPEDANCE_TESTER,5,76,100,10,NOT WS_VISIBLE - PUSHBUTTON "Crosstalk",IDC_BUTTON_CROSSTALK,5,76,100,10,NOT WS_VISIBLE - GROUPBOX "Playback",IDC_STATIC_playback,7,216,100,65 - CONTROL "Slider1",IDC_SLD_POSITION,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS,11,249,90,10 - CONTROL "Play",IDC_RDO_PLAY,"Button",BS_AUTORADIOBUTTON | BS_ICON | BS_PUSHLIKE,20,259,18,16 - CONTROL "Pause",IDC_RDO_PAUSE,"Button",BS_AUTORADIOBUTTON | BS_ICON | BS_PUSHLIKE,43,259,18,16 - RTEXT "min",IDC_TIME,57,241,46,8 - EDITTEXT IDC_FILENAME,12,226,90,12,ES_AUTOHSCROLL | ES_READONLY - LTEXT "min",IDC_TIME_ELAPSED,14,241,46,8 -END - -IDD_DLG_DATA_LOST DIALOG 0, 0, 263, 102 -STYLE DS_SETFONT | DS_MODALFRAME | DS_SETFOREGROUND | WS_POPUP | WS_VISIBLE | WS_CAPTION -CAPTION "Error" -FONT 8, "MS Sans Serif" -BEGIN - DEFPUSHBUTTON "OK",IDOK,105,80,50,14 - LTEXT "",IDC_DL_TEXT,15,5,235,65 -END - -IDD_DLG_WINOPTIONS DIALOGEX 0, 0, 224, 199 -STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "Window Options" -FONT 8, "MS Sans Serif", 0, 0, 0x0 -BEGIN - GROUPBOX "Allow multiple instances of:",IDC_STATIC,7,7,142,60 - CONTROL "Hardware Configuration Window",IDC_CHK_MULTI_HW,"Button",BS_AUTOCHECKBOX | BS_TOP | BS_MULTILINE | WS_GROUP | WS_TABSTOP,15,20,114,10 - CONTROL "Spike Panel Window",IDC_CHK_MULTI_PANEL,"Button",BS_AUTOCHECKBOX | BS_TOP | BS_MULTILINE | WS_TABSTOP,15,34,83,10 - CONTROL "Raster Plot Window",IDC_CHK_MULTI_RASTER,"Button",BS_AUTOCHECKBOX | BS_TOP | BS_MULTILINE | WS_TABSTOP,15,48,77,10 - GROUPBOX "Sorting Log Rules",IDC_GP_SORT,7,75,142,60 - CONTROL "Do Not Log Sort Changes",IDC_SORT_LOG_RULE_1,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,17,89,125,10 - CONTROL "Log Sorting Changes on 'Record'",IDC_SORT_LOG_RULE_2, - "Button",BS_AUTORADIOBUTTON | WS_TABSTOP,17,102,125,10 - CONTROL "Log Sorting Changes 'Always'",IDC_SORT_LOG_RULE_3, - "Button",BS_AUTORADIOBUTTON | WS_TABSTOP,17,116,125,10 - GROUPBOX "File Storage App Interface",IDC_GP_FILE,7,146,142,46 - CONTROL "2.x Interface",IDC_RDO_FILE_2X,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,14,159,125,10 - CONTROL "TOC Interface",IDC_RDO_FILE_TOC,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,14,173,125,10 - DEFPUSHBUTTON "OK",IDOK,167,7,50,14,WS_GROUP - PUSHBUTTON "Cancel",IDCANCEL,167,28,50,14 -END - -IDD_DLG_SS_CANCEL_OPTIONS DIALOGEX 0, 0, 186, 110 -STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "Dialog" -FONT 8, "MS Sans Serif", 0, 0, 0x0 -BEGIN - CONTROL "Rebuild Spike Sorting Model",IDC_RADIO_SS_RESTART, - "Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,7,7,105,10 - CONTROL "Lock number of units",IDC_RADIO_SS_LOCK,"Button",BS_AUTORADIOBUTTON,7,23,82,10 - CONTROL "Lock number of units AND statistics",IDC_RADIO_SS_LK_US, - "Button",BS_AUTORADIOBUTTON,7,39,126,10 - CONTROL "Load sorting rules",IDC_RADIO_SS_LOAD,"Button",BS_AUTORADIOBUTTON,7,55,71,10 - CONTROL "Turn off sorting/extraction on all channels",IDC_RADIO_SORTING_OFF, - "Button",BS_AUTORADIOBUTTON,7,71,147,10 - DEFPUSHBUTTON "OK",IDOK,36,89,50,14,WS_GROUP - PUSHBUTTON "Cancel",IDCANCEL,99,89,50,14 -END - -IDD_DLG_SUMMARY DIALOGEX 0, 0, 304, 172 -STYLE DS_SETFONT | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME -CAPTION "System Summary" -FONT 8, "MS Sans Serif", 0, 0, 0x0 -BEGIN - PUSHBUTTON "Disable Crosstalking Channels",IDC_BTN_DISABLE_CROSSTALK,66,151,111,14 - DEFPUSHBUTTON "Close",IDOK,7,151,50,14 - LTEXT "Static",IDC_TXT_SUMMARY,7,7,290,136 - PUSHBUTTON "Launch Impedance Tester",IDC_BTN_LAUNCH_IMPEDANCE,186,151,111,14 -END - -IDD_DLG_CONT_SORT DIALOG 0, 0, 187, 58 -STYLE DS_SETFONT | DS_MODALFRAME | DS_3DLOOK | DS_NOFAILCREATE | WS_POPUP | WS_VISIBLE | WS_CAPTION -CAPTION "Sort ?" -FONT 8, "MS Sans Serif" -BEGIN - DEFPUSHBUTTON "Yes",IDOK,19,37,68,14,BS_CENTER | BS_VCENTER - PUSHBUTTON "No",IDCANCEL,101,37,67,14,BS_CENTER | BS_VCENTER - CTEXT "Run Sorting module and startup diagnostics??",IDC_STATIC,7,7,173,18,SS_CENTERIMAGE | SS_SUNKEN -END - - -///////////////////////////////////////////////////////////////////////////// -// -// DESIGNINFO -// - -#ifdef APSTUDIO_INVOKED -GUIDELINES DESIGNINFO -BEGIN - IDD_ABOUT, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 204 - TOPMARGIN, 7 - BOTTOMMARGIN, 50 - END - - IDD_CENTRAL_DIALOG, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 104 - TOPMARGIN, 7 - BOTTOMMARGIN, 285 - END - - IDD_DLG_DATA_LOST, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 256 - TOPMARGIN, 7 - BOTTOMMARGIN, 95 - END - - IDD_DLG_WINOPTIONS, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 217 - TOPMARGIN, 7 - BOTTOMMARGIN, 191 - END - - IDD_DLG_SS_CANCEL_OPTIONS, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 179 - TOPMARGIN, 7 - BOTTOMMARGIN, 103 - END - - IDD_DLG_SUMMARY, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 297 - TOPMARGIN, 7 - BOTTOMMARGIN, 165 - END - - IDD_DLG_CONT_SORT, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 180 - TOPMARGIN, 7 - BOTTOMMARGIN, 51 - END -END -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Menu -// - -IDM_MENU MENU -BEGIN - POPUP "&File" - BEGIN - MENUITEM "Open Nev File...", ID_FILE_OPEN_NEV - MENUITEM "Close NEV File", ID_FILE_CLOSE_NEV - MENUITEM SEPARATOR - MENUITEM "Open Video File...", ID_FILE_OPEN_VIDEO - MENUITEM "Close Video File", ID_FILE_CLOSE_VIDEO - MENUITEM SEPARATOR - MENUITEM "Load System Settings...", ID_FILE_LOADSYSTEM - MENUITEM "Load Sorting Rules...", ID_FILE_LOADSORTINGRULES - MENUITEM SEPARATOR - MENUITEM "Save System Settings...", ID_FILE_SAVESETTINGS - MENUITEM "Save Sorting Rules...", ID_FILE_SAVESORTINGRULES - MENUITEM SEPARATOR - MENUITEM "Close Applications", ID_FILE_CLOSE - MENUITEM SEPARATOR - MENUITEM "Hardware Standby and Close", ID_FILE_STANDBY - MENUITEM "Hardware Shutdown and Close", ID_FILE_SHUTDOWN - END - POPUP "&Tools" - BEGIN - MENUITEM "Thresholding...", ID_EDIT_THRESHOLDING - MENUITEM "Options...", ID_EDIT_WINOPTIONS - MENUITEM "BrainGate Options", IDM_TOOLS_BRAINGATEOPTIONS - MENUITEM SEPARATOR - MENUITEM "Lock Unit Statistics", ID_TOOLS_LOCKUNITSTATS - MENUITEM "Rebuild Spike Sorting Model", ID_TOOLS_RESTARTSPIKESORTING - END - POPUP "&Windows" - BEGIN - MENUITEM "About Central", ID_WINDOWS_ABOUT - MENUITEM "System Summary", IDM_WINDOWS_SUMMARY - MENUITEM SEPARATOR - MENUITEM "Hardware Configuration", IDC_BUTTON_config - MENUITEM "Spike Panel", IDC_BUTTON_panel - MENUITEM "Raster Plot", IDC_BUTTON_raster - MENUITEM "Single Neural Channel", IDC_BUTTON_sort - MENUITEM "Activity Map", IDC_BUTTON_amap - MENUITEM "File Storage", IDC_BUTTON_file - MENUITEM "Impedance Tester", IDC_BUTTON_AutoImpedance - MENUITEM "Crosstalk", IDC_BUTTON_CROSSTALK - END -END - - -///////////////////////////////////////////////////////////////////////////// -// -// String Table -// - -STRINGTABLE -BEGIN - IDS_ABOUTBOX "&About Central..." - IDP_SOCKETS_INIT_FAILED "Windows sockets initialization failed." - IDS_SIM_ABOUTBOX "&About Central Sim..." - IDS_PLAY_ABOUTBOX "&About CentralPlay..." -END - -STRINGTABLE -BEGIN - ID_EDIT_THRESHOLDING "Set the threshold on all channels\nSet all thresholds" - ID_EDIT_WINOPTIONS "Change Window Options\nChange Window Options" -END - -STRINGTABLE -BEGIN - ID_TOOLS_RESTARTSPIKESORTING - "Spike sorting will start again at the beginning" - ID_TOOLS_SORTINGCONTROLS "Set sorting " - ID_TOOLS_LOCKUNITSTATS "Cease/Restart collection of stats for established units" -END - -STRINGTABLE -BEGIN - ID_FILE_OPEN "Open a NEV file to playback" - ID_FILE_CLOSE "Close the currently open NEV file." -END - -#endif // English (U.S.) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// -#define _AFX_NO_SPLITTER_RESOURCES -#define _AFX_NO_OLE_RESOURCES -#define _AFX_NO_TRACKER_RESOURCES -#define _AFX_NO_PROPERTY_RESOURCES - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -#ifdef _WIN32 -LANGUAGE 9, 1 -#pragma code_page(1252) -#endif //_WIN32 -#include "res\Central.rc2" // non-Microsoft Visual C++ edited resources -#define VERSION_DESCRIPTION "Central Application" -#define VERSION_FILENAME "Central.exe" -#include "..\cbhwlib\CkiVersion.rc" // non-Microsoft Visual C++ edited resources -#include "afxres.rc" // Standard components -#endif -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - diff --git a/bindings/cbmex/DigInOut.m b/bindings/cbmex/DigInOut.m deleted file mode 100755 index 71ff57cf..00000000 --- a/bindings/cbmex/DigInOut.m +++ /dev/null @@ -1,41 +0,0 @@ -% Author & Date: Hyrum L. Sessions 14 Sept 2009 -% Copyright: Blackrock Microsystems -% Workfile: DigInOut.m -% Purpose: Read serial data from the NSP and compare with a -% predefined value. If it is the same, generate a -% pulse on dout4 - -% This script will read data from the NSP for a period of 30 seconds. It -% is waiting for a character 'd' on the Serial I/O port of the NSP. If -% received it will generate a 10ms pulse on Digital Output 4 - -% initialize -close all; -clear variables; - -run_time = 30; % run for time -value = 100; % value to look for (100 = d) -channel_in = 152; % serial port = channel 152, digital = 151 -channel_out = 156; % dout 1 = 153, 2 = 154, 3 = 155, 4 = 156 - -t_col = tic; % collection time - -cbmex('open'); % open library -cbmex('trialconfig',1); % start library collecting data - -start = tic(); - -while (run_time > toc(t_col)) - pause(0.05); % check every 50ms - t_test = toc(t_col); - spike_data = cbmex('trialdata', 1); % read data - found = (value == spike_data{channel_in, 3}); - if (0 ~= sum(found)) - cbmex('digitalout', channel_out, 1); - pause(0.01); - cbmex('digitalout', channel_out, 0); - end -end - -% close the app -cbmex('close'); diff --git a/bindings/cbmex/Makefile b/bindings/cbmex/Makefile deleted file mode 100755 index d96be7aa..00000000 --- a/bindings/cbmex/Makefile +++ /dev/null @@ -1,306 +0,0 @@ -###################################################################################### -## -## Makefile for Cerebus API -## (c) Copyright 2012 - 2013 Blackrock Microsystems -## -## Workfile: Makefile -## Archive: /Cerebus/Human/LinuxApps/cbmex/Makefile -## Revision: 1 -## Date: 4/29/12 12:04p -## Author: Ehsan -## -## -## Major targets: -## all - make all the following targets: -## sdk - make libcbsdk -## mex - make cbmex -## pysdk - make python binding for sdk -## -## Special targets: -## x64 - make all targets in 64bit -## debug - make all targets in debug mode -## install - place all scripts and binaries in the conventional directories -## uninstall - remove all scripts and binaries that may be in OS directory tree -## clean - clean up -## test - make sdk, install it and make the test -## -## use ARCH=x64 (ex: make all ARCH=x64) in order to make 64-bit for any target -## use DEBUG=d (ex: make mex DEBUG=d) in order to force debug information for any target -## -###################################################################################### - -#========================================================================== -# Operating System -#========================================================================== -OS := $(shell echo %OS%) -ifeq ($(OS),Windows_NT) -WIN32 := 1 -PWD := $(CURDIR) -else -# figure out OSX or Linux -endif - -ifeq ($(ARCH),x64) -ARCHFLAG := -m64 -else -ARCH := -ARCHFLAG := -m32 -endif - -#========================================================================== -# UTILITIES -#========================================================================== -MKPARENT := mkdir -p `dirname $(1)` -ECHO := @echo -CP := @cp - - -#========================================================================== -# CONSTANTS -#========================================================================== - -CC := gcc -CXX := g++ -MOC := moc-qt4 -PYTHONVER := python2.7 - -# SDK naming -CBSDKLIBNAME = cbsdk$(ARCH)$(DEBUG) -CBSDKBASENAME = lib$(CBSDKLIBNAME) -CBSDKSO = $(CBSDKBASENAME).so -CBSDKVER = $(shell $(ObjDir)/$(CBSDKMAINBIN)) -# internal soname -CBSDKSONAME = $(CBSDKBASENAME).so.$(CBSDKVER) -# SDK versiong binary -CBSDKMAINBIN = cbsdk$(ARCH)$(DEBUG) -# SDK versiong binary -CBSDKTESTBIN = testcbsdk$(ARCH)$(DEBUG) - -# Python naming -CBPYBASENAME = cbpy$(ARCH) -CBPYSO = $(CBPYBASENAME)$(DEBUG).so - -# Mex naming -ifdef OSX -CBMEXSO = cbmex$(DEBUG).mexmaci64 -CBMEXLIBDIRS := $(PWD)/../Matlab/lib/osx64 -BINPREFIX := ../osx64 -else -ifeq ($(ARCH),x64) -CBMEXSO = cbmex$(DEBUG).mexa64 -CBMEXLIBDIRS := -L$(PWD)/../Matlab/lib/linux64 -BINPREFIX := ../linux64 -else -CBMEXSO = cbmex$(DEBUG).mexglx -CBMEXLIBDIRS := -L$(PWD)/../Matlab/lib/linux32 -BINPREFIX := ../linux32 -endif -endif - -INCLUDEDIRS := \ - $(shell pkg-config --cflags-only-I QtCore QtXml)\ - -I$(PWD)/../Central -I$(PWD)/../CentralCommon -I$(PWD)/../cbhwlib \ - -I$(PWD)/../Matlab/include -I/usr/include/$(PYTHONVER) -I/usr/include/numpy \ - -# Additional libraries -LIBS := -lQtCore -lQtXml -lpthread -lrt -MEXLIBS := -lmex -lmx -PYSDKLIBS := -l$(PYTHONVER) - -CFLAGS := -Wall -Wno-reorder $(ARCHFLAG) -fPIC -fvisibility=hidden \ - -maccumulate-outgoing-args \ - -funroll-loops -finline-functions \ - -fno-strict-aliasing \ - $(INCLUDEDIRS) - -ifneq ($(ARCH),x64) -# malign-double makes no sense in 64-bit -CFLAGS += -malign-double -endif - -EXTRA_DEFINES := -DCBSDK_EXPORTS -DQT_CORE -DQT_XML -DNO_AFX -DQT_APP - -CFLAGS += $(EXTRA_DEFINES) - -ifdef DEBUG -BinDir := $(BINPREFIX)/debug -CFLAGS += -O0 -g3 -UNDEBUG -DDEBUG -else -BinDir := $(BINPREFIX)/release -CFLAGS += -O2 -DNDEBUG -UDEBUG -endif - -# Directory for intermediate files and object files -ObjDir := .obj -MocDir := .moc$(DEBUG) -PySdkDir := .pysdk - -# For linking -LDFLAGS = $(LIBDIRS) $(LIBS) -LFLAGS = $(ARCHFLAG) -shared $(LDFLAGS) -Wl,-soname,$(CBSDKSONAME) -Wl,--no-undefined -MEXLFLAGS = $(LFLAGS) $(MEXLIBS) -PYSDKLFLAGS = $(LFLAGS) $(PYSDKLIBS) - -# common sources -COMMON_SRC := ./cbsdk.cpp \ - ../cbhwlib/cbhwlib.cpp \ - ../cbhwlib/cbHwlibHi.cpp \ - ../Central/Instrument.cpp \ - ../Central/UDPsocket.cpp \ - ../cbhwlib/InstNetwork.cpp \ - ../cbhwlib/CCFUtils.cpp \ - ../cbhwlib/CCFUtilsBinary.cpp \ - ../cbhwlib/CCFUtilsXml.cpp \ - ../cbhwlib/CCFUtilsXmlItems.cpp \ - ../cbhwlib/CCFUtilsConcurrent.cpp \ - ../cbhwlib/XmlFile.cpp \ - -# moc'ed sources -MOC_HEADER := ../cbhwlib/InstNetwork.h \ - -# Mex sources -MEX_SRC := ./cbmex.cpp \ - -# Python sources -PY_SRC := ./cbpy.cpp \ - ./cbpytypes.cpp \ - -# where to look for the sources -VPATH := ../cbhwlib:../Central - -# object files from sources -MOC_OBJS := $(patsubst %.h, $(ObjDir)/$(MocDir)/moc_%$(ARCH)$(DEBUG).o, $(notdir $(MOC_HEADER))) -COMMON_OBJS := $(MOC_OBJS) $(patsubst %.cpp, $(ObjDir)/%$(ARCH)$(DEBUG).o, $(notdir $(COMMON_SRC))) -MEX_OBJS := $(patsubst %.cpp, $(ObjDir)/%$(ARCH)$(DEBUG).o, $(notdir $(MEX_SRC))) -PY_OBJS := $(patsubst %.cpp, $(ObjDir)/$(PySdkDir)/%$(ARCH)$(DEBUG).o, $(notdir $(PY_SRC))) - -#### This tag must be the first build tag -all: sdk mex pysdk - -sdk: prepare $(BinDir)/$(CBSDKSO) - @echo shared library done. - -mex: LIBDIRS := $(CBMEXLIBDIRS) -mex: prepare $(BinDir)/$(CBMEXSO) - @echo Matlab extension done. - -test: sdk install $(BinDir)/$(CBSDKTESTBIN) - @echo Test suite done. - -pysdk: prepare $(BinDir)/$(CBPYSO) - @echo Python extension done. - -.PHONY: debug -debug: DEBUG := d -debug: - $(MAKE) all DEBUG=$(DEBUG) - -.PHONY: x64 -x64: ARCH := x64 -x64: all - $(MAKE) all ARCH=$(ARCH) - -# the "common" object files -$(ObjDir)/%$(ARCH)$(DEBUG).o : %.cpp Makefile - @echo creating $@ ... - $(CXX) $(CFLAGS) $(EXTRA_CFLAGS) -c -o $@ $< - -# the "pysdk" object files -$(ObjDir)/$(PySdkDir)/%$(ARCH)$(DEBUG).o : EXTRA_CFLAGS += -DCBPYSDK -$(ObjDir)/$(PySdkDir)/%$(ARCH)$(DEBUG).o : %.cpp Makefile - @echo creating $@ ... - $(CXX) $(CFLAGS) $(EXTRA_CFLAGS) -c -o $@ $< - -# the SDK version -$(ObjDir)/$(CBSDKMAINBIN) : ./main.cpp ../CentralCommon/BmiVersion.h Makefile - @echo creating $@ ... - $(CXX) $(CFLAGS) $(EXTRA_CFLAGS) -o $@ $< - -# "moc" cpp files -.SECONDARY: $(patsubst %.h, $(MocDir)/moc_%.cpp, $(notdir $(MOC_HEADER))) -$(MocDir)/moc_%.cpp: ../cbhwlib/%.h Makefile - @echo Moc\'ing $< - $(MOC) $(EXTRA_DEFINES) $(INCLUDEDIRS) -I"$(PWD)/$(MocDir)" $< -o $(patsubst %.h, $(MocDir)/moc_%.cpp, $(notdir $<)) -$(MocDir)/moc_%.cpp: %.h Makefile - @echo Moc\'ing $< - $(MOC) $(EXTRA_DEFINES) $(INCLUDEDIRS) -I"$(PWD)/$(MocDir)" $< -o $(patsubst %.h, $(MocDir)/moc_%.cpp, $(notdir $<)) - -# "moc" object files -$(ObjDir)/$(MocDir)/%$(ARCH)$(DEBUG).o: $(MocDir)/%.cpp Makefile - @echo creating $@ ... - $(CXX) $(CFLAGS) $(EXTRA_CFLAGS) -c -o $@ $< - -# This will make the cbsdk shared library -$(BinDir)/$(CBSDKSO): $(ObjDir)/$(CBSDKMAINBIN) $(COMMON_OBJS) - @echo building shared library ... - $(CXX) -o $(BinDir)/$(CBSDKSONAME) $(COMMON_OBJS) $(LFLAGS) - -# This will make the cbpy shared library -$(BinDir)/$(CBPYSO) : $(ObjDir)/$(CBSDKMAINBIN) $(PY_OBJS) $(COMMON_OBJS) - @echo building Python extension ... - $(CXX) -o $(BinDir)/$(CBPYSO) $(PY_OBJS) $(COMMON_OBJS) $(PYSDKLFLAGS) - -# This will make the cbmex shared library -$(BinDir)/$(CBMEXSO): $(ObjDir)/$(CBSDKMAINBIN) $(MEX_OBJS) $(COMMON_OBJS) - @echo building Matlab extension ... - $(CXX) -o $(BinDir)/$(CBMEXSO) $(MEX_OBJS) $(COMMON_OBJS) $(MEXLFLAGS) - -# the SDK test suite -$(BinDir)/$(CBSDKTESTBIN) : ./testcbsdk.cpp Makefile - @echo creating $@ ... - $(CXX) -I$(PWD)/../cbhwlib -L$(BinDir) -l$(CBSDKLIBNAME) -o $@ $< - -# For installing to system wide use -.PHONY: install -install: $(BinDir)/$(CBSDKSONAME) - cp -pf $(BinDir)/$(CBSDKSONAME) /usr/local/lib - @chown $(shell whoami):$(shell whoami) /usr/local/lib/$(CBSDKSONAME) - @chmod 755 /usr/local/lib/$(CBSDKSONAME) - ln -sf /usr/local/lib/$(CBSDKSONAME) /usr/local/lib/$(CBSDKSO) - -.PHONY: uninstall -uninstall: - rm -f /usr/local/lib/$(CBSDKSO) - rm -f /usr/local/lib/$(CBSDKSONAME) - - -# Clean out all files leaving installed files alone -.PHONY: clean -clean: -ifdef WIN32 - @if exist $(MocDir) del /s /q $(MocDir)\* - @if exist $(MocDir) rmdir $(MocDir) - @if exist $(ObjDir)\\$(MocDir) del /s /q $(ObjDir)\\$(MocDir)\\*.* - @if exist $(ObjDir)\\$(MocDir) rmdir $(ObjDir)\\$(MocDir) - @if exist $(ObjDir)\\$(PySdkDir) del /s /q $(ObjDir)\\$(PySdkDir)\\*.* - @if exist $(ObjDir)\\$(PySdkDir) rmdir $(ObjDir)\\$(PySdkDir) - @if exist $(ObjDir) del /s /q $(ObjDir)\\*.* - @if exist $(ObjDir) rmdir $(ObjDir) - @if exist $(BinDir) del /s /q $(BinDir)\\*.* - @if exist $(BinDir) rmdir $(BinDir) -else - rm -rf $(MocDir) - rm -rf $(ObjDir) - rm -rf $(BinDir) -endif - -.PHONEY: prepare -prepare: prepare_dir $(ObjDir)/$(CBSDKMAINBIN) - @echo - @echo building started for $(CBSDKVER).$(if $(ARCH),$(ARCH),i686) ... - @echo - -.PHONY: prepare_dir -prepare_dir: -ifdef WIN32 - @if not exist $(ObjDir) mkdir $(ObjDir) - @if not exist $(ObjDir)\\$(MocDir) mkdir $(ObjDir)\\$(MocDir) - @if not exist $(ObjDir)\\$(PySdkDir) mkdir $(ObjDir)\\$(PySdkDir) - @if not exist $(MocDir) mkdir $(MocDir) - @if not exist $(BinDir) mkdir $(BinDir) -else - @mkdir -p $(ObjDir)/$(PySdkDir) - @mkdir -p $(ObjDir)/$(MocDir) - @mkdir -p $(MocDir) - @mkdir -p $(BinDir) -endif diff --git a/bindings/cbmex/RealSpec.m b/bindings/cbmex/RealSpec.m deleted file mode 100755 index 6a7948fa..00000000 --- a/bindings/cbmex/RealSpec.m +++ /dev/null @@ -1,69 +0,0 @@ -% Author and Date: Ehsan Azar 14 Sept 2009 -% Copyright: Blackrock Microsystems -% Workfile: RealSpec.m -% Purpose: Realtime spectrum display. All sampled channels are displayed. - -close all; -clear variables; - -f_disp = 0:0.1:15; % the range of frequency to show spectrum over. -% Use f_disp = [] if you want the entire spectrum - -collect_time = 0.1; % collect samples for this time -display_period = 0.5; % display spectrum every this amount of time - -cbmex('open'); % open library - -proc_fig = figure; % main display -set(proc_fig, 'Name', 'Close this figure to stop'); -xlabel('frequency (Hz)'); -ylabel('magnitude (dB)'); - -cbmex('trialconfig', 1); % empty the buffer - -t_disp0 = tic; % display time -t_col0 = tic; % collection time -bCollect = true; % do we need to collect - % while the figure is open -while (ishandle(proc_fig)) - - if (bCollect) - et_col = toc(t_col0); % elapsed time of collection - if (et_col >= collect_time) - [spike_data, t_buf1, continuous_data] = cbmex('trialdata',1); % read some data - nGraphs = size(continuous_data,1); % number of graphs - % if the figure is still open - if (ishandle(proc_fig)) - % graph all - for ii=1:nGraphs - % get frquency of sampling - fs0 = continuous_data{ii,2}; - % get the ii'th channel data - data = continuous_data{ii,3}; - % number of samples to run through fft - collect_size = min(size(data), collect_time * fs0); - x = data(1:collect_size); - if isempty(f_disp) - [psd, f] = periodogram(double(x),[],'onesided',512,fs0); - else - [psd, f] = periodogram(double(x),[],f_disp,fs0); - end - subplot(nGraphs,1,ii,'Parent',proc_fig); - plot(f, 10*log10(psd), 'b');title(sprintf('fs = %d t = %f', fs0, t_buf1)); - xlabel('frequency (Hz)');ylabel('magnitude (dB)'); - end - drawnow; - end - bCollect = false; - end - end - - et_disp = toc(t_disp0); % elapsed time since last display - if (et_disp >= display_period) - t_col0 = tic; % collection time - t_disp0 = tic; % restart the period - bCollect = true; % start collection - end -end -cbmex('close'); % always close - diff --git a/bindings/cbmex/cbMex.rc b/bindings/cbmex/cbMex.rc deleted file mode 100755 index 7a642a76..00000000 --- a/bindings/cbmex/cbMex.rc +++ /dev/null @@ -1,69 +0,0 @@ -// =STS=> cbMex.rc[2458].aa01 closed SMID:2 -//Microsoft Developer Studio generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "afxres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (U.S.) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -#ifdef _WIN32 -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) -#endif //_WIN32 - - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""afxres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "#define VERSION_DESCRIPTION ""cbmex dll""\r\n" - "#define VERSION_FILENAME ""cbmex.dll""\r\n" - "#include ""..\\cbhwlib\\CkiVersion.rc"" // non-Microsoft Visual C++ edited resources\r\n" -END - -#endif // APSTUDIO_INVOKED - -#endif // English (U.S.) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - -#define VERSION_DESCRIPTION "cbmex dll" -#define VERSION_FILENAME "cbmex.dll" -#include "..\cbhwlib\CkiVersion.rc" // non-Microsoft Visual C++ edited resources - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - diff --git a/bindings/cbmex/cbmex.cpp b/bindings/cbmex/cbmex.cpp deleted file mode 100755 index 3ac2ab83..00000000 --- a/bindings/cbmex/cbmex.cpp +++ /dev/null @@ -1,4001 +0,0 @@ -// =STS=> cbmex.cpp[4264].aa38 submit SMID:41 -/////////////////////////////////////////////////////////////////////// -// -// Cerebus MATLAB executable SDK -// -// Copyright (C) 2001-2003 Bionic Technologies, Inc. -// (c) Copyright 2003 - 2008 Cyberkinetics, Inc -// (c) Copyright 2008 - 2014 Blackrock Microsystems, LLC -// -// $Workfile: cbmex.cpp $ -// $Archive: /Cerebus/Human/WindowsApps/cbmex/cbmex.cpp $ -// -// -// Note: -// Make sure only the SDK is used here, and not cbhwlib directly -// this will ensure SDK is capable of whatever MATLAB interface can do -// Do not throw exceptions, catch possible exceptions and handle them the earliest possible in this library -// Only functions are exported, no data -// -/*! \file cbmex.cpp - \brief This is the gateway routine for a MATLAB Math/Graphics Library-based C MATLAB MEX File. - -*/ - -#include "StdAfx.h" -#include // Use C++ default min and max implementation. -#include -#include -#include -#include "debugmacs.h" - -#include "cbmex.h" -#include -#include "cbHwlibHi.h" - -#ifdef _DEBUG -// Comment this line not to have a debug console when debugging cbsdk -#define DEBUG_CONSOLE -#endif - -bool g_bMexCall = false; // True if library called from MATLAB mex, False if from SDK - - -// Ok, this is unusual. I want to have this created once globally, and then -// I don't want anyone to be able to call this again. To make it "hidden" -// I put it into an anonymous namespace. -namespace -{ - typedef enum - { - CBMEX_FUNCTION_HELP = 0, - CBMEX_FUNCTION_OPEN, - CBMEX_FUNCTION_CLOSE, - CBMEX_FUNCTION_TIME, - CBMEX_FUNCTION_TRIALCONFIG, - CBMEX_FUNCTION_CHANLABEL, - CBMEX_FUNCTION_TRIALDATA, - CBMEX_FUNCTION_TRIALCOMMENT, - CBMEX_FUNCTION_TRIALTRACKING, - CBMEX_FUNCTION_FILECONFIG, - CBMEX_FUNCTION_DIGITALOUT, - CBMEX_FUNCTION_ANALOGOUT, - CBMEX_FUNCTION_MASK, - CBMEX_FUNCTION_COMMENT, - CBMEX_FUNCTION_CONFIG, - CBMEX_FUNCTION_CCF, - CBMEX_FUNCTION_SYSTEM, - CBMEX_FUNCTION_SYNCHOUT, - CBMEX_FUNCTION_EXT, - - CBMEX_FUNCTION_COUNT, // This must be the last item - } MexFxnIndex; - - /** - * This is the look up table for the command to function list. - */ - typedef void(*PMexFxn)( - int nlhs, ///< Number of left hand side (output) arguments - mxArray *plhs[], ///< Array of left hand side arguments - int nrhs, ///< Number of right hand side (input) arguments - const mxArray *prhs[]);///< Array of right hand side arguments - - /** - * Contains the Function and Function Index to use. - */ - typedef std::pair NAME_PAIR; - - /** - * Defines a table entry with NAME,FXN and FXN_IDX to use. - * \n LUT = Look Up Table - */ - typedef std::map NAME_LUT; - - /** - * Creates the look-up table for function lists and commands. - */ - NAME_LUT CreateLut() - { - NAME_LUT table; - table["help" ] = NAME_PAIR(&::OnHelp, CBMEX_FUNCTION_HELP); - table["open" ] = NAME_PAIR(&::OnOpen, CBMEX_FUNCTION_OPEN); - table["close" ] = NAME_PAIR(&::OnClose, CBMEX_FUNCTION_CLOSE); - table["time" ] = NAME_PAIR(&::OnTime, CBMEX_FUNCTION_TIME); - table["trialconfig" ] = NAME_PAIR(&::OnTrialConfig, CBMEX_FUNCTION_TRIALCONFIG); - table["chanlabel" ] = NAME_PAIR(&::OnChanLabel, CBMEX_FUNCTION_CHANLABEL); - table["trialdata" ] = NAME_PAIR(&::OnTrialData, CBMEX_FUNCTION_TRIALDATA); - table["trialcomment" ] = NAME_PAIR(&::OnTrialComment, CBMEX_FUNCTION_TRIALCOMMENT); - table["trialtracking" ] = NAME_PAIR(&::OnTrialTracking, CBMEX_FUNCTION_TRIALTRACKING); - table["fileconfig" ] = NAME_PAIR(&::OnFileConfig, CBMEX_FUNCTION_FILECONFIG); - table["digitalout" ] = NAME_PAIR(&::OnDigitalOut, CBMEX_FUNCTION_DIGITALOUT); - table["analogout" ] = NAME_PAIR(&::OnAnalogOut, CBMEX_FUNCTION_ANALOGOUT); - table["mask" ] = NAME_PAIR(&::OnMask, CBMEX_FUNCTION_MASK); - table["comment" ] = NAME_PAIR(&::OnComment, CBMEX_FUNCTION_COMMENT); - table["config" ] = NAME_PAIR(&::OnConfig, CBMEX_FUNCTION_CONFIG); - table["ccf" ] = NAME_PAIR(&::OnCCF, CBMEX_FUNCTION_CCF); - table["system" ] = NAME_PAIR(&::OnSystem, CBMEX_FUNCTION_SYSTEM); - table["synchout" ] = NAME_PAIR(&::OnSynchOut, CBMEX_FUNCTION_SYNCHOUT); - table["ext" ] = NAME_PAIR(&::OnExtCmd, CBMEX_FUNCTION_EXT); - return table; - }; -}; - -/** -* Cleanup function to be called at exit of the extension -*/ -static void matexit() -{ - if (g_bMexCall) - { - for (int i = 0; i < cbMAXOPEN; ++i) - cbSdkClose(i); - mexPrintf("Cerebus interface unloaded\n"); -#ifdef DEBUG_CONSOLE -#ifdef WINN32 - FreeConsole(); -#endif -#endif - } -} - -// Author & Date: Ehsan Azar 8 Nov 2012 -/** -* Prints the error message for sdk returned values. -* All non-success results are treated as error, and will drop to MATLAB prompt. -* Command should handle special cases themselves. -* -* @param[in] res Returned result value by SDK -* @param[in] szCustom Custom error message to print (Optional) -*/ -void PrintErrorSDK(cbSdkResult res, const char * szCustom = NULL) -{ - if (szCustom != NULL && res != CBSDKRESULT_SUCCESS) - { - mexPrintf(szCustom); - mexPrintf(":\n"); - } - - switch(res) - { - case CBSDKRESULT_WARNCONVERT: - mexErrMsgTxt("File conversion is needed"); - break; - case CBSDKRESULT_WARNCLOSED: - mexErrMsgTxt("Library is already closed"); - break; - case CBSDKRESULT_WARNOPEN: - mexErrMsgTxt("Library is already opened"); - break; - case CBSDKRESULT_SUCCESS: - // Do nothing - break; - case CBSDKRESULT_NOTIMPLEMENTED: - mexErrMsgTxt("Not implemented"); - break; - case CBSDKRESULT_UNKNOWN: - mexErrMsgTxt("Unknown error"); - break; - case CBSDKRESULT_INVALIDPARAM: - mexErrMsgTxt("Invalid parameter"); - break; - case CBSDKRESULT_CLOSED: - mexErrMsgTxt("Interface is closed cannot do this operation"); - break; - case CBSDKRESULT_OPEN: - mexErrMsgTxt("Interface is open cannot do this operation"); - break; - case CBSDKRESULT_NULLPTR: - mexErrMsgTxt("Null pointer"); - break; - case CBSDKRESULT_ERROPENCENTRAL: - mexErrMsgTxt("Unable to open Central interface"); - break; - case CBSDKRESULT_ERROPENUDP: - mexErrMsgTxt("Unable to open UDP interface (might happen if default)"); - break; - case CBSDKRESULT_ERROPENUDPPORT: - mexErrMsgTxt("Unable to open UDP port"); - break; - case CBSDKRESULT_ERRMEMORYTRIAL: - mexErrMsgTxt("Unable to allocate RAM for trial cache data"); - break; - case CBSDKRESULT_ERROPENUDPTHREAD: - mexErrMsgTxt("Unable to open UDP timer thread"); - break; - case CBSDKRESULT_ERROPENCENTRALTHREAD: - mexErrMsgTxt("Unable to open Central communication thread"); - break; - case CBSDKRESULT_INVALIDCHANNEL: - mexErrMsgTxt("Invalid channel number"); - break; - case CBSDKRESULT_INVALIDCOMMENT: - mexErrMsgTxt("Comment too long or invalid"); - break; - case CBSDKRESULT_INVALIDFILENAME: - mexErrMsgTxt("Filename too long or invalid"); - break; - case CBSDKRESULT_INVALIDCALLBACKTYPE: - mexErrMsgTxt("Invalid callback type"); - break; - case CBSDKRESULT_CALLBACKREGFAILED: - mexErrMsgTxt("Callback register/unregister failed"); - break; - case CBSDKRESULT_ERRCONFIG: - mexErrMsgTxt("Trying to run an unconfigured method"); - break; - case CBSDKRESULT_INVALIDTRACKABLE: - mexErrMsgTxt("Invalid trackable id, or trackable not present"); - break; - case CBSDKRESULT_INVALIDVIDEOSRC: - mexErrMsgTxt("Invalid video source id, or video source not present"); - break; - case CBSDKRESULT_ERROPENFILE: - mexErrMsgTxt("Cannot open file"); - break; - case CBSDKRESULT_ERRFORMATFILE: - mexErrMsgTxt("Wrong file format"); - break; - case CBSDKRESULT_OPTERRUDP: - mexErrMsgTxt("Socket option error (possibly permission issue)"); - break; - case CBSDKRESULT_MEMERRUDP: - mexErrMsgTxt(ERR_UDP_MESSAGE); - break; - case CBSDKRESULT_INVALIDINST: - mexErrMsgTxt("Invalid range, instrument address or instrument mode"); - break; - case CBSDKRESULT_ERRMEMORY: -#ifdef __APPLE__ - mexErrMsgTxt("Memory allocation error trying to establish master connection\n" - "Consider sysctl -w kern.sysv.shmmax=16777216\n" - " sysctl -w kern.sysv.shmall=4194304"); - -#else - mexErrMsgTxt("Memory allocation error trying to establish master connection"); -#endif - break; - case CBSDKRESULT_ERRINIT: - mexErrMsgTxt("Initialization error"); - break; - case CBSDKRESULT_TIMEOUT: - mexErrMsgTxt("Connection timeout error"); - break; - case CBSDKRESULT_BUSY: - mexErrMsgTxt("Resource is busy"); - break; - case CBSDKRESULT_ERROFFLINE: - mexErrMsgTxt("Instrument is offline"); - break; - default: - { - char errstr[128]; - sprintf(errstr, "Unexpected error (%d)", res); - mexErrMsgTxt(errstr); - } - break; - } -} - -// Author & Date: Ehsan Azar 8 Nov 2012 -/** -* Prints the help for given command. -* -* @param[in] fxnidx the cbmex command index -* @param[in] bErr indicates if it is an error message, will terminate and fall into MATLAB prompt if true -* @param[in] szCustom if specified will be printed before the usage message -*/ -void PrintHelp(MexFxnIndex fxnidx, bool bErr = false, const char * szCustom = NULL) -{ - if (szCustom != NULL) - mexPrintf(szCustom); - - if (fxnidx > CBMEX_FUNCTION_COUNT) - fxnidx = CBMEX_FUNCTION_COUNT; - - const char * szUsage[CBMEX_FUNCTION_COUNT + 1] = { - CBMEX_USAGE_HELP, - CBMEX_USAGE_OPEN, - CBMEX_USAGE_CLOSE, - CBMEX_USAGE_TIME, - CBMEX_USAGE_TRIALCONFIG, - CBMEX_USAGE_CHANLABEL, - CBMEX_USAGE_TRIALDATA, - CBMEX_USAGE_TRIALCOMMENT, - CBMEX_USAGE_TRIALTRACKING, - CBMEX_USAGE_FILECONFIG, - CBMEX_USAGE_DIGITALOUT, - CBMEX_USAGE_ANALOGOUT, - CBMEX_USAGE_MASK, - CBMEX_USAGE_COMMENT, - CBMEX_USAGE_CONFIG, - CBMEX_USAGE_CCF, - CBMEX_USAGE_SYSTEM, - CBMEX_USAGE_SYNCHOUT, - CBMEX_USAGE_EXTENSION, - - // Keep this in the end - CBMEX_USAGE_CBMEX - }; - /** - * \todo For CBMEX_USAGE_CBMEX, iterate all command names, instead of hard-coded names help string - */ - if (bErr) - mexErrMsgTxt(szUsage[fxnidx]); - else - mexPrintf(szUsage[fxnidx]); -} - -// Author & Date: Ehsan Azar 8 Nov 2012 -/** -* Processing to do with the command "help command-name". -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnHelp( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - static NAME_LUT lut = CreateLut(); // The actual look up table - if (nrhs == 1) - { - PrintHelp(CBMEX_FUNCTION_COUNT); - return; - } - // make sure there is at least one output argument - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_HELP, true, "Too many outputs requested\n"); - if (nrhs > 2) - PrintHelp(CBMEX_FUNCTION_HELP, true, "Too many inputs provided\n"); - - MexFxnIndex fxnidx = CBMEX_FUNCTION_COUNT; - - char cmdstr[128]; - if (mxGetString(prhs[1], cmdstr, 16)) - { - PrintHelp(CBMEX_FUNCTION_HELP, true, "Invalid command name\n"); - } - - NAME_LUT::iterator it = lut.find(cmdstr); - if (it == lut.end()) - { - PrintHelp(CBMEX_FUNCTION_HELP, true, "Invalid command name\n"); - } - else - { - fxnidx = (*it).second.second; - PrintHelp(fxnidx); - } -} - -// Author & Date: Kirk Korver 13 Jun 2005 -/** -* Processing to do with the command "interface = open interface". -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnOpen( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 1; - char szInstIp[16] = ""; - char szCentralIp[16] = ""; - cbSdkConnection con; - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - if (nFirstParam < nrhs) - { - if (mxIsNumeric(prhs[nFirstParam])) - { - if (mxGetNumberOfElements(prhs[nFirstParam]) != 1) - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Invalid interface parameter\n"); - int type = 3; - type = (uint32_t)mxGetScalar(prhs[nFirstParam]); - if (type > 2) - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Invalid input interface value\n"); - conType = (cbSdkConnectionType)type; - nFirstParam++; // skip the optional - } - } - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - PARAM_RECBUFSIZE, - PARAM_INST_IP, - PARAM_INST_PORT, - PARAM_CENTRAL_IP, - PARAM_CENTRAL_PORT, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 32)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid\n", i); - PrintHelp(CBMEX_FUNCTION_OPEN, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else if (_strcmpi(cmdstr, "receive-buffer-size") == 0) - { - param = PARAM_RECBUFSIZE; - } - else if (_strcmpi(cmdstr, "inst-addr") == 0) - { - param = PARAM_INST_IP; - } - else if (_strcmpi(cmdstr, "inst-port") == 0) - { - param = PARAM_INST_PORT; - } - else if (_strcmpi(cmdstr, "central-addr") == 0) - { - param = PARAM_CENTRAL_IP; - } - else if (_strcmpi(cmdstr, "central-port") == 0) - { - param = PARAM_CENTRAL_PORT; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid\n", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_OPEN, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_RECBUFSIZE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Invalid receive buffer size"); - con.nRecBufSize = (int)mxGetScalar(prhs[i]); - break; - case PARAM_INST_IP: - if (mxGetString(prhs[i], szInstIp, 16)) - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Invalid instrument ip address"); - con.szOutIP = szInstIp; - break; - case PARAM_INST_PORT: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Invalid instrument port number"); - con.nOutPort = (int)mxGetScalar(prhs[i]); - break; - case PARAM_CENTRAL_IP: - if (mxGetString(prhs[i], szCentralIp, 16)) - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Invalid central ip address"); - con.szInIP = szCentralIp; - break; - case PARAM_CENTRAL_PORT: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Invalid central port number"); - con.nInPort = (int)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is mandatory - PrintHelp(CBMEX_FUNCTION_OPEN, true, "Last parameter requires value"); - } - - cbSdkVersion ver; - cbSdkResult res = cbSdkGetVersion(nInstance, &ver); - - if (conType == CBSDKCONNECTION_DEFAULT || conType == CBSDKCONNECTION_CENTRAL) - mexPrintf("Initializing real-time interface %d.%02d.%02d.%02d (protocol cb%d.%02d)...\n", ver.major, ver.minor, ver.release, ver.beta, ver.majorp, ver.minorp); - else - mexPrintf("Initializing UDP real-time interface %d.%02d.%02d.%02d (protocol cb%d.%02d)...\n", ver.major, ver.minor, ver.release, ver.beta, ver.majorp, ver.minorp); - - cbSdkInstrumentType instType; - res = cbSdkOpen(nInstance, conType, con); - switch(res) - { - case CBSDKRESULT_WARNOPEN: - mexPrintf("Real-time interface already initialized\n"); - return; - default: - PrintErrorSDK(res, "cbSdkOpen()"); - break; - } - - // Return the actual openned connection - res = cbSdkGetType(nInstance, &conType, &instType); - PrintErrorSDK(res, "cbSdkGetType()"); - res = cbSdkGetVersion(nInstance, &ver); - PrintErrorSDK(res, "cbSdkGetVersion()"); - - if (conType < 0 || conType > CBSDKCONNECTION_CLOSED) - conType = CBSDKCONNECTION_COUNT; - if (instType < 0 || instType > CBSDKINSTRUMENT_COUNT) - instType = CBSDKINSTRUMENT_COUNT; - if (nlhs > 0) - { - plhs[0] = mxCreateDoubleScalar(conType); - if (nlhs > 1) - plhs[1] = mxCreateDoubleScalar(instType); - } - - char strConnection[CBSDKCONNECTION_COUNT + 1][8] = {"Default", "Central", "Udp", "Closed", "Unknown"}; - char strInstrument[CBSDKINSTRUMENT_COUNT + 1][13] = {"NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"}; - mexPrintf("%s real-time interface to %s (%d.%02d.%02d.%02d hwlib %d.%02d) successfully initialized\n", strConnection[conType], strInstrument[instType], ver.nspmajor, ver.nspminor, ver.nsprelease, ver.nspbeta, ver.nspmajorp, ver.nspminorp); -} - - -// Author & Date: Kirk Korver 13 Jun 2005 -/** -* Processing to do with the command "close". -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnClose( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - - int nFirstParam = 1; - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_CLOSE, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_CLOSE, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CLOSE, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_CLOSE, true, "Last parameter requires value"); - } - - cbSdkResult res = cbSdkClose(nInstance); - switch(res) - { - case CBSDKRESULT_WARNCLOSED: - mexPrintf("Real-time interface already closed\n"); - break; - default: - PrintErrorSDK(res, "cbSdkClose()"); - break; - } -} - -// Author & Date: Kirk Korver 13 Jun 2005 -/** -* Processing to do with the command "time". -* -* \n IN MATLAB => time = cbmex('time') -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnTime( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - - int nFirstParam = 1; - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - bool bSamples = false; - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_TIME, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else if (_strcmpi(cmdstr, "samples") == 0) - { - bSamples = true; - } - else { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_TIME, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TIME, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_TIME, true, "Last parameter requires value"); - } - - PROCTIME cbtime; - cbSdkResult res = cbSdkGetTime(nInstance, &cbtime); - PrintErrorSDK(res, "cbSdkGetTime()"); - - plhs[0] = mxCreateDoubleScalar(bSamples ? cbtime : cbSdk_SECONDS_PER_TICK * cbtime); -} - -// Author & Date: Kirk Korver 13 Jun 2005 -/** -* Processing to do with the command "chanlabel". Set/Get channel label. -* -* \n IN MATLAB => -* \n cbmex( 'chanlabel', [channels], [new_label_cell_array]) -* \n label_cell_array = cbmex( 'chanlabel', channels_vector, [new_label_cell_array]) -* \n label_cell_array = cbmex( 'chanlabel', channels_vector) -* \n label_cell_array = cbmex( 'chanlabel') -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ - -void OnChanLabel( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - - int nFirstParam = 1; - int idxNewLabels = 0; - - uint16_t channels[cbMAXCHANS]; - uint32_t count = cbMAXCHANS; - if (nrhs > 1 && mxIsNumeric(prhs[1])) - { - nFirstParam++; // skip the optional - count = (uint32_t)mxGetNumberOfElements(prhs[1]); - if (nrhs > 2) - { - bool bIsString = (mxGetClassID(prhs[2]) == mxCHAR_CLASS); - bool bIsParamNext = true; - if (nrhs > 3) - bIsParamNext = (mxGetClassID(prhs[3]) == mxCHAR_CLASS); - if (bIsParamNext) - { - if (count == 1 && bIsString) - { - // Special case for cbmex('chanlabel', chan, 'newlabel') - } - else if (mxGetClassID(prhs[2]) != mxCELL_CLASS) - { - PrintHelp(CBMEX_FUNCTION_CHANLABEL, true, "Wrong format for new_label_cell_array"); - } - else if (count != mxGetNumberOfElements(prhs[2])) - { - PrintHelp(CBMEX_FUNCTION_CHANLABEL, true, "Number of channels and number of new labels do not match"); - } - idxNewLabels = 2; - nFirstParam++; // skip the optional - } - } - if (count > cbMAXCHANS) - mexErrMsgTxt("Maximum of 156 channels is possible"); - for (uint32_t i = 0; i < count; ++i) - { - channels[i] = (uint16_t)(*(mxGetPr(prhs[1]) + i)); - if (channels[i] == 0|| channels[i] > cbMAXCHANS) - mexErrMsgTxt("Invalid channel number specified"); - } - } - else - { - for (uint32_t i = 0; i < count; ++i) - { - channels[i] = i + 1; - } - } - - if (nrhs > 1 && mxGetClassID(prhs[1]) == mxCELL_CLASS && mxGetNumberOfElements(prhs[1]) == cbMAXCHANS) - { - // Special case for cbmex('chanlabel', new_label_cell_array) - // with labels specified for ALL the channels - nFirstParam++; // skip the optional - idxNewLabels = 1; - } - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_CHANLABEL, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_CHANLABEL, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CHANLABEL, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_CHANLABEL, true, "Last parameter requires value"); - } - - // if output specifically requested, or if no new channel labels specified - // build the cell structure to get previous labels - if (nlhs > 0 || idxNewLabels == 0) - { - mxArray *pca = mxCreateCellMatrix(count, 7); - for (UINT ch = 1; ch <= count; ch++) - { - char label[32]; - uint32_t bValid[cbMAXUNITS + 1]; - cbSdkResult res = cbSdkGetChannelLabel(nInstance, channels[ch - 1], bValid, label, NULL, NULL); - PrintErrorSDK(res, "cbSdkGetChannelLabel()"); - - mxSetCell(pca, ch - 1, mxCreateString(label)); - if (ch <= cbNUM_ANALOG_CHANS) - { - for (int i = 0; i < 6; ++i) - { - mxSetCell(pca, ch - 1 + count * (i + 1), mxCreateDoubleScalar(bValid[i])); - } - } - else if ( (ch == MAX_CHANS_DIGITAL_IN) || (ch == MAX_CHANS_SERIAL) ) - { - mxSetCell(pca, ch - 1 + count, mxCreateDoubleScalar(bValid[0])); - } - } - plhs[0] = pca; - } - - // Now set new labels - if (idxNewLabels > 0) - { - char label[128]; - // Handle the case for single channel label assignment - if (count == 1 && mxGetClassID(prhs[idxNewLabels]) == mxCHAR_CLASS) - { - if (mxGetString(prhs[idxNewLabels], label, 16)) - mexErrMsgTxt("Invalid channel label"); - cbSdkResult res = cbSdkSetChannelLabel(nInstance, channels[0], label, 0, NULL); - PrintErrorSDK(res, "cbSdkSetChannelLabel()"); - } - else - { - for (uint32_t i = 0; i < count; ++i) - { - const mxArray * cell_element_ptr = mxGetCell(prhs[idxNewLabels], i); - if (mxGetClassID(cell_element_ptr) != mxCHAR_CLASS || mxGetString(cell_element_ptr, label, 16)) - { - sprintf(label, "Invalid label at %d", i + 1); - mexErrMsgTxt(label); - } - cbSdkResult res = cbSdkSetChannelLabel(nInstance, channels[i], label, 0, NULL); - PrintErrorSDK(res, "cbSdkSetChannelLabel()"); - } - } - } -} - -// Author & Date: Kirk Korver 13 Jun 2005 -/** -* Processing to do with the command "trialconfig". -* -* \n IN MATLAB => -* \n [ active, [ begchan begmask begval endchan endmask endval double waveform continuous event ] ] = cbmex('trialconfig') -* \n cbmex('trialconfig', bActive ) -* \n cbmex('trialconfig', bActive, [ begchan begmask begval endchan endmask endval ] ) -* \n cbmex('trialconfig', bActive, [ begchan begmask begval endchan endmask endval ], 'double') -* \n cbmex('trialconfig', bActive, [ begchan begmask begval endchan endmask endval ], 'double', 'waveform', 400) -* \n cbmex('trialconfig', bActive, [ begchan begmask begval endchan endmask endval ], 'double', 'nocontinuous') -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnTrialConfig( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 2; - // check the number of input arguments - if (nrhs < 2) - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "At least one input is required"); - - uint32_t bActive; - // Process first input argument if available - bActive = ((uint32_t)mxGetScalar(prhs[1]) > 0); - - cbSdkResult res; - - uint16_t uBegChan = 0; - uint32_t uBegMask = 0; - uint32_t uBegVal = 0; - uint16_t uEndChan = 0; - uint32_t uEndMask = 0; - uint32_t uEndVal = 0; - bool bDouble = false; - bool bAbsolute = false; - uint32_t uWaveforms = 0; - uint32_t uConts = cbSdk_CONTINUOUS_DATA_SAMPLES; - uint32_t uEvents = cbSdk_EVENT_DATA_SAMPLES; - uint32_t uComments = 0; - uint32_t uTrackings = 0; - uint32_t bWithinTrial = false; - - res = cbSdkGetTrialConfig(nInstance, &bWithinTrial, &uBegChan, &uBegMask, &uBegVal, &uEndChan, &uEndMask, &uEndVal, - &bDouble, &uWaveforms, &uConts, &uEvents, &uComments, &uTrackings); - - if (nFirstParam < nrhs) - { - if (mxIsNumeric(prhs[nFirstParam])) - { - // check for proper data structure - if (mxGetNumberOfElements(prhs[nFirstParam]) != 6) - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "Invalid config_vector_in"); - - // Trim them to 8-bit and 16-bit values - double * pcfgvals = mxGetPr(prhs[nFirstParam]); - uBegChan = ((uint32_t)(*(pcfgvals+0))) & 0x00FF; - uBegMask = ((uint32_t)(*(pcfgvals+1))) & 0xFFFF; - uBegVal = ((uint32_t)(*(pcfgvals+2))) & 0xFFFF; - uEndChan = ((uint32_t)(*(pcfgvals+3))) & 0x00FF; - uEndMask = ((uint32_t)(*(pcfgvals+4))) & 0xFFFF; - uEndVal = ((uint32_t)(*(pcfgvals+5))) & 0xFFFF; - - nFirstParam++; // skip the optional - } - } - - enum - { - PARAM_NONE, - PARAM_WAVEFORM, - PARAM_CONTINUOUS, - PARAM_EVENT, - PARAM_COMMENT, - PARAM_TRACKING, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, errstr); - } - if (_strcmpi(cmdstr, "double") == 0) - { - bDouble = true; - } - else if (_strcmpi(cmdstr, "absolute") == 0) - { - bAbsolute = true; - } - else if (_strcmpi(cmdstr, "nocontinuous") == 0) - { - uConts = 0; - } - else if (_strcmpi(cmdstr, "noevent") == 0) - { - uEvents = 0; - } - else if (_strcmpi(cmdstr, "waveform") == 0) - { - param = PARAM_WAVEFORM; - } - else if (_strcmpi(cmdstr, "continuous") == 0) - { - param = PARAM_CONTINUOUS; - } - else if (_strcmpi(cmdstr, "event") == 0) - { - param = PARAM_EVENT; - } - else if (_strcmpi(cmdstr, "comment") == 0) - { - param = PARAM_COMMENT; - } - else if (_strcmpi(cmdstr, "tracking") == 0) - { - param = PARAM_TRACKING; - } - else if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_WAVEFORM: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "Invalid waveform count"); - uWaveforms = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_CONTINUOUS: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "Invalid continuous count"); - uConts = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_EVENT: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "Invalid event count"); - uEvents = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_COMMENT: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "Invalid comment count"); - uComments = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_TRACKING: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "Invalid tracking count"); - uTrackings = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - }// end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_TRIALCONFIG, true, "Last parameter requires value"); - } - - res = cbSdkSetTrialConfig(nInstance, bActive, uBegChan, uBegMask, uBegVal, uEndChan, uEndMask, uEndVal, - bDouble, uWaveforms, uConts, uEvents, uComments, uTrackings, bAbsolute); - PrintErrorSDK(res, "cbSdkSetTrialConfig()"); - - // process first output argument if available - if (nlhs > 0) - plhs[0] = mxCreateDoubleScalar( (double)bWithinTrial ); - - // process second output argument if available - if (nlhs > 1) - { - plhs[1] = mxCreateDoubleMatrix(12, 1, mxREAL); - double *pcfgvals = mxGetPr(plhs[1]); - *(pcfgvals+0) = uBegChan; - *(pcfgvals+1) = uBegMask; - *(pcfgvals+2) = uBegVal; - *(pcfgvals+3) = uEndChan; - *(pcfgvals+4) = uEndMask; - *(pcfgvals+5) = uEndVal; - *(pcfgvals+6) = bDouble; - *(pcfgvals+7) = uWaveforms; - *(pcfgvals+8) = uConts; - *(pcfgvals+9) = uEvents; - *(pcfgvals+10) = uComments; - *(pcfgvals+11) = uTrackings; - } -} - -// Author & Date: Kirk Korver 13 Jun 2005 -/** -* Processing to do with the command "trialdata". -* -* \n IN MATLAB => -* \n timestamps_cell_array = cbmex('trialdata') -* \n timestamps_cell_array = cbmex('trialdata', 1) -* \n [timestamps_cell_array, time, continuous_cell_array] = cbmex('trialdata') -* \n [timestamps_cell_array, time, continuous_cell_array] = cbmex('trialdata', 1) -* \n [time, continuous_cell_array] = cbmex('trialdata') -* \n [time, continuous_cell_array] = cbmex('trialdata', 1) -* \n -* \n Inputs in Matlab: -* \n the 2nd parameter == 0 means to NOT flush the buffer -* \n == 1 means to flush the buffer -* \n -* \n Outputs in Matlab: -* \n timestamps_cell_array = -* \n for neural channel rows 1 - 144, -* \n { 'label' u0ts u1ts u2ts u3ts u4ts u5ts [waveform]} where u0ts = unit0 timestamps, etc. -* \n for channels 151 and 152, the digital channels, each row is defined as -* \n { 'label' timestamps values [empty] [empty] [empty] [empty] } -* \n -* \n continuous_cell_array = -* \n for each channel with continuous recordings -* \n { channel_number, sample rate, data_points[] } -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments* -*/ - -void OnTrialData( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 1; - bool bFlushBuffer = false; - cbSdkResult res; - - // make sure there is at least one output argument - if (nlhs > 3) - PrintHelp(CBMEX_FUNCTION_TRIALDATA, true, "Too many outputs requested"); - - if (nFirstParam < nrhs) - { - if (mxIsNumeric(prhs[nFirstParam])) - { - // check for proper data structure - if (mxGetNumberOfElements(prhs[nFirstParam]) != 1) - PrintHelp(CBMEX_FUNCTION_TRIALDATA, true, "Invalid active parameter"); - - // set restartTrialFlag - if (mxGetScalar(prhs[1]) != 0.0) - bFlushBuffer = true; - - nFirstParam++; // skip the optional - } - } - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_TRIALDATA, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_TRIALDATA, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALDATA, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_TRIALDATA, true, "Last parameter requires value"); - } - - cbSdkTrialEvent trialevent; - trialevent.count = 0; - cbSdkTrialCont trialcont; - trialcont.count = 0; - - // 1 - Get how many samples are waiting - - bool bTrialDouble = false; - uint32_t uEvents, uConts; - - res = cbSdkGetTrialConfig(nInstance, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &bTrialDouble, NULL, &uConts, &uEvents); - - res = cbSdkInitTrialData(nInstance, bFlushBuffer, (nlhs == 2 || !uEvents) ? NULL : &trialevent, (nlhs >= 2 && uConts) ? &trialcont : NULL, NULL, NULL); - PrintErrorSDK(res, "cbSdkInitTrialData()"); - - - // 2 - Allocate buffers for samples - - // Does the user want event data? - if (nlhs != 2) - { - // For back-ward compatibility all channels are returned no matter if empty or not - mxArray *pca = mxCreateCellMatrix(152, 7); - plhs[0] = pca; - for (uint32_t channel = 0; channel < 152; channel++) - { - char label[32] = {0}; - cbSdkGetChannelLabel(nInstance, channel + 1, NULL, label, NULL, NULL); - mxSetCell(pca, channel, mxCreateString(label)); - } - - for (uint32_t channel = 0; channel < trialevent.count; channel++) - { - uint16_t ch = trialevent.chan[channel]; // Actual channel number - // Fill timestamps for non-empty channels - for(UINT u = 0; u <= cbMAXUNITS; u++) - { - trialevent.timestamps[channel][u] = NULL; - uint32_t num_samples = trialevent.num_samples[channel][u]; - if (num_samples) - { - mxArray *mxa; - if (bTrialDouble) - mxa = mxCreateDoubleMatrix(num_samples, 1, mxREAL); - else - mxa = mxCreateNumericMatrix(num_samples, 1, mxUINT32_CLASS, mxREAL); - trialevent.timestamps[channel][u] = mxGetData(mxa); - mxSetCell(pca, (ch - 1) + 152 * (u + 1), mxa); - } - } - // Fill values for non-empty digital or serial channels - if (ch == MAX_CHANS_DIGITAL_IN || ch == MAX_CHANS_SERIAL) - { - uint32_t num_samples = trialevent.num_samples[channel][0]; - if (num_samples) - { - mxArray *mxa; - if (bTrialDouble) - mxa = mxCreateDoubleMatrix(num_samples, 1, mxREAL); - else - mxa = mxCreateNumericMatrix(num_samples, 1, mxUINT16_CLASS, mxREAL); - trialevent.waveforms[channel] = mxGetData(mxa); - mxSetCell(pca, (ch - 1) + 152 * (1 + 1), mxa); - } - } - } - } - - // Does the user want continuous data? - if (nlhs >= 2) - { - // Output format for continuous data: - // - // Each row contains: - // - // [channel] [sample rate] [n x 1 data array] - - mxArray *pca = mxCreateCellMatrix(trialcont.count, 3); - plhs[nlhs - 1] = pca; - for (uint32_t channel = 0; channel < trialcont.count; channel++) - { - trialcont.samples[channel] = NULL; - uint32_t num_samples = trialcont.num_samples[channel]; - mxSetCell(pca, channel, mxCreateDoubleScalar(trialcont.chan[channel])); - mxSetCell(pca, channel + trialcont.count, mxCreateDoubleScalar(trialcont.sample_rates[channel])); - mxArray *mxa; - if (bTrialDouble) - mxa = mxCreateDoubleMatrix(num_samples, 1, mxREAL); - else - mxa = mxCreateNumericMatrix(num_samples, 1, mxINT16_CLASS, mxREAL); - trialcont.samples[channel] = mxGetData(mxa); - mxSetCell(pca, channel + trialcont.count * 2, mxa); - } - } - - // 3 - Now get buffered data - - res = cbSdkGetTrialData(nInstance, bFlushBuffer, (nlhs == 2 || !uEvents) ? NULL : &trialevent, (nlhs >= 2 && uConts) ? &trialcont : NULL, NULL, NULL); - PrintErrorSDK(res, "cbSdkGetTrialData()"); - - // Does the user want event data? - if (nlhs != 2) - { - // Buffers are already filled - } - - // Does the user want continuous data? - if (nlhs >= 2) - { - // Buffers are already filled - plhs[nlhs - 2] = mxCreateDoubleScalar(cbSdk_SECONDS_PER_TICK * trialcont.time); - } -} - -// Author & Date: Ehsan Azar 26 Oct 2011 -/** -* Processing to do with the command "trialcomment". -* -* \n IN MATLAB => -* \n [comments_cell_array ts] = cbmex('trialcomment') -* \n [comments_cell_array ts] = cbmex('trialcomment', 1) -* \n -* \n Inputs: -* \n the 2nd parameter == 0 means to NOT flush the buffer -* \n == 1 means to flush the buffer -* \n -* \n Outputs: -* \n comments_cell_array: is an array of the variable-sized comments, each row is a comment -* \n ts: is the timestamps -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ - -void OnTrialComment( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 1; - bool bFlushBuffer = false; - cbSdkResult res; - - // make sure there is at least one output argument - if (nlhs > 4) - PrintHelp(CBMEX_FUNCTION_TRIALCOMMENT, true, "Too many outputs requested"); - - if (nFirstParam < nrhs) - { - if (mxIsNumeric(prhs[nFirstParam])) - { - // check for proper data structure - if (mxGetNumberOfElements(prhs[nFirstParam]) != 1) - PrintHelp(CBMEX_FUNCTION_TRIALCOMMENT, true, "Invalid active parameter"); - - // set restartTrialFlag - if (mxGetScalar(prhs[1]) != 0.0) - bFlushBuffer = true; - - nFirstParam++; // skip the optional - } - } - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_TRIALCOMMENT, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_TRIALCOMMENT, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALCOMMENT, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_TRIALCOMMENT, true, "Last parameter requires value"); - } - - cbSdkTrialComment trialcomment; - - bool bTrialDouble = false; - res = cbSdkGetTrialConfig(nInstance, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - &bTrialDouble); - - // 1 - Get how many samples are waiting - - res = cbSdkInitTrialData(nInstance, bFlushBuffer, NULL, NULL, &trialcomment, NULL); - PrintErrorSDK(res, "cbSdkInitTrialData()"); - - trialcomment.comments = NULL; - // 2 - Allocate buffers - { - mxArray *pca = mxCreateCellMatrix(trialcomment.num_samples, 1); - plhs[0] = pca; - if (trialcomment.num_samples) - { - trialcomment.comments = (uint8_t * *)mxMalloc(trialcomment.num_samples * sizeof(uint8_t *)); - for (int i = 0; i < trialcomment.num_samples; ++i) - { - const mwSize dims[2] = {1, cbMAX_COMMENT + 1}; - mxArray *mxa = mxCreateCharArray(2, dims); - mxSetCell(pca, i, mxa); - trialcomment.comments[i] = (uint8_t *)mxGetData(mxa); - } - } - } - trialcomment.timestamps = NULL; - if (nlhs > 1) - { - mxArray *mxa; - if (bTrialDouble) - mxa = mxCreateDoubleMatrix(trialcomment.num_samples, 1, mxREAL); - else - mxa = mxCreateNumericMatrix(trialcomment.num_samples, 1, mxUINT32_CLASS, mxREAL); - plhs[1] = mxa; - trialcomment.timestamps = mxGetData(mxa); - } - trialcomment.rgbas = NULL; - if (nlhs > 2) - { - mxArray *mxa; - mxa = mxCreateNumericMatrix(trialcomment.num_samples, 1, mxUINT32_CLASS, mxREAL); - plhs[2] = mxa; - trialcomment.rgbas = (uint32_t *)mxGetData(mxa); - } - trialcomment.charsets = NULL; - if (nlhs > 3) - { - mxArray *mxa; - mxa = mxCreateNumericMatrix(trialcomment.num_samples, 1, mxUINT8_CLASS, mxREAL); - plhs[3] = mxa; - trialcomment.charsets = (uint8_t *)mxGetData(mxa); - } - - // 3 - Now get buffered data - - res = cbSdkGetTrialData(nInstance, bFlushBuffer, NULL, NULL, &trialcomment, NULL); - PrintErrorSDK(res, "cbSdkGetTrialData()"); - - // NSP returns strings as char, but Matlab wants wide characters. The following function - // converts the char string to wchar_t string to return to Matlab - wchar_t szTempWcString[cbMAX_COMMENT]; - for (unsigned int i = 0; i < trialcomment.num_samples; ++i) - { - memset(szTempWcString, 0, sizeof(szTempWcString)); - for (unsigned int j = 0; j < strlen((char*)trialcomment.comments[i]); ++j) - { - szTempWcString[j] = trialcomment.comments[i][j]; - } - wcscpy((wchar_t*)trialcomment.comments[i], szTempWcString); - } - - // free memory - if (trialcomment.comments) - mxFree(trialcomment.comments); -} - -// Author & Date: Ehsan Azar 26 Oct 2011 -/** -* Processing to do with the command "trialtracking". -* -* \n IN MATLAB => -* \n tracking_cell_array = cbmex('trialtracking') -* \n tracking_cell_array = cbmex('trialtracking', 1) -* \n -* \n Inputs: -* \n the 2nd parameter == 0 means to NOT flush the buffer -* \n == 1 means to flush the buffer -* \n -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnTrialTracking( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 1; - bool bFlushBuffer = false; - cbSdkResult res; - - if (nlhs > 1) - PrintHelp(CBMEX_FUNCTION_TRIALTRACKING, true, "Too many outputs requested"); - - if (nFirstParam < nrhs) - { - if (mxIsNumeric(prhs[nFirstParam])) - { - // check for proper data structure - if (mxGetNumberOfElements(prhs[nFirstParam]) != 1) - PrintHelp(CBMEX_FUNCTION_TRIALTRACKING, true, "Invalid active parameter"); - - // set restartTrialFlag - if (mxGetScalar(prhs[1]) != 0.0) - bFlushBuffer = true; - - nFirstParam++; // skip the optional - } - } - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_TRIALTRACKING, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_TRIALTRACKING, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_TRIALTRACKING, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_TRIALTRACKING, true, "Last parameter requires value"); - } - - cbSdkTrialTracking trialtracking; - - // 1 - Get how many samples are waiting - - res = cbSdkInitTrialData(nInstance, bFlushBuffer, NULL, NULL, NULL, &trialtracking); - PrintErrorSDK(res, "cbSdkInitTrialData()"); - - bool bTrialDouble = false; - res = cbSdkGetTrialConfig(nInstance, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - &bTrialDouble); - - // 2 - Allocate buffers - mxArray *pca_rb[cbMAXTRACKOBJ] = {NULL}; - mxArray *pca = mxCreateCellMatrix(trialtracking.count, 6); - plhs[0] = pca; - for (int i = 0; i < trialtracking.count; ++i) - { - mxSetCell(pca, i * 6 + 0, mxCreateString((char *)trialtracking.names[i])); - mxArray *mxa; - mxa = mxCreateNumericMatrix(3, 1, mxUINT16_CLASS, mxREAL); - mxSetCell(pca, i * 6 + 1, mxa); - uint16_t * pvals = (uint16_t *)mxGetData(mxa); - *(pvals + 0) = trialtracking.types[i]; - *(pvals + 1) = trialtracking.ids[i]; - *(pvals + 2) = trialtracking.max_point_counts[i]; - if (bTrialDouble) - mxa = mxCreateDoubleMatrix(trialtracking.num_samples[i], 1, mxREAL); - else - mxa = mxCreateNumericMatrix(trialtracking.num_samples[i], 1, mxUINT32_CLASS, mxREAL); - mxSetCell(pca, i * 6 + 2, mxa); - trialtracking.timestamps[i] = mxGetData(mxa); - - mxa = mxCreateNumericMatrix(trialtracking.num_samples[i], 1, mxUINT32_CLASS, mxREAL); - mxSetCell(pca, i * 6 + 3, mxa); - trialtracking.synch_timestamps[i] = (uint32_t *)mxGetData(mxa); - - mxa = mxCreateNumericMatrix(trialtracking.num_samples[i], 1, mxUINT32_CLASS, mxREAL); - mxSetCell(pca, i * 5 + 4, mxa); - trialtracking.synch_frame_numbers[i]= (uint32_t *)mxGetData(mxa); - - trialtracking.point_counts[i] = (uint16_t *)mxMalloc(trialtracking.num_samples[i] * sizeof(uint16_t)); - - bool bWordData = false; // if data is of word-length - int dim_count = 2; // number of dimensionf for each point - switch(trialtracking.types[i]) - { - case cbTRACKOBJ_TYPE_2DMARKERS: - case cbTRACKOBJ_TYPE_2DBLOB: - case cbTRACKOBJ_TYPE_2DBOUNDARY: - dim_count = 2; - break; - case cbTRACKOBJ_TYPE_1DSIZE: - bWordData = true; - dim_count = 1; - break; - default: - dim_count = 3; - break; - } - - if (bWordData) - trialtracking.coords[i] = (void * *)mxMalloc(trialtracking.num_samples[i] * sizeof(uint32_t *)); - else - trialtracking.coords[i] = (void * *)mxMalloc(trialtracking.num_samples[i] * sizeof(uint16_t *)); - - // Rigid-body cell array - pca_rb[i] = mxCreateCellMatrix(trialtracking.num_samples[i], 1); - mxSetCell(pca, i * 5 + 5, pca_rb[i]); - - // We allocate for the maximum number of points, later we reduce dimension - for (int j = 0; j < trialtracking.num_samples[i]; ++j) - { - if (bWordData) - { - mxa = mxCreateNumericMatrix(trialtracking.max_point_counts[i], dim_count, mxUINT32_CLASS, mxREAL); - trialtracking.coords[i][j] = (uint32_t *)mxGetData(mxa); - } - else - { - mxa = mxCreateNumericMatrix(trialtracking.max_point_counts[i], dim_count, mxUINT16_CLASS, mxREAL); - trialtracking.coords[i][j] = (uint16_t *)mxGetData(mxa); - } - mxSetCell(pca_rb[i], j, mxa); - } //end for (int j - } //end for (int i - - // 3 - Now get buffered data - res = cbSdkGetTrialData(nInstance, bFlushBuffer, NULL, NULL, NULL, &trialtracking); - PrintErrorSDK(res, "cbSdkGetTrialData()"); - - // Reduce dimensions if needed, - // and free memory - for (int i = 0; i < trialtracking.count; ++i) - { - for (int j = 0; j < trialtracking.num_samples[i]; ++j) - { - mxArray *mxa = mxGetCell(pca_rb[i], j); - mxSetM(mxa, trialtracking.point_counts[i][j]); - } - mxFree(trialtracking.point_counts[i]); - mxFree(trialtracking.coords[i]); - } -} - -// Author & Date: Kirk Korver 13 Jun 2005 -/** -* Processing to do with the command "fileconfig". -* -* \n In MATLAB use => -* \n cbmex('fileconfig', filename, comments, 1) to start recording -* \n -or- -* \n cbmex('fileconfig', filename, comments, 0) to stop recording -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnFileConfig( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - bool bInstanceFound = false; - int nFirstParam = 4; - bool bGetFileInfo = false; - cbSdkResult res; - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - PARAM_OPTION, - } param = PARAM_NONE; - - // Do a quick look at the options just to find the instance if specified - for (int i = nFirstParam; i < nrhs; ++i) - { - - if (param == PARAM_NONE) - { - - char cmdstr[255]; - if (mxGetString(prhs[i], cmdstr, 255)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else if (_strcmpi(cmdstr, "option") == 0) - { - param = PARAM_OPTION; - } - else - { - param = PARAM_NONE; // Just something valid but instance - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - bInstanceFound = true; - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - - if ((nlhs > 0) && ((!bInstanceFound && (nrhs == 1)) || (bInstanceFound && (nrhs == 3)))) - { - if (nlhs > 3) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Too many outputs requested"); - bGetFileInfo = true; - } - else - { - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Too many outputs requested"); - } - - if (bGetFileInfo) - { - char filename[256] = {'\0'}; - char username[256] = {'\0'}; - bool bRecording = false; - res = cbSdkGetFileConfig(nInstance, filename, username, &bRecording); - PrintErrorSDK(res, "cbSdkGetFileConfig()"); - plhs[0] = mxCreateDoubleScalar(bRecording); - if (nlhs > 1) - plhs[1] = mxCreateString(filename); - if (nlhs > 2) - plhs[2] = mxCreateString(username); - // If no inputs given, nothing else to do - if ((!bInstanceFound && (nrhs == 1)) || (bInstanceFound && (nrhs == 3))) - return; - } - - if (nrhs < 4) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Too few inputs provided"); - - // declare the packet that will be sent - cbPKT_FILECFG fcpkt; - - // fill in the filename string - if (mxGetString(prhs[1], fcpkt.filename, 255)) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Invalid file name specified"); - - // fill in the comment string - if (mxGetString(prhs[2], fcpkt.comment, 255)) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Invalid comment specified"); - - if (!mxIsNumeric(prhs[3]) || mxGetNumberOfElements(prhs[3]) != 1) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Invalid action parameter"); - - uint32_t bStart = (uint32_t) mxGetScalar(prhs[3]); - uint32_t options = cbFILECFG_OPT_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else if (_strcmpi(cmdstr, "option") == 0) - { - param = PARAM_OPTION; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_OPTION: - { - char cmdstr[128]; - // check for proper data structure - if (mxGetClassID(prhs[i]) != mxCHAR_CLASS || mxGetString(prhs[i], cmdstr, 10)) - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Invalid option parameter"); - if (_strcmpi(cmdstr, "none") == 0) - { - options = cbFILECFG_OPT_NONE; - } - else if (_strcmpi(cmdstr, "close") == 0) - { - options = cbFILECFG_OPT_CLOSE; - } - else if (_strcmpi(cmdstr, "open") == 0) - { - options = cbFILECFG_OPT_OPEN; - } - else - { - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Invalid option parameter"); - } - } - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_FILECONFIG, true, "Last parameter requires value"); - } - - res = cbSdkSetFileConfig(nInstance, fcpkt.filename, fcpkt.comment, bStart, options); - PrintErrorSDK(res, "cbSdkSetFileConfig()"); -} - - -// Author & Date: Hyrum L. Sessions 1 Apr 2008 -/** -* Processing to do with the command "digitalout". -* -* \n In MATLAB use => -* \n cbmex('digitalout', channel, 1) to set digital out -* \n -or- -* \n cbmex('digitalout', channel, 0) to clear digital out -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnDigitalOut( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 3; - uint16_t nChannel = 0; - uint16_t nValue = 99; - bool bHasNewParams = false; - cbSdkResult res = CBSDKRESULT_SUCCESS; - cbPKT_CHANINFO chaninfo; - - if (nrhs < 2) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Too few inputs provided"); - if (nlhs > 1) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Too many outputs requested"); - - if (!mxIsNumeric(prhs[1]) || mxGetNumberOfElements(prhs[1]) != 1) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid channel parameter"); - nChannel = (uint16_t) mxGetScalar(prhs[1]); - - if (2 < nrhs) - { - if (mxGetNumberOfElements(prhs[2]) != 1) - nFirstParam = 2; - else - nValue = (uint16_t) mxGetScalar(prhs[2]); - - enum - { - PARAM_NONE, - PARAM_MONITOR, - PARAM_TRACK, - PARAM_TRIGGER, - PARAM_TIMED, - PARAM_OFFSET, - PARAM_VALUE, - PARAM_INPUT, - PARAM_INSTANCE, - } param = PARAM_NONE; - - bool bDisable = false; - int nIdxTimed = 0; - int nIdxMonitor = 0; - int nIdxTrigger = 0; - uint16_t nTrigChan = 0; - uint16_t nTrigValue = 0; - uint32_t nOffset = 0; - - // Do a quick look at the options just to find the instance if specified - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else if (_strcmpi(cmdstr, "disable") == 0) - { - param = PARAM_NONE; - } - else if (_strcmpi(cmdstr, "track") == 0) - { - param = PARAM_NONE; - } - else - { - param = PARAM_MONITOR; // Just something valid but instance - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Last parameter requires value"); - } - - // Get old config, so that we would only change the bits requested - res = cbSdkGetChannelConfig(nInstance, nChannel, &chaninfo); - PrintErrorSDK(res, "cbSdkGetChannelConfig()"); - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, errstr); - } - else if (_strcmpi(cmdstr, "monitor") == 0) - { - param = PARAM_MONITOR; - } - else if (_strcmpi(cmdstr, "track") == 0) - { - if (nIdxMonitor == 0) - mexErrMsgTxt("Cannot track with no monitor channel specified"); - chaninfo.doutopts |= cbDOUT_TRACK; - bHasNewParams = true; - } - else if (_strcmpi(cmdstr, "trigger") == 0) - { - param = PARAM_TRIGGER; - } - else if (_strcmpi(cmdstr, "value") == 0) - { - param = PARAM_VALUE; - } - else if (_strcmpi(cmdstr, "input") == 0) - { - param = PARAM_INPUT; - } - else if (_strcmpi(cmdstr, "disable") == 0) - { - bDisable = true; - if (i != 2) - mexErrMsgTxt("Cannot specify any other parameters with 'disable' command"); - chaninfo.doutopts &= ~(cbDOUT_FREQUENCY | cbDOUT_TRIGGERED | cbDOUT_TRACK); - chaninfo.moninst = 0; - chaninfo.monchan = 0; - bHasNewParams = true; - } - else if (_strcmpi(cmdstr, "timed") == 0) - { - param = PARAM_TIMED; - } - else if (_strcmpi(cmdstr, "offset") == 0) - { - param = PARAM_OFFSET; - } - else if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_MONITOR: - if (nIdxMonitor) - mexErrMsgTxt("Cannot specify multiple monitor commands"); - if (nIdxTimed) - mexErrMsgTxt("Cannot specify monitor and timed commands together"); - if (nIdxTrigger) - mexErrMsgTxt("Cannot specify monitor and trigger commands together"); - nIdxMonitor = i; - chaninfo.doutopts &= ~(cbDOUT_FREQUENCY | cbDOUT_TRIGGERED); - bHasNewParams = true; - break; - case PARAM_TRIGGER: - if (nIdxTrigger) - mexErrMsgTxt("Cannot specify multiple waveform triggers"); - if (nIdxMonitor) - mexErrMsgTxt("Cannot specify trigger and monitor commands together"); - if (nIdxTimed) - mexErrMsgTxt("Cannot specify trigger and timed commands together"); - nIdxTrigger = i; - chaninfo.doutopts &= ~(cbDOUT_FREQUENCY | cbDOUT_TRACK); - chaninfo.doutopts |= cbDOUT_TRIGGERED; - bHasNewParams = true; - break; - case PARAM_TIMED: - if (nIdxTimed) - mexErrMsgTxt("Cannot specify multiple timed commands"); - if (nIdxMonitor) - mexErrMsgTxt("Cannot specify timed and monitor commands together"); - if (nIdxTrigger) - mexErrMsgTxt("Cannot specify timed and trigger commands together"); - nIdxTimed = i; - chaninfo.doutopts &= ~(cbDOUT_TRACK | cbDOUT_TRIGGERED); - chaninfo.doutopts |= cbDOUT_FREQUENCY; - bHasNewParams = true; - break; - case PARAM_OFFSET: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid offset number"); - nOffset = (uint16_t)(*mxGetPr(prhs[i])); - bHasNewParams = true; - break; - case PARAM_VALUE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid value number"); - nTrigValue = (uint16_t)(*mxGetPr(prhs[i])); - bHasNewParams = true; - break; - case PARAM_INPUT: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid input number"); - nTrigChan = (uint16_t)(*mxGetPr(prhs[i])); - bHasNewParams = true; - break; - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Last parameter requires value"); - } - - // Ambiguous: (!nIdxTimed) == 0, or !(nIdxTimed == 0). I guess the former, otherwise one would use nIdxTimed != 0 - if (nIdxTrigger == 0 && nIdxMonitor == 0 && !nIdxTimed == 0 && !bDisable && !nValue) - { - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, - "No action specified\n" - "specify monitor, trigger, timed or disable"); - } - - // If any trigger is specified, parse it - if (nIdxTimed) - { - char cmdstr[128]; - - // check for proper data structure - if (mxGetClassID(prhs[nIdxTimed]) != mxCHAR_CLASS || mxGetString(prhs[nIdxTimed], cmdstr, 10)) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid timed parameters"); - - if (_strcmpi(cmdstr, "frequency") == 0) - { - if (nTrigChan == 0 || nTrigChan > 15000) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid timer frequency"); - if (nTrigValue == 0 || nTrigValue > 99) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid timer duty cycle"); - if (nOffset > 30000) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid timer offset"); - int nTotalSamples = 30000 / nTrigChan; - chaninfo.highsamples = std::max(1, (int)((float)nTotalSamples * (nTrigValue / 100.0))); - chaninfo.lowsamples = nTotalSamples - chaninfo.highsamples; - chaninfo.offset = nOffset; - } - if (_strcmpi(cmdstr, "samples") == 0) - { - if (nTrigChan == 0 || nTrigChan > 15000) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid high sample value"); - if (nTrigValue == 0 || nTrigValue > 15000) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid low sample value"); - chaninfo.lowsamples = nTrigValue; - chaninfo.highsamples = nTrigChan; - if (nTrigValue > 30000) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid timer offset"); - chaninfo.offset = nOffset; - } - } - - // If any trigger is specified, parse it - if (nIdxTrigger) - { - char cmdstr[128]; - // check for proper data structure - if (mxGetClassID(prhs[nIdxTrigger]) != mxCHAR_CLASS || mxGetString(prhs[nIdxTrigger], cmdstr, 10)) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid trigger"); - - if (_strcmpi(cmdstr, "dinrise") == 0) - { - chaninfo.trigtype = cbSdkWaveformTrigger_DINPREG; - if (nTrigChan == 0 || nTrigChan > 16) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Only bits 1-16 (first digital input) trigger is supported"); - chaninfo.trigchan = nTrigChan; - if (nOffset == 0 || nOffset > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid samples high"); - chaninfo.highsamples = nOffset; - } - else if (_strcmpi(cmdstr, "dinfall") == 0) - { - chaninfo.trigtype = cbSdkWaveformTrigger_DINPFEG; - if (nTrigChan == 0 || nTrigChan > 16) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Only bits 1-16 (first digital input) trigger is supported"); - chaninfo.trigchan = nTrigChan; - if (nOffset == 0 || nOffset > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid samples high"); - chaninfo.highsamples = nOffset; - } - else if (_strcmpi(cmdstr, "spike") == 0) - { - chaninfo.trigtype = cbSdkWaveformTrigger_SPIKEUNIT; - if (nTrigChan == 0 || nTrigChan > cbMAXCHANS) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid channel number"); - chaninfo.trigchan = nTrigChan; - if (nTrigValue == 0 || nTrigValue > cbMAXUNITS) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid unit number"); - chaninfo.trigval = nTrigValue; - if (nOffset == 0 || nOffset > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid samples high"); - chaninfo.highsamples = nOffset; - } - else if (_strcmpi(cmdstr, "roi") == 0) - { - chaninfo.trigtype = cbSdkWaveformTrigger_COMMENTCOLOR; - if (nTrigChan == 0 || nTrigChan > 4) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid ROI"); - chaninfo.trigval = nTrigChan; - if (nTrigValue == 0 || nTrigValue > 2) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid NeuroMotive Event"); - chaninfo.trigval += nTrigValue * 256; - if (nOffset == 0 || nOffset > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid samples high"); - chaninfo.highsamples = nOffset; - } - else if (_strcmpi(cmdstr, "cmtcolor") == 0) - { - chaninfo.trigtype = cbSdkWaveformTrigger_COMMENTCOLOR; - if (nTrigChan == 0 || nTrigChan > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid color"); - chaninfo.trigchan = nTrigChan; - if (nTrigValue == 0 || nTrigValue > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid color"); - chaninfo.trigval = nTrigValue; - if (nOffset == 0 || nOffset > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid samples high"); - chaninfo.highsamples = nOffset; - } - else if (_strcmpi(cmdstr, "softreset") == 0) - { - chaninfo.trigtype = cbSdkWaveformTrigger_SOFTRESET; - if (nOffset == 0 || nOffset > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid samples high"); - chaninfo.highsamples = nOffset; - } - else if (_strcmpi(cmdstr, "extension") == 0) - { - chaninfo.trigtype = cbSdkWaveformTrigger_EXTENSION; - if (nOffset == 0 || nOffset > 65535) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid samples high"); - chaninfo.highsamples = nOffset; - } - else - { - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid trigger"); - } - } - - if (nIdxMonitor) - { - char cmdstr[128]; - // check for proper data structure - chaninfo.moninst = cbGetChanInstrument(nTrigChan) - 1; - chaninfo.monchan = cbGetInstrumentLocalChannelNumber(nTrigChan); - chaninfo.doutopts &= ~cbDOUT_MONITOR_UNIT_ALL; - if (mxGetClassID(prhs[nIdxMonitor]) != mxCHAR_CLASS || mxGetString(prhs[nIdxMonitor], cmdstr, sizeof(cmdstr))) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid monitor"); - if (strstr(cmdstr, "unclass") != 0) - { - chaninfo.doutopts |= cbDOUT_MONITOR_UNIT0; - } - if (strstr(cmdstr, "unit1") != 0) - { - chaninfo.doutopts |= cbDOUT_MONITOR_UNIT1; - } - if (strstr(cmdstr, "unit2") != 0) - { - chaninfo.doutopts |= cbDOUT_MONITOR_UNIT2; - } - if (strstr(cmdstr, "unit3") != 0) - { - chaninfo.doutopts |= cbDOUT_MONITOR_UNIT3; - } - if (strstr(cmdstr, "unit4") != 0) - { - chaninfo.doutopts |= cbDOUT_MONITOR_UNIT4; - } - if (strstr(cmdstr, "unit5") != 0) - { - chaninfo.doutopts |= cbDOUT_MONITOR_UNIT5; - } - if (strstr(cmdstr, "all") != 0) - { - chaninfo.doutopts |= cbDOUT_MONITOR_UNIT_ALL; - } - if (0 == (chaninfo.doutopts & cbDOUT_MONITOR_UNIT_ALL)) - { - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid monitor unit"); - } - uint32_t nExpChan = cbGetExpandedChannelNumber(chaninfo.moninst, chaninfo.monchan); - if ((nExpChan == 0) || (nExpChan > cbNUM_ANALOG_CHANS)) - PrintHelp(CBMEX_FUNCTION_DIGITALOUT, true, "Invalid input channel number"); - } - } - - // if output specifically requested, or if no new configuration specified - // build the cell structure to get previous values - if (nlhs > 0 || ! bHasNewParams) - { - int count = 4; // number of parameters - char string[128]; - mxArray *pca = mxCreateCellMatrix(count, 2); - plhs[0] = pca; - - // describe the monitoring mode and parameters - mxSetCell(pca, 0, mxCreateString("monitor")); - uint32_t nExpChan = cbGetExpandedChannelNumber(chaninfo.moninst, chaninfo.monchan); - if ((0 == (cbDOUT_FREQUENCY & chaninfo.doutopts)) && (0 == (cbDOUT_TRIGGERED & chaninfo.doutopts)) && (0 != nExpChan)) - { - sprintf(string, "monitor chan %d", nExpChan); - if (cbDOUT_MONITOR_UNIT_ALL == (chaninfo.doutopts & cbDOUT_MONITOR_UNIT_ALL)) - strncat(string, " all units", sizeof(string) - strlen(string) - 1); - else - { - if (cbDOUT_MONITOR_UNIT0 == (chaninfo.doutopts & cbDOUT_MONITOR_UNIT0)) - strncat(string, " unclass ", sizeof(string) - strlen(string) - 1); - if (cbDOUT_MONITOR_UNIT1 == (chaninfo.doutopts & cbDOUT_MONITOR_UNIT1)) - strncat(string, " unit1 ", sizeof(string) - strlen(string) - 1); - if (cbDOUT_MONITOR_UNIT2 == (chaninfo.doutopts & cbDOUT_MONITOR_UNIT2)) - strncat(string, " unit2 ", sizeof(string) - strlen(string) - 1); - if (cbDOUT_MONITOR_UNIT3 == (chaninfo.doutopts & cbDOUT_MONITOR_UNIT3)) - strncat(string, " unit3 ", sizeof(string) - strlen(string) - 1); - if (cbDOUT_MONITOR_UNIT4 == (chaninfo.doutopts & cbDOUT_MONITOR_UNIT4)) - strncat(string, " unit4 ", sizeof(string) - strlen(string) - 1); - if (cbDOUT_MONITOR_UNIT5 == (chaninfo.doutopts & cbDOUT_MONITOR_UNIT5)) - strncat(string, " unit5 ", sizeof(string) - strlen(string) - 1); - } - } - else - strncpy(string, "disable", sizeof(string)); - mxSetCell(pca, count + 0, mxCreateString(string)); - - // describe if track recently selected channel is on of off - mxSetCell(pca, 1, mxCreateString("track")); - if (cbDOUT_TRACK == (chaninfo.doutopts & cbDOUT_TRACK)) - strncpy(string, "yes", sizeof(string)); - else - strncpy(string, "no", sizeof(string)); - mxSetCell(pca, count + 1, mxCreateString(string)); - - // describe the timed mode and parameters - mxSetCell(pca, 2, mxCreateString("timed")); - memset(string, 0, sizeof(string)); - if (cbDOUT_FREQUENCY == (chaninfo.doutopts & cbDOUT_FREQUENCY)) - sprintf(string, "Timed %dHz %d%% Duty Cycle - Samples High %d, Low %d, Offset %d", - (int)(30000.0 / ((float)chaninfo.highsamples + (float)chaninfo.lowsamples) + 0.5), - (int)((float)chaninfo.highsamples / (float)(chaninfo.highsamples + chaninfo.lowsamples) * 100.0), - chaninfo.highsamples, chaninfo.lowsamples, chaninfo.offset); - else - sprintf(string, "disable"); - mxSetCell(pca, count + 2, mxCreateString(string)); - - // describe the trigger mode and parameters - mxSetCell(pca, 3, mxCreateString("trigger")); - memset(string, 0, sizeof(string)); - if (cbDOUT_TRIGGERED == (chaninfo.doutopts & cbDOUT_TRIGGERED)) - { - switch(chaninfo.trigtype) - { - case cbSdkWaveformTrigger_DINPREG: - sprintf(string, "dinrise bit %d for %d samples", chaninfo.trigchan, chaninfo.highsamples); - break; - case cbSdkWaveformTrigger_DINPFEG: - sprintf(string, "dinfall bit %d for %d samples", chaninfo.trigchan, chaninfo.highsamples); - break; - case cbSdkWaveformTrigger_SPIKEUNIT: - sprintf(string, "spike on chan %d unit %d for %d samples", chaninfo.trigchan, chaninfo.trigval, chaninfo.highsamples); - break; - case cbSdkWaveformTrigger_COMMENTCOLOR: - sprintf(string, "roi ROI %d %s for %d samples", chaninfo.trigval & 0xFF, (1 == chaninfo.trigval / 256) ? "Enter" : "Exit", chaninfo.highsamples); - break; - case cbSdkWaveformTrigger_SOFTRESET: - sprintf(string, "softreset for %d samples", chaninfo.highsamples); - break; - case cbSdkWaveformTrigger_EXTENSION: - sprintf(string, "extension for %d samples", chaninfo.highsamples); - break; - default: - sprintf(string, "off"); - } - } - else - sprintf(string, "disable"); - mxSetCell(pca, count + 3, mxCreateString(string)); - } - - // if new configuration to send - if (bHasNewParams) - { - res = cbSdkSetChannelConfig(nInstance, nChannel, &chaninfo); - PrintErrorSDK(res, "cbSdkSetChannelConfig()"); - } - - // set the output value if specified - if (99 != nValue) - { - res = cbSdkSetDigitalOutput(nInstance, nChannel, nValue); - PrintErrorSDK(res, "cbSdkSetDigitalOutput()"); - } -} - -// Author & Date: Ehsan Azar 26 Oct 2011 -/** -* Processing to do with the command "analogout", to set specified waveform. -* -* \n In MATLAB use => -* \n cbmex('analogout', channel, [, [value]]) -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnAnalogOut( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 2; - - if (nrhs < 3) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Too few inputs provided"); - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Too many outputs requested"); - - enum - { - PARAM_NONE, - PARAM_PULSES, - PARAM_SEQUENCE, - PARAM_SINUSOID, - PARAM_OFFSET, - PARAM_REPEATS, - PARAM_TRIGGER, - PARAM_VALUE, - PARAM_INDEX, - PARAM_INPUT, - PARAM_MONITOR, - PARAM_TRACK, - PARAM_INSTANCE, - } param = PARAM_NONE; - - bool bUnitMv = false; // mv voltage units - bool bUnitMs = false; // ms interval units - bool bPulses = false; - bool bDisable = false; - double dOffset = 0; - uint16_t duration[cbMAX_WAVEFORM_PHASES]; - int16_t amplitude[cbMAX_WAVEFORM_PHASES]; - - cbSdkAoutMon mon; - memset(&mon, 0, sizeof(mon)); - - cbSdkWaveformData wf; - wf.type = cbSdkWaveform_NONE; - wf.repeats = 0; - wf.trig = cbSdkWaveformTrigger_NONE; - wf.trigChan = 0; - wf.trigValue = 0; - wf.trigNum = 0; - wf.offset = 0; - wf.duration = duration; - wf.amplitude = amplitude; - int nIdxWave = 0; - int nIdxTrigger = 0; - int nIdxMonitor = 0; - - // Channel number - uint16_t channel = (uint16_t)mxGetScalar(prhs[1]); - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - char cmdstr[128]; - if (param == PARAM_NONE) - { - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, errstr); - } - if (_strcmpi(cmdstr, "pulses") == 0) - { - param = PARAM_PULSES; - } - else if (_strcmpi(cmdstr, "sequence") == 0) - { - param = PARAM_SEQUENCE; - } - else if (_strcmpi(cmdstr, "sinusoid") == 0) - { - param = PARAM_SINUSOID; - } - else if (_strcmpi(cmdstr, "offset") == 0) - { - param = PARAM_OFFSET; - } - else if (_strcmpi(cmdstr, "repeats") == 0) - { - param = PARAM_REPEATS; - } - else if (_strcmpi(cmdstr, "trigger") == 0) - { - param = PARAM_TRIGGER; - } - else if (_strcmpi(cmdstr, "value") == 0) - { - param = PARAM_VALUE; - } - else if (_strcmpi(cmdstr, "index") == 0) - { - param = PARAM_INDEX; - } - else if (_strcmpi(cmdstr, "input") == 0) - { - param = PARAM_INPUT; - } - else if (_strcmpi(cmdstr, "monitor") == 0) - { - param = PARAM_MONITOR; - } - else if (_strcmpi(cmdstr, "track") == 0) - { - mon.bTrack = true; - if (nIdxMonitor == 0) - mexErrMsgTxt("Cannot track with no monitor channel specified"); - } - else if (_strcmpi(cmdstr, "disable") == 0) - { - bDisable = true; - if (i != 2) - mexErrMsgTxt("Cannot specify any other parameters with 'disable' command"); - } - else if (_strcmpi(cmdstr, "mv") == 0) - { - bUnitMv = true; - } - else if (_strcmpi(cmdstr, "ms") == 0) - { - bUnitMs = true; - } - else if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - mexErrMsgTxt("Invalid parameter"); - } - } - else - { - // parameters are checked here - switch (param) - { - case PARAM_PULSES: - if (nIdxWave) - mexErrMsgTxt("Cannot specify multiple waveform commands"); - if (nIdxMonitor) - mexErrMsgTxt("Cannot specify monitor and waveform commands together"); - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid pulses waveform"); - bPulses = true; - wf.type = cbSdkWaveform_PARAMETERS; - nIdxWave = i; - break; - case PARAM_SEQUENCE: - if (nIdxWave) - mexErrMsgTxt("Cannot specify multiple waveform commands"); - if (nIdxMonitor) - mexErrMsgTxt("Cannot specify monitor and waveform commands together"); - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid sequence waveform"); - wf.type = cbSdkWaveform_PARAMETERS; - nIdxWave = i; - break; - case PARAM_MONITOR: - if (nIdxMonitor) - mexErrMsgTxt("Cannot specify multiple monitor commands"); - if (nIdxWave) - mexErrMsgTxt("Cannot specify monitor and waveform commands together"); - nIdxMonitor = i; - break; - case PARAM_SINUSOID: - if (nIdxWave) - mexErrMsgTxt("Cannot specify multiple waveform commands"); - if (nIdxMonitor) - mexErrMsgTxt("Cannot specify monitor and waveform commands together"); - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid sinusoid waveform"); - wf.type = cbSdkWaveform_SINE; - nIdxWave = i; - break; - case PARAM_OFFSET: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid offset number"); - dOffset = *mxGetPr(prhs[i]); - break; - case PARAM_REPEATS: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid repeats number"); - wf.repeats = (uint32_t)(*mxGetPr(prhs[i])); - break; - case PARAM_VALUE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid value number"); - wf.trigValue = (uint16_t)(*mxGetPr(prhs[i])); - break; - case PARAM_INDEX: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid index number"); - wf.trigNum = (uint8_t)(*mxGetPr(prhs[i])); - break; - case PARAM_INPUT: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid input number"); - wf.trigChan = (uint16_t)(*mxGetPr(prhs[i])); - mon.chan = wf.trigChan; - break; - case PARAM_TRIGGER: - if (nIdxTrigger) - mexErrMsgTxt("Cannot specify multiple waveform triggers"); - if (nIdxMonitor) - mexErrMsgTxt("Cannot specify monitor and waveform commands together"); - nIdxTrigger = i; - break; - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Last parameter requires value"); - } - - if (nIdxWave == 0 && nIdxMonitor == 0 && !bDisable) - { - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, - "No action specified\n" - "specify waveform, monitoring or disable"); - } - - if (nIdxWave) - { - if (wf.type == cbSdkWaveform_PARAMETERS) - { - double dDuration[cbMAX_WAVEFORM_PHASES]; - double dAmplitude[cbMAX_WAVEFORM_PHASES]; - if (bPulses) - { - // check for proper data structure - if (mxGetClassID(prhs[nIdxWave]) != mxDOUBLE_CLASS || mxGetNumberOfElements(prhs[nIdxWave]) != 6) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid pulses waveform"); - - double *pcfgvals = mxGetPr(prhs[nIdxWave]); - double dPhase1Duration = *(pcfgvals+0); - double dPhase1Amplitude = *(pcfgvals+1); - double dInterPhaseDelay = *(pcfgvals+2); - double dPhase2Duration = *(pcfgvals+3); - double dPhase2Amplitude = *(pcfgvals+4); - double dInterPulseDelay = *(pcfgvals+5); - wf.phases = 4; - dDuration[0] = dPhase1Duration; - dDuration[1] = dInterPhaseDelay; - dDuration[2] = dPhase2Duration; - dDuration[3] = dInterPulseDelay; - dAmplitude[0] = dPhase1Amplitude; - dAmplitude[1] = 0; - dAmplitude[2] = dPhase2Amplitude; - dAmplitude[3] = 0; - } - else - { - uint32_t count = (uint32_t)mxGetNumberOfElements(prhs[nIdxWave]); - // check for proper data structure - if (mxGetClassID(prhs[nIdxWave]) != mxDOUBLE_CLASS || count < 2 || (count & 0x01)) - { - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid sequence waveform"); - } - if (count > (2 * cbMAX_WAVEFORM_PHASES)) - { - char cmdstr[256]; - sprintf(cmdstr, "Maximum of %u phases can be specified for each sequence", (uint32_t)cbMAX_WAVEFORM_PHASES); - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, cmdstr); - } - /// \todo use sequence to pump larger number of phases in NSP1.5 - wf.phases = count / 2; - double *pcfgvals = mxGetPr(prhs[nIdxWave]); - for (uint16_t i = 0; i < wf.phases; ++i) - { - dDuration[i] = *(pcfgvals + i * 2 + 0); - dAmplitude[i] = *(pcfgvals + i * 2 + 1); - } - } - - if (bUnitMv) - { - // FIXME: add to SDK - cbSCALING isScaleOut; - ::cbGetAoutCaps(channel, NULL, &isScaleOut, NULL, nInstance); - - int nAnaAmplitude = isScaleOut.anamax; - int nDigiAmplitude = isScaleOut.digmax; - for (uint16_t i = 0; i < wf.phases; ++i) - { - dAmplitude[i] = (dAmplitude[i] * nDigiAmplitude) / nAnaAmplitude; - if (dAmplitude[i] > MAX_int16_T) - dAmplitude[i] = MAX_int16_T; - else if (dAmplitude[i] < MIN_int16_T) - dAmplitude[i] = MIN_int16_T; - } - dOffset = (dOffset * nDigiAmplitude) / nAnaAmplitude; - } - for (uint16_t i = 0; i < wf.phases; ++i) - wf.amplitude[i] = (int16_t)dAmplitude[i]; - - if (bUnitMs) - { - for (uint16_t i = 0; i < wf.phases; ++i) - { - dDuration[i] *= 30; - if (dDuration[i] > MAX_uint16_T) - dDuration[i] = MAX_uint16_T; - } - } - for (uint16_t i = 0; i < wf.phases; ++i) - { - // Zero is, but let's not allow it with high level library, because it wastes NSP CPU cycles - if (dDuration[i] <= 0) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Negative or zero duration is not valid"); - wf.duration[i] = (uint16_t)dDuration[i]; - } - wf.offset = (int16_t)dOffset; - } - else if (wf.type == cbSdkWaveform_SINE) - { - if (mxGetClassID(prhs[nIdxWave]) != mxDOUBLE_CLASS || mxGetNumberOfElements(prhs[nIdxWave]) != 2) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid sinusoid waveform"); - - double *pcfgvals = mxGetPr(prhs[nIdxWave]); - wf.sineFrequency = (int32_t)(*(pcfgvals + 0)); - double dAmplitude = *(pcfgvals + 1); - if (bUnitMv) - { - // FIXME: add to SDK - cbSCALING isScaleOut; - ::cbGetAoutCaps(channel, NULL, &isScaleOut, NULL, nInstance); - - int nAnaAmplitude = isScaleOut.anamax; - int nDigiAmplitude = isScaleOut.digmax; - - dAmplitude = (dAmplitude * nDigiAmplitude) / nAnaAmplitude; - dOffset = (dOffset * nDigiAmplitude) / nAnaAmplitude; - } - wf.sineAmplitude = (int16_t)dAmplitude; - wf.offset = (int16_t)dOffset; - } - - // If any trigger is specified, parse it - if (nIdxTrigger) - { - char cmdstr[128]; - // check for proper data structure - if (mxGetClassID(prhs[nIdxTrigger]) != mxCHAR_CLASS || mxGetString(prhs[nIdxTrigger], cmdstr, 10)) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid trigger"); - - if (_strcmpi(cmdstr, "instant") == 0) - { - wf.trig = cbSdkWaveformTrigger_NONE; - } - else if (_strcmpi(cmdstr, "dinrise") == 0) - { - wf.trig = cbSdkWaveformTrigger_DINPREG; - if (wf.trigChan == 0 || wf.trigChan > 16) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Only bits 1-16 (first digital input) trigger is supported"); - } - else if (_strcmpi(cmdstr, "dinfall") == 0) - { - wf.trig = cbSdkWaveformTrigger_DINPFEG; - if (wf.trigChan == 0 || wf.trigChan > 16) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Only bits 1-16 (first digital input) trigger is supported"); - } - else if (_strcmpi(cmdstr, "spike") == 0) - { - wf.trig = cbSdkWaveformTrigger_SPIKEUNIT; - if (wf.trigChan == 0 || wf.trigChan > cbMAXCHANS) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid input channel number"); - } - else if (_strcmpi(cmdstr, "cmtcolor") == 0) - { - wf.trig = cbSdkWaveformTrigger_COMMENTCOLOR; - } - else if (_strcmpi(cmdstr, "softreset") == 0) - { - wf.trig = cbSdkWaveformTrigger_SOFTRESET; - } - else if (_strcmpi(cmdstr, "extension") == 0) - { - wf.trig = cbSdkWaveformTrigger_EXTENSION; - } - else if (_strcmpi(cmdstr, "off") == 0) - { - wf.trig = cbSdkWaveformTrigger_NONE; - if (wf.type != cbSdkWaveform_NONE) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Trigger off is incompatible with specifying a waveform"); - } - else - { - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid trigger"); - } - } - } - - if (nIdxMonitor) - { - char cmdstr[128]; - // check for proper data structure - if (mxGetClassID(prhs[nIdxMonitor]) != mxCHAR_CLASS || mxGetString(prhs[nIdxMonitor], cmdstr, 10)) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid monitor"); - if (_strcmpi(cmdstr, "spike") == 0) - { - mon.bSpike = true; - } - else if (_strcmpi(cmdstr, "continuous") == 0) - { - mon.bSpike = false; - } - else - { - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid monitor"); - } - if (mon.chan == 0 || mon.chan > cbMAXCHANS) - PrintHelp(CBMEX_FUNCTION_ANALOGOUT, true, "Invalid input channel number"); - } - - cbSdkResult res = cbSdkSetAnalogOutput(nInstance, channel, nIdxWave ? &wf : NULL, nIdxMonitor ? &mon : NULL); - PrintErrorSDK(res, "cbSdkSetAnalogOutput()"); -} - -// Author & Date: Ehsan Azar 11 March 2011 -/** -* Set a channel mask. -* -* \n In MATLAB use => -* \n cbmex('mask', channel, [bActive]) -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnMask( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 2; - uint32_t bActive = 1; - - if (nrhs < 2) - PrintHelp(CBMEX_FUNCTION_MASK, true, "Too few inputs provided"); - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_MASK, true, "Too many outputs requested"); - - if (!mxIsNumeric(prhs[1]) || mxGetNumberOfElements(prhs[1]) != 1) - PrintHelp(CBMEX_FUNCTION_MASK, true, "Invalid channel parameter"); - - uint16_t nChannel = (uint16_t) mxGetScalar(prhs[1]); - - if (nFirstParam < nrhs) - { - if (mxIsNumeric(prhs[nFirstParam])) - { - // check for proper data structure - if (mxGetNumberOfElements(prhs[nFirstParam]) != 1) - PrintHelp(CBMEX_FUNCTION_MASK, true, "Invalid active parameter"); - - bActive = (uint32_t)mxGetScalar(prhs[nFirstParam]); - - nFirstParam++; // skip the optional - } - } - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_MASK, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_MASK, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_MASK, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_MASK, true, "Last parameter requires value"); - } - - cbSdkResult res = cbSdkSetChannelMask(nInstance, nChannel, bActive); - PrintErrorSDK(res, "cbSdkSetChannelMask()"); -} - -// Author & Date: Ehsan Azar 25 Feb 2011 -/** -* Send a comment or custom event. -* -* \n In MATLAB use => -* \n cbmex('comment', rgba, charset, comment) to send a comment -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnComment( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 4; - - if (nrhs < 4) - PrintHelp(CBMEX_FUNCTION_COMMENT, true, "Too few inputs provided"); - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_COMMENT, true, "Too many outputs requested"); - - if (!mxIsNumeric(prhs[1]) || mxGetNumberOfElements(prhs[1]) != 1) - PrintHelp(CBMEX_FUNCTION_COMMENT, true, "Invalid rgba parameter"); - - if (!mxIsNumeric(prhs[2]) || mxGetNumberOfElements(prhs[2]) != 1) - PrintHelp(CBMEX_FUNCTION_COMMENT, true, "Invalid charset parameter"); - - uint32_t rgba = (uint32_t) mxGetScalar(prhs[1]); - uint8_t charset = (uint8_t) mxGetScalar(prhs[2]); - - char cmt[cbMAX_COMMENT] = {0}; - // fill in the comment string - if (mxGetString(prhs[3], cmt, cbMAX_COMMENT)) - PrintHelp(CBMEX_FUNCTION_COMMENT, true, "Invalid comment or comment is too long"); - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_COMMENT, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_COMMENT, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_COMMENT, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_COMMENT, true, "Last parameter requires value"); - } - - cbSdkResult res = cbSdkSetComment(nInstance, rgba, charset, cmt); - PrintErrorSDK(res, "cbSdkSetComment()"); -} - -// Author & Date: Ehsan Azar 25 Feb 2011 -/** -* Send a channel configuration, and return values. -* -* \n In MATLAB use => -* \n [config_cell_aray] = cbmex('config', channel, [, value], ...) to set one or more parameters -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnConfig( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 2; - cbSdkResult res; - bool bHasNewParams = false; - - if (nrhs < 2) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Too few inputs provided"); - if (nlhs > 1) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Too many outputs requested"); - - if (!mxIsNumeric(prhs[1]) || mxGetNumberOfElements(prhs[1]) != 1) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid channel parameter"); - - uint16_t channel = (uint16_t)mxGetScalar(prhs[1]); - - enum - { - PARAM_NONE, - PARAM_USERFLAGS, - PARAM_SMPFILTER, - PARAM_SMPGROUP, - PARAM_SPKFILTER, - PARAM_SPKGROUP, - PARAM_SPKTHRLEVEL, - PARAM_AMPLREJPOS, - PARAM_AMPLREJNEG, - PARAM_REFELECCHAN, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Do a quick look at the options just to find the instance if specified - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_CONFIG, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - param = PARAM_USERFLAGS; // Just something valid but instance - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Last parameter requires value"); - } - - // Get old config, so that we would only change the bits requested - cbPKT_CHANINFO chaninfo; - res = cbSdkGetChannelConfig(nInstance, channel, &chaninfo); - PrintErrorSDK(res, "cbSdkGetChannelConfig()"); - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_CONFIG, true, errstr); - } - if (_strcmpi(cmdstr, "userflags") == 0) - { - param = PARAM_USERFLAGS; - } - else if (_strcmpi(cmdstr, "smpfilter") == 0) - { - param = PARAM_SMPFILTER; - } - else if (_strcmpi(cmdstr, "smpgroup") == 0) - { - param = PARAM_SMPGROUP; - } - else if (_strcmpi(cmdstr, "spkfilter") == 0) - { - param = PARAM_SPKFILTER; - } - else if (_strcmpi(cmdstr, "spkthrlevel") == 0) - { - param = PARAM_SPKTHRLEVEL; - } - else if (_strcmpi(cmdstr, "spkgroup") == 0) - { - param = PARAM_SPKGROUP; - } - else if (_strcmpi(cmdstr, "amplrejpos") == 0) - { - param = PARAM_AMPLREJPOS; - } - else if (_strcmpi(cmdstr, "amplrejneg") == 0) - { - param = PARAM_AMPLREJNEG; - } - else if (_strcmpi(cmdstr, "refelecchan") == 0) - { - param = PARAM_REFELECCHAN; - } - else if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_CONFIG, true, errstr); - } - } - else - { - char cmdstr[128]; - switch(param) - { - case PARAM_USERFLAGS: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid userflags number"); - chaninfo.userflags = (uint32_t)mxGetScalar(prhs[i]); - bHasNewParams = true; - break; - case PARAM_SMPFILTER: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid smpfilter number"); - chaninfo.smpfilter = (uint32_t)mxGetScalar(prhs[i]); - if (chaninfo.smpfilter >= (cbFIRST_DIGITAL_FILTER + cbNUM_DIGITAL_FILTERS)) - mexErrMsgTxt("Invalid continuous filter number"); - bHasNewParams = true; - break; - case PARAM_SMPGROUP: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid smpgroup number"); - chaninfo.smpgroup = (uint32_t)mxGetScalar(prhs[i]); - if (chaninfo.smpgroup >= cbMAXGROUPS) - mexErrMsgTxt("Invalid sampling group number"); - bHasNewParams = true; - break; - case PARAM_SPKFILTER: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid spkfilter number"); - chaninfo.spkfilter = (uint32_t)mxGetScalar(prhs[i]); - if (chaninfo.spkfilter >= (cbFIRST_DIGITAL_FILTER + cbNUM_DIGITAL_FILTERS)) - mexErrMsgTxt("Invalid spike filter number"); - bHasNewParams = true; - break; - case PARAM_SPKTHRLEVEL: - if (!mxGetString(prhs[i], cmdstr, 16)) - { - int32_t nValue = 0; - res = cbSdkAnalogToDigital(nInstance, channel, cmdstr, &nValue); - PrintErrorSDK(res, "cbSdkAnalogToDigital()"); - chaninfo.spkthrlevel = nValue; - } - else if (mxIsNumeric(prhs[i])) - { - chaninfo.spkthrlevel = (uint32_t)mxGetScalar(prhs[i]); - } - else - { - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid spkthrlevel value"); - } - bHasNewParams = true; - break; - case PARAM_SPKGROUP: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid spkgroup number"); - chaninfo.spkgroup = (uint32_t)mxGetScalar(prhs[i]); - bHasNewParams = true; - break; - case PARAM_AMPLREJPOS: - if (!mxGetString(prhs[i], cmdstr, 16)) - { - int32_t nValue = 0; - res = cbSdkAnalogToDigital(nInstance, channel, cmdstr, &nValue); - PrintErrorSDK(res, "cbSdkAnalogToDigital()"); - chaninfo.amplrejpos = nValue; - } - else if (mxIsNumeric(prhs[i])) - { - chaninfo.amplrejpos = (uint32_t)mxGetScalar(prhs[i]); - } - else - { - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid amprejpos number"); - } - bHasNewParams = true; - break; - case PARAM_AMPLREJNEG: - if (!mxGetString(prhs[i], cmdstr, 16)) - { - int32_t nValue = 0; - res = cbSdkAnalogToDigital(nInstance, channel, cmdstr, &nValue); - PrintErrorSDK(res, "cbSdkAnalogToDigital()"); - chaninfo.amplrejneg = nValue; - } - else if (mxIsNumeric(prhs[i])) - { - chaninfo.amplrejneg = (uint32_t)mxGetScalar(prhs[i]); - } - else - { - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid amprejneg number"); - } - bHasNewParams = true; - break; - case PARAM_REFELECCHAN: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Invalid refelecchan number"); - chaninfo.refelecchan = (uint32_t)mxGetScalar(prhs[i]); - bHasNewParams = true; - break; - case PARAM_INSTANCE: - // Done before - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_CONFIG, true, "Last parameter requires value"); - } - - - /// \todo use map for parameter names - /// \todo add more parameters - - // if output specifically requested, or if no new configuration specified - // build the cell structure to get previous values - if (nlhs > 0 || ! bHasNewParams) - { - int count = 14; // number of parameters - mxArray *pca = mxCreateCellMatrix(count, 2); - plhs[0] = pca; - mxSetCell(pca, 0, mxCreateString("userflags")); - mxSetCell(pca, count + 0, mxCreateDoubleScalar(chaninfo.userflags)); - - mxSetCell(pca, 1, mxCreateString("smpfilter")); - mxSetCell(pca, count + 1, mxCreateDoubleScalar(chaninfo.smpfilter)); - - mxSetCell(pca, 2, mxCreateString("smpgroup")); - mxSetCell(pca, count + 2, mxCreateDoubleScalar(chaninfo.smpgroup)); - - mxSetCell(pca, 3, mxCreateString("spkfilter")); - mxSetCell(pca, count + 3, mxCreateDoubleScalar(chaninfo.spkfilter)); - - mxSetCell(pca, 4, mxCreateString("spkgroup")); - mxSetCell(pca, count + 4, mxCreateDoubleScalar(chaninfo.spkgroup)); - - mxSetCell(pca, 5, mxCreateString("spkthrlevel")); - mxSetCell(pca, count + 5, mxCreateDoubleScalar(chaninfo.spkthrlevel)); - - mxSetCell(pca, 6, mxCreateString("amplrejpos")); - mxSetCell(pca, count + 6, mxCreateDoubleScalar(chaninfo.amplrejpos)); - - mxSetCell(pca, 7, mxCreateString("amplrejneg")); - mxSetCell(pca, count + 7, mxCreateDoubleScalar(chaninfo.amplrejneg)); - - mxSetCell(pca, 8, mxCreateString("refelecchan")); - mxSetCell(pca, count + 8, mxCreateDoubleScalar(chaninfo.refelecchan)); - - mxSetCell(pca, 9, mxCreateString("analog_unit")); - mxSetCell(pca, count + 9, mxCreateString(chaninfo.physcalin.anaunit)); - - mxSetCell(pca, 10, mxCreateString("max_analog")); - mxSetCell(pca, count + 10, mxCreateDoubleScalar(chaninfo.physcalin.anamax)); - - mxSetCell(pca, 11, mxCreateString("max_digital")); - mxSetCell(pca, count + 11, mxCreateDoubleScalar(chaninfo.physcalin.digmax)); - - mxSetCell(pca, 12, mxCreateString("min_analog")); - mxSetCell(pca, count + 12, mxCreateDoubleScalar(chaninfo.physcalin.anamin)); - - mxSetCell(pca, 13, mxCreateString("min_digital")); - mxSetCell(pca, count + 13, mxCreateDoubleScalar(chaninfo.physcalin.digmin)); - } - - // if new configuration to send - if (bHasNewParams) - { - res = cbSdkSetChannelConfig(nInstance, channel, &chaninfo); - PrintErrorSDK(res, "cbSdkSetChannelConfig()"); - } -} - -// Author & Date: Ehsan Azar 13 April 2012 -/** -* Read or convert CCF - Cerebus Configuration File. -* -* \n In MATLAB use => -* \n cbmex('ccf', filename, [, value], ...) -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnCCF( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 1; - cbSdkResult res = CBSDKRESULT_SUCCESS; - - if (nrhs < 2) - PrintHelp(CBMEX_FUNCTION_CCF, true, "Too few inputs provided"); - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_CCF, true, "Too many outputs requested"); - - char source_file[256] = {0}; - char destination_file[256] = {0}; - cbSdkCCF ccf; - - enum - { - PARAM_NONE, - PARAM_SEND, - PARAM_CONVERT, - PARAM_LOAD, - PARAM_SAVE, - PARAM_INSTANCE, - } param = PARAM_NONE, command = PARAM_NONE; - - bool bThreaded = false; - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_CCF, true, errstr); - } - if (_strcmpi(cmdstr, "send") == 0) - { - param = PARAM_SEND; - } - else if (_strcmpi(cmdstr, "threaded") == 0) - { - bThreaded = true; - } - else if (_strcmpi(cmdstr, "load") == 0) - { - param = PARAM_LOAD; - } - else if (_strcmpi(cmdstr, "convert") == 0) - { - param = PARAM_CONVERT; - } - else if (_strcmpi(cmdstr, "save") == 0) - { - param = PARAM_SAVE; - } - else if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_CCF, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_SEND: - if (source_file[0] != 0 || command != PARAM_NONE) - mexErrMsgTxt("Cannot specify multiple commands"); - if (mxGetString(prhs[i], source_file, sizeof(source_file))) - mexErrMsgTxt("Invalid source CCF filename"); - command = PARAM_SEND; - break; - case PARAM_LOAD: - if (source_file[0] != 0) - mexErrMsgTxt("Cannot specify multiple commands"); - if (mxGetString(prhs[i], source_file, sizeof(source_file))) - mexErrMsgTxt("Invalid source CCF filename"); - break; - case PARAM_CONVERT: - if (destination_file[0] != 0 || command != PARAM_NONE) - mexErrMsgTxt("Cannot specify multiple commands"); - if (mxGetString(prhs[i], destination_file, sizeof(destination_file))) - mexErrMsgTxt("Invalid destination CCF filename"); - command = PARAM_CONVERT; - break; - case PARAM_SAVE: - if (destination_file[0] != 0 || command != PARAM_NONE) - mexErrMsgTxt("Cannot specify multiple commands"); - if (mxGetString(prhs[i], destination_file, sizeof(destination_file))) - mexErrMsgTxt("Invalid destination CCF filename"); - command = PARAM_SAVE; - break; - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_CCF, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_CCF, true, "Last parameter requires value"); - } - - switch (command) - { - case PARAM_SEND: - res = cbSdkReadCCF(nInstance, &ccf, NULL, true, true, bThreaded); - break; - case PARAM_SAVE: - res = cbSdkReadCCF(nInstance, &ccf, NULL, true, false, false); - break; - case PARAM_CONVERT: - if (source_file[0] == 0) - PrintHelp(CBMEX_FUNCTION_CCF, true, "source file not specified for 'convert' command"); - res = cbSdkReadCCF(nInstance, &ccf, source_file, true, false, false); - break; - default: - // Never should happen - break; - } - // Check if reading was successful - PrintErrorSDK(res, "cbSdkReadCCF()"); - - if (command == PARAM_SAVE || command == PARAM_CONVERT) - { - res = cbSdkWriteCCF(nInstance, &ccf, destination_file, bThreaded); - PrintErrorSDK(res, "cbSdkWriteCCF()"); - } -} -// Author & Date: Ehsan Azar 11 May 2012 -/** -* Send a system runlevel command. -* -* \n In MATLAB use => -* \n cbmex('system', ) to send a runelvel command -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnSystem( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - int nFirstParam = 2; - - if (nrhs < 2) - PrintHelp(CBMEX_FUNCTION_SYSTEM, true, "Too few inputs provided"); - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_SYSTEM, true, "Too many outputs requested"); - - char cmdstr[128]; - - // check the input argument count - if (mxGetString(prhs[1], cmdstr, 16)) - PrintHelp(CBMEX_FUNCTION_SYSTEM, true, "Invalid system command"); - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = nFirstParam; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_SYSTEM, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_SYSTEM, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_SYSTEM, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_SYSTEM, true, "Last parameter requires value"); - } - - cbSdkSystemType cmd = cbSdkSystem_RESET; - if (_strcmpi(cmdstr, "reset") == 0) - cmd = cbSdkSystem_RESET; - else if (_strcmpi(cmdstr, "shutdown") == 0) - cmd = cbSdkSystem_SHUTDOWN; - else if (_strcmpi(cmdstr, "standby") == 0) - cmd = cbSdkSystem_STANDBY; - else - PrintHelp(CBMEX_FUNCTION_SYSTEM, true, "Invalid system command"); - - cbSdkResult res = cbSdkSystem(nInstance, cmd); - PrintErrorSDK(res, "cbSdkSystem()"); -} - -// Author & Date: Ehsan Azar 23 April 2013 -/** -* Send a synch output command. -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnSynchOut( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - uint32_t nFreq = 0; - uint32_t nRepeats = 0; - uint32_t nChannel = 1; - - if (nrhs < 2) - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, "Too few inputs provided"); - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, "Too many outputs requested"); - - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - PARAM_FREQ, - PARAM_REPEATS, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = 1; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else if (_strcmpi(cmdstr, "freq") == 0) - { - param = PARAM_FREQ; - } - else if (_strcmpi(cmdstr, "repeats") == 0) - { - param = PARAM_REPEATS; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, errstr); - } - } - else - { - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_FREQ: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, "Invalid frequency value"); - nFreq = (uint32_t)(mxGetScalar(prhs[i]) * 1000); - break; - case PARAM_REPEATS: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, "Invalid repeats value"); - nRepeats = (uint32_t)mxGetScalar(prhs[i]); - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, "Last parameter requires value"); - } - - cbSdkResult res = cbSdkSetSynchOutput(nInstance, nChannel, nFreq, nRepeats); - if (res == CBSDKRESULT_NOTIMPLEMENTED) - PrintErrorSDK(res, "cbSdkSynchOut(): Only CerePlex currently supports this command"); - PrintErrorSDK(res, "cbSdkSynchOut()"); -} - -// Author & Date: Ehsan Azar 14 May 2013 -/** -* Send an extension command. -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ -void OnExtCmd( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ - uint32_t nInstance = 0; - cbSdkResult res = CBSDKRESULT_SUCCESS; - cbSdkExtCmd extCmd; - - if (nrhs < 2) - PrintHelp(CBMEX_FUNCTION_EXT, true, "Too few inputs provided"); - if (nlhs > 0) - PrintHelp(CBMEX_FUNCTION_EXT, true, "Too many outputs requested"); - - - enum - { - PARAM_NONE, - PARAM_INSTANCE, - PARAM_CMD, - PARAM_UPLOAD, - PARAM_INPUT, - } param = PARAM_NONE; - - // Process remaining input arguments if available - for (int i = 1; i < nrhs; ++i) - { - if (param == PARAM_NONE) - { - char cmdstr[128]; - if (mxGetString(prhs[i], cmdstr, 16)) - { - char errstr[128]; - sprintf(errstr, "Parameter %d is invalid", i); - PrintHelp(CBMEX_FUNCTION_EXT, true, errstr); - } - if (_strcmpi(cmdstr, "instance") == 0) - { - param = PARAM_INSTANCE; - } - else if (_strcmpi(cmdstr, "upload") == 0) - { - param = PARAM_UPLOAD; - } - else if (_strcmpi(cmdstr, "command") == 0) - { - param = PARAM_CMD; - } - else if (_strcmpi(cmdstr, "input") == 0) - { - param = PARAM_INPUT; - } - else if (_strcmpi(cmdstr, "terminate") == 0) - { - // Send a kill request - extCmd.cmd = cbSdkExtCmd_TERMINATE; - res = cbSdkExtDoCommand(nInstance, &extCmd); - // On error just get out of the for-loop - if (res != CBSDKRESULT_SUCCESS) - break; - } - else - { - char errstr[128]; - sprintf(errstr, "Parameter %d (%s) is invalid", i, cmdstr); - PrintHelp(CBMEX_FUNCTION_EXT, true, errstr); - } - } - else - { - char cmdstr[cbMAX_LOG] = {'\0'}; - switch(param) - { - case PARAM_INSTANCE: - if (!mxIsNumeric(prhs[i])) - PrintHelp(CBMEX_FUNCTION_SYNCHOUT, true, "Invalid instance number"); - nInstance = (uint32_t)mxGetScalar(prhs[i]); - break; - case PARAM_UPLOAD: - if (mxGetString(prhs[i], cmdstr, cbMAX_LOG - 1)) - PrintHelp(CBMEX_FUNCTION_EXT, true, "Invalid filename"); - extCmd.cmd = cbSdkExtCmd_UPLOAD; - strncpy(extCmd.szCmd, cmdstr, sizeof(extCmd.szCmd)); - res = cbSdkExtDoCommand(nInstance, &extCmd); - // On error just get out of the for-loop without goto - if (res != CBSDKRESULT_SUCCESS) - i = nrhs; - break; - case PARAM_CMD: - if (mxGetString(prhs[i], cmdstr, cbMAX_LOG - 1)) - PrintHelp(CBMEX_FUNCTION_EXT, true, "Invalid command string"); - extCmd.cmd = cbSdkExtCmd_RPC; - strncpy(extCmd.szCmd, cmdstr, sizeof(extCmd.szCmd)); - res = cbSdkExtDoCommand(nInstance, &extCmd); - // On error just get out of the for-loop without goto - if (res != CBSDKRESULT_SUCCESS) - i = nrhs; - break; - case PARAM_INPUT: - if (mxGetString(prhs[i], cmdstr, cbMAX_LOG - 1)) - PrintHelp(CBMEX_FUNCTION_EXT, true, "Invalid input string"); - extCmd.cmd = cbSdkExtCmd_INPUT; - strncpy(extCmd.szCmd, cmdstr, sizeof(extCmd.szCmd)); - res = cbSdkExtDoCommand(nInstance, &extCmd); - // On error just get out of the for-loop without goto - if (res != CBSDKRESULT_SUCCESS) - i = nrhs; - break; - default: - break; - } - param = PARAM_NONE; - } - } // end for (int i = nFirstParam - - if (param != PARAM_NONE) - { - // Some parameter did not have a value, and value is non-optional - PrintHelp(CBMEX_FUNCTION_EXT, true, "Last parameter requires value"); - } - - if (res == CBSDKRESULT_NOTIMPLEMENTED) - PrintErrorSDK(res, "cbSdkExtCmd(): NSP1 does not support this"); - PrintErrorSDK(res, "cbSdkExtCmd()"); -} - -#ifdef WIN32 -#define MEX_EXPORT -#else -#define MEX_EXPORT CBSDKAPI -#endif - -///////////////////////////////////////////////////////////////////////////// -/** -* The one and only mexFunction. -* -* @param[in] nlhs Number of left hand side (output) arguments -* @param[in] plhs Array of left hand side arguments -* @param[in] nrhs Number of right hand side (input) arguments -* @param[in] prhs Array of right hand side arguments -*/ - -extern "C" MEX_EXPORT void mexFunction( - int nlhs, - mxArray *plhs[], - int nrhs, - const mxArray *prhs[] ) -{ -#ifdef DEBUG_CONSOLE -#ifdef WIN32 - AllocConsole(); -#endif -#endif - - static NAME_LUT lut = CreateLut(); // The actual look up table - - g_bMexCall = true; - - // assign the atexit function to close the library and deallocate everything if - // the mex DLL is closed for any reason before the 'close' command is called - mexAtExit(matexit); - - // test for minimum number of arguments - if (nrhs < 1) - PrintHelp(CBMEX_FUNCTION_COUNT, true); - - // get the command string and process the command - char cmdstr[16]; - if (mxGetString(prhs[0], cmdstr, 16)) - PrintHelp(CBMEX_FUNCTION_COUNT, true); - - // Find and then call the correct function - NAME_LUT::iterator it = lut.find(cmdstr); - if (it == lut.end()) - { - // Not found - PrintHelp(CBMEX_FUNCTION_COUNT, true); - } - else - { - // The map triple is "command name", "function pointer", "function index" - // so call the function pointer (2nd value of pair) - (*it).second.first(nlhs, plhs, nrhs, prhs); - } -} diff --git a/bindings/cbmex/cbmex.h b/bindings/cbmex/cbmex.h deleted file mode 100755 index 4fe9aa6d..00000000 --- a/bindings/cbmex/cbmex.h +++ /dev/null @@ -1,424 +0,0 @@ -/* =STS=> cbmex.h[4257].aa10 open SMID:10 */ -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2010 - 2011 Blackrock Microsystems -// -// $Workfile: cbmex.h $ -// $Archive: /Cerebus/Human/WindowsApps/cbmex/cbmex.h $ -// $Revision: 1 $ -// $Date: 2/17/11 3:15p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// -/*! \file cbmex.h - \brief MATLAB executable (MEX) interface for cbsdk. - -*/ -#ifndef CBMEX_H_INCLUDED -#define CBMEX_H_INCLUDED - -#if defined(WIN32) -#include -#else -#include "stdint.h" -#endif -#include "mex.h" -#include "mex_compat.h" - - -///////////////////////////// Prototypes for all of the Matlab events //////// -void OnHelp (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnOpen (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnClose (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnTime (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnTrialConfig (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnChanLabel (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnTrialData (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnTrialComment (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnTrialTracking (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnFileConfig (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnDigitalOut (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnAnalogOut (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnMask (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnComment (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnConfig (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnCCF (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnSystem (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnSynchOut (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -void OnExtCmd (int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ); -////////////////////// end of Prototypes for all of the Matlab events //////// - -#define CBMEX_USAGE_CBMEX \ - "cbmex is the live MATLAB interface for Cerebus system\n" \ - "Format: cbmex(, [arg1], ...)\n" \ - "Inputs:\n" \ - " is a string and can be any of:\n" \ - "'help', 'open', 'close', 'time', 'trialconfig', 'chanlabel',\n" \ - "'trialdata', 'fileconfig', 'digitalout', 'mask', 'comment', 'config',\n" \ - "'analogout', 'trialcomment', 'trialtracking', 'ccf', 'system', 'synchout', 'ext'\n" \ - "Use cbmex('help', ) for each command usage\n" \ - -#define CBMEX_USAGE_HELP \ - "Prints usage information about cbmex functions\n" \ - "Format: cbmex('help', [command])\n" \ - "Inputs:\n" \ - "command: is the command name\n" \ - "Use cbmex('help') to get the list of commands\n" \ - -#define CBMEX_USAGE_OPEN \ - "Opens the live MATLAB interface for Cerebus system\n" \ - "Format: [connection instrument] = cbmex('open', [interface = 0], [[, value]])\n" \ - "Inputs:\n" \ - " interface (optional): 0 (Default), 1 (Central), 2 (UDP)\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to open (default is 0)\n" \ - "'inst-addr', value: value is string containing instrument ipv4 address\n" \ - "'inst-port', value: value is the instrument port number\n" \ - "'central-addr', value: value is string containing central ipv4 address\n" \ - "'central-port', value: value is the central port number\n" \ - "'receive-buffer-size', value: override default network buffer size (low value may result in drops)\n" \ - "\n" \ - "Outputs:\n" \ - " connection (optional): 1 (Central), 2 (UDP)\n" \ - " instrument (optional): 0 (NSP), 1 (Local nPlay), 2 (Local NSP), 3 (Remote nPlay)\n" \ - -#define CBMEX_USAGE_CLOSE \ - "Closes the live MATLAB interface for Cerebus system\n" \ - "Format: cbmex('close', [[, value]])\n" \ - "Inputs:\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to close (default is 0)\n" \ - -#define CBMEX_USAGE_TIME \ - "Get the latest Cerebus time past last reset\n" \ - "Format: cbmex('time', [[, value]])\n" \ - "Inputs:\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - "'samples': if specified sample number is returned otherwise seconds\n" \ - -#define CBMEX_USAGE_CHANLABEL \ - "Get current channel labbels and optionally set new labels\n" \ - "Format: [label_cell_array] = cbmex('chanlabel', [channels_vector], [new_label_cell_array], [[, value]])\n" \ - "Inputs:\n" \ - "channels_vector (optional): a vector of all the channel numbers to change their label\n" \ - " if not specified all the 156 channels are considered\n" \ - "new_label_cell_array (optional): cell array of new labels (each cell a string of maximum 16 characters)\n" \ - " number of labels must match number of channels\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - "\n" \ - "Outputs:\n" \ - " label_cell_array (optional): If specified previous labels are returned\n" \ - "Each row in label_cell_array looks like:\n" \ - " For spike channels:\n" \ - " 'channel label' [spike_enabled] [unit0_valid] [unit1_valid] [unit2_valid] [unit3_valid] [unit4_valid]\n" \ - " For digital input channels:\n" \ - " 'channel label' [digin_enabled] ...remaining columns are empty...\n" \ - -#define CBMEX_USAGE_TRIALCONFIG \ - "Configures a trial to grab data with 'trialdata'\n" \ - "Format: [ active_state, [config_vector_out] ]\n" \ - " = cbmex('trialconfig', active, [config_vector_in], [[, value]])\n" \ - "Inputs:\n" \ - "active: set 1 to flush data cache and start collecting data immediately,\n" \ - " set 0 to stop collecting data immediately\n" \ - "config_vector_in (optional):\n" \ - " vector [begchan begmask begval endchan endmask endval] specifying start and stop channels, default is zero all\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'double': if specified, the data is in double precision format (old behaviour)\n" \ - "'absolute': if specified event timing is absolute (active will not reset time for events)\n" \ - "'nocontinuous': if specified, continuous data cache is not created nor configured (same as 'continuous',0)\n" \ - "'noevent': if specified, event data cache is not created nor configured (same as 'event',0)\n" \ - "'waveform', value: set the number of waveforms to be cached (internal cache if less than 400)\n" \ - "'continuous', value: set the number of continuous data to be cached\n" \ - "'event', value: set the number of evnets to be cached\n" \ - "'comment', value: set number of comments to be cached\n" \ - "'tracking', value: set the number of video tracking evnets to be cached" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - "\n" \ - "Outputs:\n" \ - "active_state: return 1 if data collection is active, 0 otherwise\n" \ - "config_vector_out:\n" \ - " vector [begchan begmask begval endchan endmask endval double waveform continuous event comment tracking]\n" \ - " specifying the configuration state\n" \ - -#define CBMEX_USAGE_TRIALDATA \ - "Grab continuous and even data configured by 'trialconfig'\n" \ - "Format:\n" \ - " [timestamps_cell_array[, time, continuous_cell_array]] = cbmex('trialdata', [active = 0], [[, value]])\n" \ - " [time, continuous_cell_array] = cbmex('trialdata', [active = 0], [[, value]])\n" \ - "Note: above format means:\n" \ - " if one output requested it will be timestamps_cell_array\n" \ - " if two outputs requested it will be time and continuous_cell_array\n" \ - "Inputs:\n" \ - "active (optional):\n" \ - " set 0 (default) to leave buffer intact\n" \ - " set 1 to clear all the data and reset the trial time to the current time\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - "\n" \ - "Outputs:\n" \ - "timestamps_cell_array: Timestamps for events of 152 channels (152 rows). Each row in this matrix looks like:\n" \ - " For spike channels:\n" \ - " 'channel name' [unclassified timestamps_vector] [u1_timestamps_vector] [u2_timestamps_vector] [u3_timestamps_vector] [u4_timestamps_vector] [u5_timestamps_vector]\n" \ - " For digital input channels:\n" \ - " 'channel name' [timestamps_vector] [values_vector] ...remaining columns are empty...\n" \ - "time: Time (in seconds) that the data buffer was most recently cleared.\n" \ - "continuous_cell_array: Continuous sample data, variable number of rows. Each row in this matrix looks like:\n" \ - " [channel number] [sample rate (in samples / s)] [values_vector]\n" \ - -#define CBMEX_USAGE_TRIALCOMMENT \ - "Grab comments configured by 'trialconfig'\n" \ - "Format:\n" \ - " [comments_cell_array, [timestamps_vector], [rgba_vector], [charset_vector]] \n" \ - " = cbmex('trialcomment', [active = 0], [[, value]])\n" \ - "Inputs:\n" \ - "active (optional):\n" \ - " set 0 (default) to leave buffer intact\n" \ - " set 1 to clear all the data and reset the trial time to the current time\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - "\n" \ - "Outputs:\n" \ - "comments_cell_array: cell-array of comments (strings of possibly different sizes)\n" \ - "timestamps_vector (optional): timastamps of the comments\n" \ - "rgba_vector (optional): comments colors\n" \ - "charset_vector (optional): characterset vector for comments\n" \ - - -#define CBMEX_USAGE_TRIALTRACKING \ - "Grab tracking data configured by 'trialconfig'\n" \ - "Format:\n" \ - " [tracking_cell_array] = cbmex('trialtracking', [active = 0], [[, value]])\n" \ - "Inputs:\n" \ - "active (optional):\n" \ - " set 0 (default) to leave buffer intact\n" \ - " set 1 to clear all the data and reset the trial time to the current time\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - "\n" \ - "Outputs:\n" \ - "tracking_cell_array: Each row in this matrix looks like:\n" \ - " 'trackable_name' desc_vector timestamps_vector synch_timestamps_vector synch_frame_numbers_vector rb_cell_array\n" \ - " Here is the description of output:\n" \ - " desc_vector: column vector [type id max_point_count]\n" \ - " type: 1 (2DMARKERS), 2 (2DBLOB), 3 (3DMARKERS), 4 (2DBOUNDARY)\n" \ - " id: node unique ID\n" \ - " max_point_count: maximum number of points for this trackable\n" \ - " timestamps_vector: the timestamps of the tracking packets\n" \ - " synch_timestamps_vector: synchronized timestamps of the tracking (in milliseconds)\n" \ - " synch_frame_numbers_vector: synchronized frame numbers of tracking\n" \ - " rb_cell_array: each cell is a matrix of rigid-body, the rows are points, columns are coordinates\n"\ - - -#define CBMEX_USAGE_FILECONFIG \ - "Configures file recording\n" \ - "Format: [recording filename username] = cbmex('fileconfig', [filename, comments, action, [[, value]]])\n" \ - "Inputs:\n" \ - "filename: file name string (255 character maximum)\n" \ - "comments: file comment string (255 character maximum)\n" \ - "action: set 1 to start recording, 0 to stop recording" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - "'option', value: value is can be any of the 'open', 'close', 'none' (default)\n" \ - " 'close' - closes the File dialog if open\n" \ - " 'open' - opens the File dialog if closed, ignoring other parameters\n" \ - " 'none' - opens the File dialog if closed, sets parameters given, starts or stops recording\n" \ - "Outputs:\n" \ - "recording: 1 if recording is in progress, 0 if not\n" \ - "filename: recording file name\n" \ - "username: recording user name\n" \ - -#define CBMEX_USAGE_DIGITALOUT \ - "Set digital output properties for given channel\n" \ - "Format: cbmex('digitalout', channel, default_value, [[, value]])\n" \ - "Inputs:\n" \ - "channel: 153 (dout1), 154 (dout2), 155 (dout3), 156 (dout4)\n" \ - "default_value: 1 sets dout default to ttl high and 0 sets dout default to ttl low" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - - -#define CBMEX_USAGE_ANALOGOUT \ - "Set analog output properties for given channel\n" \ - "Format: cbmex('analogout', channel, [[, value]])\n" \ - "Inputs:\n" \ - "channel: 145 (aout1), 146 (aout2), 147 (aout3), 148 (aout4)\n" \ - " 149 (audout1), 150 (audout2)\n" \ - "Each analog output can exclusivlely monitor a channel, generate custom waveform or be disabled\n" \ - ", pairs are optional, Some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'pulses', waveform: waveform is vector \n" \ - " [nPhase1Duration nPhase1Amplitude nInterPhaseDelay nPhase2Duration nPhase2Amplitude nInterPulseDelay]\n" \ - "'sequence', waveform: waveform is variable length vector of duration and amplitude\n" \ - " Waveform format is [nDuration1 nAmplitude1 nDuration2 nAmplitude2 ...]\n" \ - " Waveform must be a nonempty even-numbered vector of double-precision numbers\n" \ - " Each duration must be followed by amplitude for that duration\n" \ - " Durations must be positive\n" \ - " Each duration-amplitude pair is a phase in the waveform\n" \ - "'sinusoid', waveform: waveform is vector [nFrequency nAmplitude]\n" \ - "'monitor', type: type can be any of 'spike', 'continuous'\n" \ - " 'spike' means spikes on 'input' channel are monitored\n" \ - " 'continuous' means continuous 'input' channel is monitored\n" \ - "'track': monitor the last tracked channel\n" \ - "'disable': disable analog output\n" \ - "'offset', offset: amplitude offset\n" \ - "'repeats', repeats: number of repeats. 0 (default) means non-stop\n" \ - "'index', index: trigger index (0 to 4) is the per-channel trigger index (default is 0)\n" \ - "'trigger', trigger: trigger can be any of 'instant' (default), 'off', 'dinrise', 'dinfall', 'spike', 'cmtcolor', 'softreset'\n" \ - " 'off' means this trigger is not used\n" \ - " 'instant' means immediate trigger (immediate analog output waveform)\n" \ - " 'dinrise' is for digital input rising edge, 'dinfall' is for digital input falling edge\n" \ - " 'spike' is the spike event on given input channel\n" \ - " 'cmtcolor' is the trigger based on colored comment\n" \ - " 'softreset' is the trigger based on software reset (e.g. result of file recording)\n" \ - " 'extension' is the trigger based on extension\n" \ - "'input', input: input depends on 'trigger' or 'monitor'\n" \ - " If trigger is 'dinrise' or 'dinfall' then 'input' is bit number of 1 to 16 for first digital input\n" \ - " If trigger is 'spike' then 'input' is input channel with spike data\n" \ - " If trigger is 'cmtcolor' then 'input' is the high word (two bytes) of the comment color\n" \ - " If monitor is 'spike' then 'input' is input channel with spike processing\n" \ - " If monitor is 'continuous' then 'input' is input channel with continuous data\n" \ - "'value', value: trigger value depends on 'trigger'\n" \ - " If trigger is 'cmtcolor' then 'value' is the low word (two bytes) of the comment color\n" \ - " If trigger is 'spike' then 'value' is spike unit number\n" \ - " (0 for unclassified, 1-5 for first to fifth unit and 254 for any unit)\n" \ - "'mv': if specified, voltages are considered in milli volts instead of raw integer value\n" \ - "'ms': if specified, intervals are considered in milli seconds instead of samples\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - - -#define CBMEX_USAGE_MASK \ - "Masks or unmasks given channels from trials\n" \ - "Format: cbmex('mask', channel, [active = 1], [[, value]])\n" \ - "Inputs:\n" \ - "channel: The channel number to mask\n" \ - " channel 0 means all of the channels\n" \ - "active (optional): set 1 (default) to activate, 0 to deactivate (mask out)\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - - -#define CBMEX_USAGE_COMMENT \ - "Send comment or user event\n" \ - "Format: cbmex('comment', rgba, charset, comment, [[, value]])\n" \ - "Inputs:\n" \ - "rgba: color coding or custom event number\n" \ - "charset: character-set 0 for ASCII or 1 for UTF16 or any user-defined\n" \ - "comment: comment string (maximum 127 characters)\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - - -#define CBMEX_USAGE_CONFIG \ - "Get channel config and optionally change some channel parameters\n" \ - "Format: [config_cell_aray] = cbmex('config', channel, [[, value]])\n" \ - "Note: \n" \ - " if new configuration is given and config_cell_aray is not present, nothing will be returned\n" \ - "Inputs:\n" \ - "channel: The channel number\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - "'userflags', value: a user-defind value for the channel\n" \ - "'smpfilter', value: continuous filter number\n" \ - "'smpgroup', value: continuous sampling group number\n" \ - "'spkfilter', value: spike filter number\n" \ - "'spkgroup', value: NTrode group number\n" \ - "'spkthrlevel', value: spike threshold level (can be raw integer, or string such as '-65mV')\n" \ - "'amplrejpos', value: amplitude rejection positive range (can be raw integer, or string such as '5V')\n" \ - "'amplrejneg', value: amplitude rejection negative range (can be raw integer, or string such as '-5V')\n" \ - "'refelecchan', value: reference electrode number\n" \ - "\n" \ - "Outputs:\n" \ - "config_cell_array (optional): if specified previous parameters are returned\n" \ - "Each row in this matrix looks like:" \ - " , " \ - " can be anything that is also specified in the Inputs section (plus the additional parameters below)\n" \ - " will be the configuration value for the channel\n" \ - " additional \n" \ - "'analog_unit': unit of analog value\n" \ - "'max_analog': maximum analog value\n" \ - "'max_digital': maximum digital value\n" \ - "'min_analog': minimum analog value\n" \ - "'min_digital': minimum digital value\n" \ - -#define CBMEX_USAGE_CCF \ - "Read, write and send CCF configuration file\n" \ - "Format: cbmex('ccf', [[, value]])\n" \ - "Inputs:\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'threaded': Use threaded operation when possible\n" \ - "'send', filename: read CCF file and send it to NSP\n" \ - "'save', destination_file: Save active configuration to CCF file\n" \ - "'load', source_file, 'convert', destination_file: convert source file to new CCF file (in latest format)\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - -#define CBMEX_USAGE_SYSTEM \ - "Perform given cbmex system command\n" \ - "Format: cbmex('system', , [[, value]])\n" \ - "Inputs:\n" \ - " can be any of the following\n" \ - " 'reset' : resets instrument\n" \ - " 'shutdown' : shuts down instrument\n" \ - " 'standby' : sends instrument to standby mode" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - -#define CBMEX_USAGE_SYNCHOUT \ - "Set synch output clock\n" \ - "Format: cbmex('synchout', [[, value]])\n" \ - "Inputs:\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'freq', value: value is the frequency in Hz, 0 to stop (closest supported frequency will be used)\n" \ - "'repeats', repeats: number of repeats. 0 (default) means non-stop\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - -#define CBMEX_USAGE_EXTENSION \ - "Extension control\n" \ - "Format: cbmex('ext', [[, value]])\n" \ - "Inputs:\n" \ - "[, value] pairs are optional, some parameters do not have values.\n" \ - " from left to right parameters will override previous ones or combine with them if possible.\n" \ - "[, value] can be any of:\n" \ - "'upload', filepath: upload given file to extension root (blocks until upload finish)\n" \ - "'command', cmd: cmd is string command to run on extension root\n" \ - "'input', line: line is input to the previous command (if running)\n" \ - "'terminate': to signal last running command to terminate (if running)\n" \ - "'instance', value: value is the library instance to use (default is 0)\n" \ - -#endif /* CBMEX_H_INCLUDED */ diff --git a/bindings/cbmex/cbmex.m b/bindings/cbmex/cbmex.m deleted file mode 100755 index 2718ce18..00000000 --- a/bindings/cbmex/cbmex.m +++ /dev/null @@ -1,49 +0,0 @@ -%CBMEX Cerebus real-time matlab interface for the NSP128E -% This interface allows blocks of cerebus data from experimental -% trials to be captured and loaded during real-time operation. -% -% CBMEX('open') allocates real-time data cache memory for trials -% and establishes a connection with the Central Application. -% -% TIME = CBMEX('time') returns current Cerebus system time in sec. -% -% [ACTcur,CFGcur] = CBMEX('trialconfig') % cur = current state -% [ACTnew,CFGcur] = CBMEX('trialconfig',ACT) -% [ACTnew,CFGnew] = CBMEX('trialconfig',ACT,CFG) allow the active -% state of the trial recording data cache to be set and allow NSP -% input ports to be configured to start and stop trials directly. -% ACT is 0 or 1 to indicate whether a trial is currently active. -% CFG takes the form [ BEGCH BEGMSK BEGVAL ENDCH ENDMSK ENDVAL ] -% For BEGx and ENDx, the channel CH is ANDed with MSK and compared -% with value VAL to test for the beginning and end of trials. -% For example, if the words of the digital input port (ch 151) -% have their LSB set to 1 at the beginning and middle of a trial, -% and a word with the LSB set to 0 is sent to mark the end of a -% trial, you would use CFG = [ 151 1 1 151 1 0 ]. -% -% CF = CBMEX('chanconfig') gets the current configuration for the -% first 152 system channels. CF is a 2D cell array accessed with -% each row describing aspects of a specific channel. For neural -% channels CF{chid,:} = { 'label' CHEN U1EN U2EN U3EN U4EN U5EN } -% where CHEN and UxEN are 0 or 1 if the channel and unit x are -% enabled. Analog outputs are defined by label only. For digital -% input channels, CF{chid,:} = { 'label' chen [] [] [] [] [] } -% -% TD = CBMEX('trialdata') retrieves the current trial data from -% the real-time data cache for the first 152 channels. TD is a -% cell array where each row contains data for a specific channel. -% For neural channels, CF{chid,:} = {'label' U0 U1 U2 U3 U4 U5 }, -% where Ux are timestamps for unsorted (U0) and sorted (U1-U5) -% events. For Digital inputs, CF{chid,:} = {'label' TIM VAL .. } -% where TIM is the timestamps and VAL are the digital values. -% Timestamps are given in seconds from the trial beginning. -% -% CBMEX('start') - start streaming data to disk -% CBMEX('stop') - stop saving data to disk -% -% CBMEX('close') closes the Central Application connection and -% releases the trial real-time cache data memory. -% -% $ Revision 0.9a - written by Shane Guillory (Nov-2002) -% $ Copyright 2002 - Bionic Technologies / Cyberkinetics, Inc. -% diff --git a/bindings/cbmex/main.cpp b/bindings/cbmex/main.cpp deleted file mode 100755 index c3b3a134..00000000 --- a/bindings/cbmex/main.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// =STS=> main.cpp[2461].aa17 open SMID:18 - -/////////////////////////////////////////////////////////////////////// -// -// Cerebus SDK -// -// (c) Copyright 2012-2013 Blackrock Microsystems -// -// $Workfile: main.cpp $ -// $Archive: /Cerebus/Human/WindowsApps/cbmex/main.cpp $ -// $Revision: 1 $ -// $Date: 04/29/12 11:06a $ -// $Author: Ehsan $ -// -// Purpose: -// Produce SDK version information -// -// - -#include "StdAfx.h" -#include "../CentralCommon/BmiVersion.h" - -// Author & Date: Ehsan Azar 29 April 2012 -// Purpose: Print out the compatible library version -int main(int argc, char *argv[]) -{ - int major = BMI_VERSION_MAJOR; - int minor = BMI_VERSION_MINOR; - int release = BMI_VERSION_RELEASE; - - // We assume the beta releases all share the same ABI - printf("%d.%02d.%02d", major, minor, release); - - return 0; -} diff --git a/bindings/cbmex/mex_compat.h b/bindings/cbmex/mex_compat.h deleted file mode 100644 index d3153ed6..00000000 --- a/bindings/cbmex/mex_compat.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef _MEX_COMPAT_H -#define _MEX_COMPAT_H - -// compatibility layer for Octave - -#ifndef MAX_int16_T -#define MAX_int16_T ((int16_T)(32767)) -#define MIN_int16_T ((int16_T)(-32768)) -#define MAX_uint16_T ((uint16_T)(65535U)) -#define MIN_uint16_T ((uint16_T)(0U)) -#endif - -#endif - diff --git a/bindings/cbmex/mexprog.def b/bindings/cbmex/mexprog.def deleted file mode 100755 index 5fb09f0b..00000000 --- a/bindings/cbmex/mexprog.def +++ /dev/null @@ -1,2 +0,0 @@ -LIBRARY cbmex.mexw32 -EXPORTS mexFunction diff --git a/bindings/cbmex/res/cbmex.rc2 b/bindings/cbmex/res/cbmex.rc2 deleted file mode 100644 index b703285d..00000000 --- a/bindings/cbmex/res/cbmex.rc2 +++ /dev/null @@ -1,26 +0,0 @@ -// -// CBMEX.RC2 - resources Microsoft Visual C++ does not edit directly -// - -#ifdef APSTUDIO_INVOKED - #error this file is not editable by Microsoft Visual C++ -#endif //APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// Add manually edited resources here... - -#include "../../../src/central/BmiVersion.h" - -///////////////////////////////////////////////////////////////////////////// -// -// String Table -// - -#ifdef APSTUDIO_INVOKED -STRINGTABLE -BEGIN - IDS_COPYRIGHT "Copyright (C) " STRINGIFY(BMI_COPYRIGHT_FROM) " - " STRINGIFY(BMI_COPYRIGHT_TO) " Blackrock Microsystems" - IDS_DATEBUILT "on " STRINGIFY(BMI_VERSION_DATE_BUILT) -END -#endif //APSTUDIO_INVOKED diff --git a/bindings/cbmex/resource.h b/bindings/cbmex/resource.h deleted file mode 100755 index 6d0f1965..00000000 --- a/bindings/cbmex/resource.h +++ /dev/null @@ -1,24 +0,0 @@ -/* =STS=> resource.h[2463].aa02 open SMID:2 */ -//{{NO_DEPENDENCIES}} -// Microsoft Developer Studio generated include file. -// Used by cbMex.rc -// - -#define IDR_MAINFRAME 128 -#define IDC_STATIC_NSP_ID 994 -#define IDS_COPYRIGHT_FROM 995 -#define IDS_APPNAME 996 -#define IDC_STATIC_APP_VERSION 997 -#define IDC_STATIC_LIB_VERSION 998 -#define IDC_STATIC_NSP_VERSION 999 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 101 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1000 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/bindings/cli/AssemblyInfo.cpp b/bindings/cli/AssemblyInfo.cpp deleted file mode 100644 index 9657e597..00000000 --- a/bindings/cli/AssemblyInfo.cpp +++ /dev/null @@ -1,38 +0,0 @@ -//#include "stdafx.h" - -using namespace System; -using namespace System::Reflection; -using namespace System::Runtime::CompilerServices; -using namespace System::Runtime::InteropServices; -using namespace System::Security::Permissions; - -// -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -// -[assembly:AssemblyTitleAttribute("CereLink")]; -[assembly:AssemblyDescriptionAttribute("")]; -[assembly:AssemblyConfigurationAttribute("")]; -[assembly:AssemblyCompanyAttribute("ChadwickBoulay")]; -[assembly:AssemblyProductAttribute("CereLink")]; -[assembly:AssemblyCopyrightAttribute("Copyright (c) Chadwick Boulay 2018")]; -[assembly:AssemblyTrademarkAttribute("")]; -[assembly:AssemblyCultureAttribute("")]; - -// -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the value or you can default the Revision and Build Numbers -// by using the '*' as shown below: - -[assembly:AssemblyVersionAttribute("1.0.*")]; - -[assembly:ComVisible(false)]; - -[assembly:CLSCompliantAttribute(true)]; diff --git a/bindings/cli/CMakeLists.txt b/bindings/cli/CMakeLists.txt deleted file mode 100644 index 2e67102a..00000000 --- a/bindings/cli/CMakeLists.txt +++ /dev/null @@ -1,52 +0,0 @@ -option(CBSDK_BUILD_CLI "Build C++/CLI" OFF) - -IF(${CBSDK_BUILD_CLI}) - cmake_minimum_required(VERSION 3.12) - set(CMAKE_CSharp_FLAGS "/langversion:latest") - SET( LIB_NAME_CLI cbsdk_cli) - SET(cbsdk_cli_SOURCE - ${CMAKE_CURRENT_LIST_DIR}/cbsdk_native.h - ${CMAKE_CURRENT_LIST_DIR}/cbsdk_native.cpp - ${CMAKE_CURRENT_LIST_DIR}/AssemblyInfo.cpp) - set_source_files_properties(${cbsdk_cli_SOURCE} PROPERTIES LANGUAGE "CXX") - ADD_LIBRARY(${LIB_NAME_CLI} MODULE ${cbsdk_cli_SOURCE}) - TARGET_INCLUDE_DIRECTORIES(${LIB_NAME_CLI} PRIVATE - ${LIB_INCL_DIRS} - ${PROJECT_SOURCE_DIR}/wrappers/cbmex) - # Link to the native library. - TARGET_LINK_LIBRARIES( ${LIB_NAME_CLI} - ${LIB_NAME} - # ${QT_LIBRARIES} - ) - ADD_DEPENDENCIES(${LIB_NAME_CLI} ${LIB_NAME}) - # Set to CLR - SET_TARGET_PROPERTIES(${LIB_NAME_CLI} - PROPERTIES - COMMON_LANGUAGE_RUNTIME "" - #DEBUG_POSTFIX "" # With d postfix then the C# app can't find it. - ) - # target_compile_options(${LIB_NAME_CLI} PRIVATE /clr) - # target_compile_options(${LIB_NAME_CLI} PRIVATE /EHa) - # STRING(REPLACE "/EHsc" "/EHa" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) - # STRING (REGEX REPLACE "[/|-]RTC(su|[1su])" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - # STRING (REGEX REPLACE "[/|-]RTC(su|[1su])" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") - LIST(APPEND INSTALL_TARGET_LIST ${LIB_NAME_CLI}) - - # CSharp program to TestCLI - add_subdirectory(TestCLI) - - # Create an extended version of the library with a simple C++ wrapper class. - SET(LIB_NAME_EXT cbsdk_ext) - ADD_LIBRARY(${LIB_NAME_EXT} SHARED - ${CMAKE_CURRENT_LIST_DIR}/cbsdk_native.h - ${CMAKE_CURRENT_LIST_DIR}/cbsdk_native.cpp - ) - TARGET_INCLUDE_DIRECTORIES(${LIB_NAME_EXT} PRIVATE ${LIB_INCL_DIRS} ${CMAKE_SOURCE_DIR}/wrappers/cbmex) - TARGET_LINK_LIBRARIES(${LIB_NAME_EXT} ${LIB_NAME}) - ADD_DEPENDENCIES(${LIB_NAME_EXT} ${LIB_NAME}) - LIST(APPEND INSTALL_TARGET_LIST ${LIB_NAME_EXT}) - - # CSharp program to test cbsdk_ext - add_subdirectory(TestCSharp) - -ENDIF(${CBSDK_BUILD_CLI}) \ No newline at end of file diff --git a/bindings/cli/README.md b/bindings/cli/README.md deleted file mode 100644 index bc311fe5..00000000 --- a/bindings/cli/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# CereLink C-Sharp Interface - -There are two different interfaces provided: - -* C++/CLI Interface -* C# Interface using P/Invoke - -## Build system - -Requires CMake >= 3.12. -Only tested on Windows VS 2017. -Add the `-DBUILD_CLI=ON` option to the build command described in ../BUILD.md. - -If you followed the instructions in ../BUILD.md then you will have already built the release and this will have generated some errors. -Next, built the INSTALL target: -* `cmake --build . --config Release --target install` - -This will generate at least one `setlocal` error you can ignore. -It will also generate a specific error for the C++/CLI interface. The fix is described below. - -## C++/CLI Interface - -I couldn't figure out how to use CMake to get the C++/CLI test project (in TestCLI folder) to -reference the cbsdk_cli.dll properly. So you'll have to do that manually in the project. - -Under the TestCLI target, delete the cbsdk_cli reference and add a new reference pointing to -dist/lib64/cbsdk_cli.dll - -* Open the build\CBSDK.sln file. -* Change the config from Debug to Release -* Expand the TestCLI target (click on right arrow) -* Expand "References" -* Right click on `cbsdk_cli` and remove it. -* Right click on References and select `Add Reference...` -* In the new dialog window, browse to dist/lib64/cbsdk_cli.dll - -## C# Interface using P/Invoke - -An example is provided in TestCSharp. - -### On Unity - -First copy the dlls and Qt dependencies from CereLink/dist/bin to your Unity project folder. -Add to your assets the CereLink/cli/TestCSharp/CereLink.cs and CereLink/cli/UnityExample/CereLinkInterface.cs -Create a new game object and add CereLinkInterface.cs as a script component. diff --git a/bindings/cli/TestCLI/App.config b/bindings/cli/TestCLI/App.config deleted file mode 100644 index 96f35e94..00000000 --- a/bindings/cli/TestCLI/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/bindings/cli/TestCLI/CMakeLists.txt b/bindings/cli/TestCLI/CMakeLists.txt deleted file mode 100644 index 4ab4cac6..00000000 --- a/bindings/cli/TestCLI/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -cmake_minimum_required(VERSION 3.8) - -project(TestCLI VERSION 0.1.0 LANGUAGES CSharp) - -include(CSharpUtilities) - -add_executable(${PROJECT_NAME} - ${CMAKE_CURRENT_LIST_DIR}/App.config - ${CMAKE_CURRENT_LIST_DIR}/Program.cs - ${CMAKE_CURRENT_LIST_DIR}/Properties/AssemblyInfo.cs) - -#target_link_libraries(${PROJECT_NAME} ${LIB_NAME_CLI}) # If you uncomment this then comment out the VS_DOTNET_REFERENCE_cbsdk_cli below. - -file(TO_NATIVE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../dist/lib64" MY_CLI_DIST_DIR) -set_target_properties( - ${PROJECT_NAME} - PROPERTIES - VS_DOTNET_TARGET_FRAMEWORK_VERSION "v4.6.1" - WIN32_EXECUTABLE TRUE - VS_DOTNET_REFERENCE_cbsdk_cli "${MY_CLI_DIST_DIR}" -) -set_property( - TARGET ${PROJECT_NAME} - PROPERTY - VS_DOTNET_REFERENCES - "System" - "System.Core" - "System.Xml.Linq" - "System.Data.DataSetExtensions" - "Microsoft.CSharp" - "System.Data" - "System.Net.Http" - "System.Xml" -) diff --git a/bindings/cli/TestCLI/Program.cs b/bindings/cli/TestCLI/Program.cs deleted file mode 100644 index 80307bd0..00000000 --- a/bindings/cli/TestCLI/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using CereLink; - -namespace SimpleClient -{ - - class Program - { - static void Main(string[] args) - { - UInt32 nInstance = 0; - int inPort = 51002; - int outPort = 51001; - int bufsize = 8 * 1024 * 1024; - string inIP = "127.0.0.1"; - string outIP = ""; - bool use_double = false; - - CbSdkNative managed = new CbSdkNative(nInstance, inPort, outPort, bufsize, inIP, outIP, use_double); - Console.WriteLine("C# - managed.GetIsOnline(): {0}", managed.GetIsOnline()); - if (managed.GetIsOnline()) - { - for (int i = 0; i < 5; i++) - { - UInt16 nChans = managed.Fetch(); - Console.WriteLine("C# - managed.Fetch() {0} - fetched {1} channels.", i, nChans); - System.Threading.Thread.Sleep(11); - if (managed.GetIsDouble()) - { - for (UInt16 chan_idx = 0; chan_idx < nChans; chan_idx++) - { - Double[] arr = managed.GetDataDbl(chan_idx); - Console.WriteLine("C# - Channel {0} ({1} samples): [{2} ... {3}]", chan_idx, arr.Length, arr[0], arr[arr.Length - 1]); - } - } - else - { - for (UInt16 chan_idx = 0; chan_idx < nChans; chan_idx++) - { - Int16[] arr = managed.GetDataInt(chan_idx); - Console.WriteLine("C# - Channel {0} ({1} samples): [{2} ... {3}]", chan_idx, arr.Length, arr[0], arr[arr.Length - 1]); - } - } - } - - } - } - } -} \ No newline at end of file diff --git a/bindings/cli/TestCLI/Properties/AssemblyInfo.cs b/bindings/cli/TestCLI/Properties/AssemblyInfo.cs deleted file mode 100644 index 7e7769f8..00000000 --- a/bindings/cli/TestCLI/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("TestCLI")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("TestCLI")] -[assembly: AssemblyCopyright("Copyright © 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("349769a5-261c-4234-bc48-f558817438f0")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/bindings/cli/TestCSharp/App.config b/bindings/cli/TestCSharp/App.config deleted file mode 100644 index 96f35e94..00000000 --- a/bindings/cli/TestCSharp/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/bindings/cli/TestCSharp/CMakeLists.txt b/bindings/cli/TestCSharp/CMakeLists.txt deleted file mode 100644 index 755466a2..00000000 --- a/bindings/cli/TestCSharp/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -cmake_minimum_required(VERSION 3.8) - -project(TestCSharp VERSION 0.1.0 LANGUAGES CSharp) - -include(CSharpUtilities) - -add_executable(${PROJECT_NAME} - ${CMAKE_CURRENT_LIST_DIR}/App.config - ${CMAKE_CURRENT_LIST_DIR}/Program.cs - ${CMAKE_CURRENT_LIST_DIR}/CereLink.cs - ${CMAKE_CURRENT_LIST_DIR}/Properties/AssemblyInfo.cs) - -target_link_libraries(${PROJECT_NAME} ${LIB_NAME_EXT}) - -file(TO_NATIVE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../dist/bin" MY_CLI_DIST_DIR) -set_target_properties( - ${PROJECT_NAME} - PROPERTIES - VS_DOTNET_TARGET_FRAMEWORK_VERSION "v4.6.1" - WIN32_EXECUTABLE TRUE -) -set_property( - TARGET ${PROJECT_NAME} - PROPERTY - VS_DOTNET_REFERENCES - "System" - "System.Core" - "System.Xml.Linq" - "System.Data.DataSetExtensions" - "Microsoft.CSharp" - "System.Data" - "System.Net.Http" - "System.Xml" -) diff --git a/bindings/cli/TestCSharp/CereLink.cs b/bindings/cli/TestCSharp/CereLink.cs deleted file mode 100644 index 0e5f67da..00000000 --- a/bindings/cli/TestCSharp/CereLink.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Runtime.InteropServices; // needed for Marshal - -namespace CereLink -{ - static class Constants - { - /// The default number of continuous samples that will be stored per channel in the trial buffer - public const UInt32 cbSdk_CONTINUOUS_DATA_SAMPLES = 102400; // multiple of 4096 - - /// The default number of events that will be stored per channel in the trial buffer - public const UInt32 cbSdk_EVENT_DATA_SAMPLES = (2 * 8192); // multiple of 4096 - - /// Maximum file size (in bytes) that is allowed to upload to NSP - public const UInt32 cbSdk_MAX_UPOLOAD_SIZE = (1024 * 1024 * 1024); - - /// \todo these should become functions as we may introduce different instruments - public const double cbSdk_TICKS_PER_SECOND = 30000.0; - - /// The number of seconds corresponding to one cb clock tick - public const double cbSdk_SECONDS_PER_TICK = (1.0 / cbSdk_TICKS_PER_SECOND); - - public const UInt16 cbNUM_FE_CHANS = 256; - public const UInt16 cbNUM_ANAIN_CHANS = 16; - public const UInt16 cbNUM_ANALOG_CHANS = (cbNUM_FE_CHANS + cbNUM_ANAIN_CHANS); - public const UInt16 cbNUM_ANAOUT_CHANS = 4; - public const UInt16 cbNUM_AUDOUT_CHANS = 2; - public const UInt16 cbNUM_ANALOGOUT_CHANS = (cbNUM_ANAOUT_CHANS + cbNUM_AUDOUT_CHANS); - public const UInt16 cbNUM_DIGIN_CHANS = 1; - public const UInt16 cbNUM_SERIAL_CHANS = 1; - public const UInt16 cbNUM_DIGOUT_CHANS = 4; - public const UInt32 cbMAXCHANS = (cbNUM_ANALOG_CHANS + cbNUM_ANALOGOUT_CHANS + cbNUM_DIGIN_CHANS + cbNUM_SERIAL_CHANS + cbNUM_DIGOUT_CHANS); - public const UInt16 cbMAXUNITS = 5; - // MAX_CHANS_DIGITAL_IN = (cbNUM_ANALOG_CHANS + cbNUM_ANALOGOUT_CHANS + cbNUM_DIGIN_CHANS) - // MAX_CHANS_SERIAL = (MAX_CHANS_DIGITAL_IN + cbNUM_SERIAL_CHANS) - public const UInt16 cbMAXTRACKOBJ = 20; // maximum number of trackable objects - public const UInt16 cbMAXHOOPS = 4; - public const UInt16 cbPKT_SPKCACHEPKTCNT = 400; - public const UInt16 cbMAX_PNTS = 128; // make large enough to track longest possible spike width in samples - } - - public class CereLinkConnection - { - [DllImport("cbsdk_ext")] - private static extern IntPtr CbSdkNative_Create(UInt32 nInstance, int inPort, int outPort, int bufsize, String inIP, String outIP, bool use_double); - - [DllImport("cbsdk_ext")] - private static extern bool CbSdkNative_GetIsDouble(IntPtr pCbSdk); - - [DllImport("cbsdk_ext")] - private static extern bool CbSdkNative_GetIsOnline(IntPtr pCbSdk); - - [DllImport("cbsdk_ext")] - private static extern void CbSdkNative_SetComment(IntPtr pCbSdk, String comment, byte red, byte green, byte blue, int charset); - - [DllImport("cbsdk_ext")] - private static extern void CbSdkNative_PrefetchData(IntPtr pCbSdk, ref UInt16 chan_count, UInt32[] samps_per_chan, UInt16[] chan_numbers); - - [DllImport("cbsdk_ext")] - private static extern void CbSdkNative_TransferData(IntPtr pCbSdk, IntPtr arr, ref UInt64 timestamp); - - [DllImport("cbsdk_ext")] - private static extern void CbSdkNative_Delete(IntPtr value); - - [DllImport("cbsdk_ext")] - private static extern bool CbSdkNative_SetFileStorage(IntPtr pCbSdk, String file_name, String file_comment, bool bStart); - - [DllImport("cbsdk_ext")] - private static extern bool CbSdkNative_SetPatientInfo(IntPtr pCbSdk, String ID, String f_name, String l_name, UInt32 DOB_month, UInt32 DOB_day, UInt32 DOB_year); - - [DllImport("cbsdk_ext")] - private static extern bool CbSdkNative_GetIsRecording(IntPtr pCbSdk); - - - private IntPtr pNative; - - public CereLinkConnection(UInt32 nInstance, int inPort, int outPort, int bufsize, String inIP, String outIP, bool useDouble) - { - pNative = CbSdkNative_Create(nInstance, inPort, outPort, bufsize, inIP, outIP, useDouble); - } - - public bool IsOnline() - { - return CbSdkNative_GetIsOnline(pNative); - } - - public bool IsDouble() - { - return CbSdkNative_GetIsDouble(pNative); - } - - // charset: (0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI) - public void SetComment(String comment, byte red, byte green, byte blue, int charset) - { - CbSdkNative_SetComment(pNative, comment, red, green, blue, charset); - } - - // Should be running << 1ms. - public void FetchData(out double[][] data) - { - UInt16 chan_count = 0; - UInt32[] samps_per_chan = new UInt32[Constants.cbNUM_ANALOG_CHANS]; - UInt16[] chan_numbers = new UInt16[Constants.cbNUM_ANALOG_CHANS]; - CbSdkNative_PrefetchData(pNative, ref chan_count, samps_per_chan, chan_numbers); - UInt64 timestamp = 0; - - data = new double[chan_count][]; - - // Garbage collector handles and their pinned ptrs - GCHandle[] gchandles = new GCHandle[chan_count]; - IntPtr[] gcptrs = new IntPtr[chan_count]; - - if (chan_count > 0) - { - for (int i = 0; i < chan_count; i++) - { - data[i] = new double[samps_per_chan[i]]; - gchandles[i] = GCHandle.Alloc(data[i], GCHandleType.Pinned); - gcptrs[i] = gchandles[i].AddrOfPinnedObject(); - } - - // handle/ptr of pinned IntPtr[] - GCHandle arrH = GCHandle.Alloc(gcptrs, GCHandleType.Pinned); - IntPtr arr = arrH.AddrOfPinnedObject(); - - CbSdkNative_TransferData(pNative, arr, ref timestamp); - - // Unpin the pointers - foreach (GCHandle han in gchandles) - { - han.Free(); - } - arrH.Free(); - - } - else - { - data = null; - } - } - - public void FetchData(out Int16[][] data) - { - UInt16 chan_count = 0; - UInt32[] samps_per_chan = new UInt32[Constants.cbNUM_ANALOG_CHANS]; - UInt16[] chan_numbers = new UInt16[Constants.cbNUM_ANALOG_CHANS]; - CbSdkNative_PrefetchData(pNative, ref chan_count, samps_per_chan, chan_numbers); - UInt64 timestamp = 0; - - data = new short[chan_count][]; - - // Garbage collector handles and their pinned ptrs - GCHandle[] gchandles = new GCHandle[chan_count]; - IntPtr[] gcptrs = new IntPtr[chan_count]; - - if (chan_count > 0) - { - for (int i = 0; i < chan_count; i++) - { - data[i] = new short[samps_per_chan[i]]; - gchandles[i] = GCHandle.Alloc(data[i], GCHandleType.Pinned); - gcptrs[i] = gchandles[i].AddrOfPinnedObject(); - } - - // handle/ptr of pinned IntPtr[] - GCHandle arrH = GCHandle.Alloc(gcptrs, GCHandleType.Pinned); - IntPtr arr = arrH.AddrOfPinnedObject(); - - CbSdkNative_TransferData(pNative, arr, ref timestamp); - - // Unpin the pointers - foreach (GCHandle han in gchandles) - { - han.Free(); - } - arrH.Free(); - - } - else - { - data = null; - } - } - - public bool SetFileStorage(string file_name, string file_comment, bool bStart) - { - return CbSdkNative_SetFileStorage(pNative, file_name, file_comment, bStart); - } - - public void SetPatientInfo(string ID, string f_name, string l_name, UInt32 DOB_month, UInt32 DOB_day, UInt32 DOB_year) - { - CbSdkNative_SetPatientInfo(pNative, ID, f_name, l_name, DOB_month, DOB_day, DOB_year); - } - - public bool IsRecording() - { - return CbSdkNative_GetIsRecording(pNative); - } - - public void Clear() - { - CbSdkNative_Delete(pNative); - pNative = IntPtr.Zero; - } - - ~CereLinkConnection() - { - Clear(); - } - } -} \ No newline at end of file diff --git a/bindings/cli/TestCSharp/Program.cs b/bindings/cli/TestCSharp/Program.cs deleted file mode 100644 index 76212e28..00000000 --- a/bindings/cli/TestCSharp/Program.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using CereLink; - -namespace SimpleClient -{ - - class Program - { - static void Main(string[] args) - { - UInt32 nInstance = 0; - int inPort = 51002; - int outPort = 51001; - int bufsize = 8 * 1024 * 1024; - string inIP = "127.0.0.1"; - string outIP = ""; - bool useDouble = false; - // bool use_double = false; - - CereLinkConnection conn = new CereLinkConnection(nInstance, inPort, outPort, bufsize, inIP, outIP, useDouble); - if (conn.IsOnline()) - { - for (int fetch_ix = 0; fetch_ix < 5; fetch_ix++) - { - conn.SetComment("TestCSharp fetch", 255, 0, 0, 0); - conn.FetchData(out Int16[][] result); - Console.WriteLine("Returned {0} chans.", result.Length); - for (int chan_ix = 0; chan_ix < result.Length; chan_ix++) - { - Console.WriteLine("Chan {0} has {1} samples: [{2} ... {3}]", - chan_ix, result[chan_ix].Length, result[chan_ix][0], result[chan_ix][result[chan_ix].Length - 1]); - } - System.Threading.Thread.Sleep(11); - } - } - else - { - Console.WriteLine("Not online."); - } - } - } -} \ No newline at end of file diff --git a/bindings/cli/TestCSharp/Properties/AssemblyInfo.cs b/bindings/cli/TestCSharp/Properties/AssemblyInfo.cs deleted file mode 100644 index 7e7769f8..00000000 --- a/bindings/cli/TestCSharp/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("TestCLI")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("TestCLI")] -[assembly: AssemblyCopyright("Copyright © 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("349769a5-261c-4234-bc48-f558817438f0")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/bindings/cli/UnityExample/CereLinkInterface.cs b/bindings/cli/UnityExample/CereLinkInterface.cs deleted file mode 100644 index 85c46a0d..00000000 --- a/bindings/cli/UnityExample/CereLinkInterface.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using CereLink; - -public class CereLinkInterface : MonoBehaviour { - - public uint nInstance = 0; - public int inPort = 51002; - public int outPort = 51001; - public int bufsize = 8 * 1024 * 1024; - public string inIP = "127.0.0.1"; - public string outIP = ""; - public bool useDouble = false; - - private CereLinkConnection conn; - - // Use this for initialization - void Start () { - conn = new CereLinkConnection(nInstance, inPort, outPort, bufsize, inIP, outIP, useDouble); - - } - - // Update is called once per frame - void Update () { - if (conn.IsOnline()) - { - conn.FetchData(out short[][] result); - Debug.Log(string.Format("Returned {0} chans.", result.Length)); - for (int chan_ix = 0; chan_ix < result.Length; chan_ix++) - { - Debug.Log(string.Format("Chan {0} has {1} samples: [{2} ... {3}]", - chan_ix, result[chan_ix].Length, result[chan_ix][0], result[chan_ix][result[chan_ix].Length - 1])); - } - } - } -} diff --git a/bindings/cli/cbsdk_native.cpp b/bindings/cli/cbsdk_native.cpp deleted file mode 100644 index df7370bc..00000000 --- a/bindings/cli/cbsdk_native.cpp +++ /dev/null @@ -1,255 +0,0 @@ -#include "cbsdk_native.h" -#include -#include - - -struct my_cbSdkTrialEvent : public cbSdkTrialEvent {}; -struct my_cbSdkTrialCont : public cbSdkTrialCont {}; -struct my_cbSdkTrialComment : public cbSdkTrialComment {}; - - -CbSdkNative::CbSdkNative(uint32_t nInstance, int inPort, int outPort, int bufsize, std::string inIP, std::string outIP, bool use_double) - :p_trialEvent(new my_cbSdkTrialEvent()), p_trialCont(new my_cbSdkTrialCont), p_trialComment(new my_cbSdkTrialComment) -{ - m_instance = nInstance; - cbSdkConnection con = cbSdkConnection(); - con.nInPort = inPort; - con.nOutPort = outPort; - con.nRecBufSize = bufsize; - con.szInIP = inIP.c_str(); - con.szOutIP = outIP.c_str(); - cbSdkResult res = cbSdkOpen(m_instance, CBSDKCONNECTION_DEFAULT, con); - isOnline = res == CBSDKRESULT_SUCCESS; - if (isOnline) - { - cbSdkVersion ver = cbSdkVersion(); - cbSdkGetVersion(m_instance, &ver); - // TODO: Construct version string and store as member variable. - - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - cbSdkInstrumentType instType = CBSDKINSTRUMENT_COUNT; - res = cbSdkGetType(m_instance, &conType, &instType); - - cfg = FetchTrialConfig(); - cfg.bActive = 1; - cfg.bDouble = use_double; - cfg.uWaveforms = 0; - cfg.uConts = cbSdk_CONTINUOUS_DATA_SAMPLES; - cfg.uEvents = 0; // cbSdk_EVENT_DATA_SAMPLES; - cfg.uComments = 0; - cfg.uTrackings = 0; - cfg.bAbsolute = true; - PushTrialConfig(cfg); - } -} - -CbSdkNative::~CbSdkNative() -{ - cbSdkResult res = cbSdkClose(m_instance); -} - -void CbSdkNative::PushTrialConfig(CbSdkConfigParam config) -{ - cfg.bActive = config.bActive; - cfg.Begchan = config.Begchan; - cfg.Begmask = config.Begmask; - cfg.Begval = config.Begval; - cfg.Endchan = config.Endchan; - cfg.Endmask = config.Endmask; - cfg.Endval = config.Endval; - cfg.bDouble = config.bDouble; - cfg.uWaveforms = config.uWaveforms; - cfg.uConts = config.uConts; - cfg.uEvents = config.uEvents; - cfg.uComments = config.uComments; - cfg.uTrackings = config.uTrackings; - cfg.bAbsolute = config.bAbsolute; - cbSdkResult res = cbSdkSetTrialConfig(m_instance, cfg.bActive, cfg.Begchan, cfg.Begmask, cfg.Begval, - cfg.Endchan, cfg.Endmask, cfg.Endval, cfg.bDouble, cfg.uWaveforms, cfg.uConts, cfg.uEvents, - cfg.uComments, cfg.uTrackings, cfg.bAbsolute); -} - -bool CbSdkNative::GetIsDouble() -{ - return cfg.bDouble; -} - -void CbSdkNative::SetComment(std::string comment, uint32_t t_bgr, uint8_t charset) -{ - //uint32_t t_bgr = (transparency << 24) + (blue << 16) + (green << 8) + red; - //uint8_t charset = 0 // Character set(0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI); - cbSdkResult res = cbSdkSetComment(m_instance, t_bgr, charset, comment.c_str()); -} - -/* -bool CbSdkNative::GetComment() -{ - cbSdkResult sdkres = cbSdkInitTrialData(m_instance, 1, 0, 0, p_trialComment.get(), 0); - return (sdkres == CBSDKRESULT_SUCCESS); -} -*/ - -CbSdkNative::CbSdkConfigParam CbSdkNative::FetchTrialConfig() -{ - CbSdkConfigParam config; - cbSdkResult res = cbSdkGetTrialConfig(m_instance, &config.bActive, &config.Begchan, &config.Begmask, &config.Begval, - &config.Endchan, &config.Endmask, &config.Endval, &config.bDouble, &config.uWaveforms, &config.uConts, &config.uEvents, - &config.uComments, &config.uTrackings, &config.bAbsolute); - return config; -} - -void CbSdkNative::PrefetchData(uint16_t &chan_count, uint32_t* samps_per_chan, uint16_t* chan_numbers) -{ - // Determine the current number of samples. - cbSdkResult sdkres = cbSdkInitTrialData(m_instance, 1, 0, p_trialCont.get(), 0, 0); // Put p_trialEvent.get() back - chan_count = p_trialCont->count; - for (int chan_ix = 0; chan_ix < chan_count; chan_ix++) - { - samps_per_chan[chan_ix] = p_trialCont->num_samples[chan_ix]; - chan_numbers[chan_ix] = p_trialCont->chan[chan_ix]; - } -} - -// Receives a pointer to an array of pointers from C# to copy data to. -// We just need to pass the pointer to p_trialCont->samples. -// Whether the data are double or int16 will be determined in C#. -void CbSdkNative::TransferData(void** buffer, PROCTIME* timestamp) -{ - if (timestamp != NULL) *timestamp = p_trialCont->time; - if (p_trialCont->count > 0) - { - for (int chan_ix = 0; chan_ix < p_trialCont->count; chan_ix++) - { - if (cfg.bDouble) - { - p_trialCont->samples[chan_ix] = (double*)buffer[chan_ix]; - } - else - { - p_trialCont->samples[chan_ix] = (int16_t*)buffer[chan_ix]; - } - } - cbSdkResult sdkres = cbSdkGetTrialData(m_instance, 1, 0, p_trialCont.get(), 0, 0); // p_trialEvent.get() - } -} - -// Added by GD 2019-08-20. -bool CbSdkNative::SetFileStorage(char* file_name, char* file_comment, bool* bStart) -{ - // For some reason the script accepts an integer instead of a boolean. - uint32_t iStart = bStart?1:0; - - cbSdkResult res = cbSdkSetFileConfig(m_instance, file_name, file_comment, iStart); - return (res == CBSDKRESULT_SUCCESS); -} - -void CbSdkNative::SetPatientInfo(char* ID, char* f_name, char* l_name, uint32_t DOB_month, uint32_t DOB_day, uint32_t DOB_year ) -{ - cbSdkResult res = cbSdkSetPatientInfo(m_instance, ID, f_name, l_name, DOB_month, DOB_day, DOB_year); -} - -bool CbSdkNative::GetIsRecording() -{ - bool bIsRecording = false; - char filename[256]; // Could be added to the return. - char username[256]; - cbSdkResult res = cbSdkGetFileConfig(m_instance, filename, username, &bIsRecording); - if (res == CBSDKRESULT_SUCCESS) - return bIsRecording; - else - return false; -} - - - -CbSdkNative* CbSdkNative_Create(uint32_t nInstance, int inPort, int outPort, int bufsize, const char* inIP, const char* outIP, bool use_double) -{ - //inIP and outIP get auto-cast to std::string - return new CbSdkNative(nInstance, inPort, outPort, bufsize, inIP, outIP, use_double); -} - -bool CbSdkNative_GetIsDouble(CbSdkNative* pCbSdk) { - if (pCbSdk != NULL) - return pCbSdk->GetIsDouble(); - else - return false; -} - -bool CbSdkNative_GetIsOnline(CbSdkNative* pCbSdk) -{ - if (pCbSdk != NULL) - return pCbSdk->isOnline; - else - return false; -} - -//rgba: (red << 24) + (green << 16) + (blue << 8) + alpha -//charset: 0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI -void CbSdkNative_SetComment(CbSdkNative* pCbSdk, const char* comment, uint8_t red, uint8_t green, uint8_t blue, uint8_t charset) -{ - uint32_t t_bgr = (blue << 16) + (green << 8) + (red); - if (pCbSdk != NULL) - pCbSdk->SetComment(comment, t_bgr, charset); -} - -/* -bool CbSdkNative_GetComment(CbSdkNative* pCbSdk) -{ - if (pCbSdk != NULL) - return pCbSdk->GetComment(); - else - return false; -} -*/ - -void CbSdkNative_PrefetchData(CbSdkNative* pCbSdk, uint16_t &chan_count, uint32_t* samps_per_chan, uint16_t* chan_numbers) -{ - if (pCbSdk != NULL) - { - pCbSdk->PrefetchData(chan_count, samps_per_chan, chan_numbers); - std::cout << "pCbSdk->PrefetchData(chan_count, samps_per_chan, chan_numbers) returned." << std::endl; - } - -} - -void CbSdkNative_TransferData(CbSdkNative* pCbSdk, void** buffer, PROCTIME* timestamp) -{ - if (pCbSdk != NULL) - pCbSdk->TransferData(buffer, timestamp); -} - -void CbSdkNative_Delete(CbSdkNative* pCbSdk) { - if (pCbSdk != NULL) - { - delete pCbSdk; - pCbSdk = NULL; - } -} - - -// To set the File Storage settings of the Central suite. -// return true if OK; False if error; -bool CbSdkNative_SetFileStorage(CbSdkNative* pCbSdk, char* file_name, char* file_comment, bool* bStart) -{ - if (pCbSdk != NULL) - return pCbSdk->SetFileStorage(file_name, file_comment, bStart); - else - return false; - -} - -void CbSdkNative_SetPatientInfo(CbSdkNative* pCbSdk, char* patient_ID, char* first_name, char* last_name, uint32_t DOB_month, uint32_t DOB_day, uint32_t DOB_year) -{ - if (pCbSdk != NULL) - pCbSdk->SetPatientInfo(patient_ID, first_name, last_name, DOB_month, DOB_day, DOB_year); - -} - -bool CbSdkNative_GetIsRecording(CbSdkNative* pCbSdk) -{ - if (pCbSdk != NULL) - return pCbSdk->GetIsRecording(); - else - return false; -} - diff --git a/bindings/cli/cbsdk_native.h b/bindings/cli/cbsdk_native.h deleted file mode 100644 index 4b5fdd9e..00000000 --- a/bindings/cli/cbsdk_native.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once -#include -#include -#include -#include - -// Incomplete types. Come from cbsdk.h -struct my_cbSdkTrialEvent; -struct my_cbSdkTrialCont; -struct my_cbSdkTrialComment; - -class CbSdkNative { -public: - - struct CbSdkConfigParam - { - uint32_t bActive; // Start if true, stop otherwise - uint16_t Begchan; // Start channel - uint32_t Begmask; // Mask for start value - uint32_t Begval; // Start value - uint16_t Endchan; // Last channel - uint32_t Endmask; // Mask for end value - uint32_t Endval; // End value - bool bDouble; // If data array is double precision - uint32_t uWaveforms; // Number of spike waveform to buffer - uint32_t uConts; // Number of continuous data to buffer - uint32_t uEvents; // Number of events to buffer - uint32_t uComments; // Number of comments to buffer - uint32_t uTrackings; // Number of tracking data to buffer - bool bAbsolute; // If event timing is absolute or relative to trial - }; - - bool isOnline = false; - - CbSdkNative(uint32_t nInstance, int inPort= 51002, int outPort= 51001, int bufsize= 8 * 1024 * 1024, - std::string inIP="", std::string outIP="", bool use_double=false); - ~CbSdkNative(); - bool GetIsDouble(); - void SetComment(std::string comment="", uint32_t t_bgr=255, uint8_t charset=0); - //bool GetComment(); - void PrefetchData(uint16_t &chan_count, uint32_t* samps_per_chan, uint16_t* chan_numbers); - void TransferData(void** buffer, PROCTIME* timestamp = NULL); - bool SetFileStorage(char* file_name, char* file_comment, bool* bStart); - void SetPatientInfo(char* ID, char* f_name, char* l_name, uint32_t DOB_month, uint32_t DOB_day, uint32_t DOB_year); - bool GetIsRecording(); - -private: - int32_t m_instance; - CbSdkConfigParam cfg; - - // forward declarations of structures from cbsdk.h - std::unique_ptr p_trialEvent; - std::unique_ptr p_trialCont; - std::unique_ptr p_trialComment; - - // Data vectors to store the result of TransferData. - std::array, 256 + 16> dblData; - std::array, 256 + 16> intData; - - CbSdkConfigParam FetchTrialConfig(); - void PushTrialConfig(CbSdkConfigParam config); -}; - - -extern "C" -{ - __declspec(dllexport) CbSdkNative* CbSdkNative_Create(uint32_t nInstance, int inPort, int outPort, int bufsize, const char* inIP, const char* outIP, bool use_double); - __declspec(dllexport) bool CbSdkNative_GetIsDouble(CbSdkNative* pCbSdk); - __declspec(dllexport) bool CbSdkNative_GetIsOnline(CbSdkNative* pCbSdk); - //__declspec(dllexport) bool CbSdkNative_GetComment(CbSdkNative* pCbSdk); - __declspec(dllexport) void CbSdkNative_SetComment(CbSdkNative* pCbSdk, const char* comment, uint8_t red, uint8_t green, uint8_t blue, uint8_t charset); - __declspec(dllexport) void CbSdkNative_PrefetchData(CbSdkNative* pCbSdk, uint16_t &chan_count, uint32_t* samps_per_chan, uint16_t* chan_numbers); - __declspec(dllexport) void CbSdkNative_TransferData(CbSdkNative* pCbSdk, void** buffer, PROCTIME* timestamp = NULL); - __declspec(dllexport) void CbSdkNative_Delete(CbSdkNative* pCbSdk); - __declspec(dllexport) bool CbSdkNative_SetFileStorage(CbSdkNative* pCbSdk, char* file_name, char* file_comment, bool* bStart); - __declspec(dllexport) void CbSdkNative_SetPatientInfo(CbSdkNative* pCbSdk, char* ID, char* f_name, char* l_name, uint32_t DOB_month, uint32_t DOB_day, uint32_t DOB_year ); - __declspec(dllexport) bool CbSdkNative_GetIsRecording(CbSdkNative* pCbSdk); -} \ No newline at end of file diff --git a/docs/central_shared_memory_layout.md b/docs/central_shared_memory_layout.md new file mode 100644 index 00000000..32bf7a73 --- /dev/null +++ b/docs/central_shared_memory_layout.md @@ -0,0 +1,302 @@ +# Central-Suite Shared Memory Layout + +This document describes the shared memory layout used by the upstream Central-Suite application +(the proprietary Windows application from Blackrock Neurotech). CereLink may need to interoperate +with this layout when running as a CLIENT attached to Central's shared memory. + +Source of truth: `Central-Suite/cbhwlib/cbhwlib.h` and `Central-Suite/cbhwlib/cbhwlib.cpp` + +## Key Concepts: Instance vs Instrument + +Central has **two independent indexing dimensions** for multi-device support: + +### Instance ID (`nInstance`) + +- **Range**: 0 to `cbMAXOPEN - 1` (0 to 3) +- **Purpose**: Selects which **set of shared memory segments** to use. Each instance is a + completely independent set of all 7 segments. This allows multiple Central applications + (or multiple CereLink sessions) to run simultaneously. +- **Set via**: `--instance` command-line parameter for Central, or first argument to `cbSdkOpen(nInstance, ...)` +- **Effect on naming**: Instance 0 uses bare names (`"cbRECbuffer"`). Instance N>0 appends + the number (`"cbRECbuffer1"`, `"cbRECbuffer2"`, etc.) + +### Instrument Number (`nInstrument`) + +- **Range**: 1 to `cbMAXPROCS` (1-based; `cbNSP1 = 1`) + - For Central: `cbMAXPROCS = 4` (supports up to 4 NSPs) + - For NSP firmware: `cbMAXPROCS = 1` +- **Purpose**: Identifies a **physical NSP** within a single instance's shared memory. The + `cbCFGBUFF` config buffer has arrays dimensioned by `cbMAXPROCS` to hold per-instrument data. +- **In packet headers**: Stored as **0-based** in `cbPKT_HEADER.instrument` (so instrument 1 is + stored as 0). Functions that take `nInstrument` are 1-based and subtract 1 for indexing. + +### Relationship + +``` +nInstance (0-3) Selects WHICH shared memory set + | + +-- Instance 0 --> "cbRECbuffer", "cbCFGbuffer", ... + | +-- cbCFGBUFF.procinfo[0..3] <-- nInstrument 1-4 + | +-- cbCFGBUFF.chaninfo[0..879] <-- global channel numbers + | + +-- Instance 1 --> "cbRECbuffer1", "cbCFGbuffer1", ... + | +-- (same structure, independent data) + : +``` + +### The `cb_library_index` Indirection + +There is an indirection layer between `nInstance` and the internal buffer slot `nIdx`: + +```c +UINT32 cb_library_index[cbMAXOPEN]; // maps nInstance -> nIdx +UINT32 cb_library_initialized[cbMAXOPEN]; + +// In cbOpen(): +// 1. Find first uninitialized slot -> nIdx +// 2. cb_library_index[nInstance] = nIdx +// 3. Create/open shared memory at slot nIdx +``` + +Every buffer access goes through this indirection: +```c +UINT32 nIdx = cb_library_index[nInstance]; +cbRECBUFF* rec = cb_rec_buffer_ptr[nIdx]; +``` + +## Shared Memory Segments + +Central creates **7 named shared memory segments** per instance. + +### Naming Convention + +| Segment | Base Name | Instance 0 | Instance N (N>0) | +|-----------------|---------------------|---------------------|----------------------| +| Config buffer | `cbCFGbuffer` | `cbCFGbuffer` | `cbCFGbufferN` | +| Receive buffer | `cbRECbuffer` | `cbRECbuffer` | `cbRECbufferN` | +| Global transmit | `XmtGlobal` | `XmtGlobal` | `XmtGlobalN` | +| Local transmit | `XmtLocal` | `XmtLocal` | `XmtLocalN` | +| PC status | `cbSTATUSbuffer` | `cbSTATUSbuffer` | `cbSTATUSbufferN` | +| Spike cache | `cbSPKbuffer` | `cbSPKbuffer` | `cbSPKbufferN` | +| Signal event | `cbSIGNALevent` | `cbSIGNALevent` | `cbSIGNALeventN` | +| System mutex | `cbSharedDataMutex` | `cbSharedDataMutex` | `cbSharedDataMutexN` | + +Implementation: `cbhwlib.cpp` lines 271-399 (`cbOpen()`) and lines 3744-3904 (`CreateSharedObjects()`) + +### Segment Details + +#### 1. cbCFGBUFF (Configuration Buffer) + +The largest and most complex structure. Contains cached configuration for the entire system. + +```c +// cbhwlib.h line 1121 +typedef struct { + UINT32 version; + UINT32 sysflags; + cbOPTIONTABLE optiontable; // 32 uint32 values + cbCOLORTABLE colortable; // 96 uint32 values + cbPKT_SYSINFO sysinfo; + cbPKT_PROCINFO procinfo[cbMAXPROCS]; // [4] per-instrument + cbPKT_BANKINFO bankinfo[cbMAXPROCS][cbMAXBANKS]; // [4][30] + cbPKT_GROUPINFO groupinfo[cbMAXPROCS][cbMAXGROUPS]; // [4][8] + cbPKT_FILTINFO filtinfo[cbMAXPROCS][cbMAXFILTS]; // [4][32] + cbPKT_ADAPTFILTINFO adaptinfo[cbMAXPROCS]; // [4] + cbPKT_REFELECFILTINFO refelecinfo[cbMAXPROCS]; // [4] + cbPKT_CHANINFO chaninfo[cbMAXCHANS]; // [880] all channels + cbSPIKE_SORTING isSortingOptions; // spike sorting state + cbPKT_NTRODEINFO isNTrodeInfo[cbMAXNTRODES]; // ntrode config + cbPKT_AOUT_WAVEFORM isWaveform[AOUT_NUM_GAIN_CHANS][cbMAX_AOUT_TRIGGER]; + cbPKT_LNC isLnc[cbMAXPROCS]; // [4] line noise cancellation + cbPKT_NPLAY isNPlay; // nPlay info + cbVIDEOSOURCE isVideoSource[cbMAXVIDEOSOURCE]; // video sources + cbTRACKOBJ isTrackObj[cbMAXTRACKOBJ]; // trackable objects + cbPKT_FILECFG fileinfo; // file recording status + HANDLE hwndCentral; // Central window handle (32 or 64 bit) +} cbCFGBUFF; +``` + +**Key points**: +- `optiontable` and `colortable` come **before** `sysinfo` +- `isLnc[4]` comes **after** `isWaveform` +- `hwndCentral` is last and its size depends on 32-bit vs 64-bit build +- Arrays indexed by `cbMAXPROCS` hold per-instrument data (up to 4 NSPs) +- `chaninfo[cbMAXCHANS]` uses global channel numbers across all instruments + +**Channel count (`cbMAXCHANS`)** for Central (`cbMAXPROCS=4`, `cbNUM_FE_CHANS=768`): + +| Type | Count | Formula | +|----------------|---------|-------------------| +| Front-end | 768 | `cbNUM_FE_CHANS` | +| Analog input | 64 | `16 * cbMAXPROCS` | +| Analog output | 16 | `4 * cbMAXPROCS` | +| Audio output | 8 | `2 * cbMAXPROCS` | +| Digital input | 4 | `1 * cbMAXPROCS` | +| Serial | 4 | `1 * cbMAXPROCS` | +| Digital output | 16 | `4 * cbMAXPROCS` | +| **Total** | **880** | `cbMAXCHANS` | + +#### 2. cbRECBUFF (Receive Ring Buffer) + +```c +// cbhwlib.h line 975 +#define cbRECBUFFLEN cbNUM_FE_CHANS * 65536 * 4 - 1 + +typedef struct { + UINT32 received; // total packets received + PROCTIME lasttime; // timestamp of last packet + UINT32 headwrap; // wrap counter for head + UINT32 headindex; // current write position + UINT32 buffer[cbRECBUFFLEN]; // ring buffer data +} cbRECBUFF; +``` + +**Size**: For Central (`cbNUM_FE_CHANS=768`): 201,326,591 uint32 words = **~768 MB** + +- STANDALONE writes packets here, advances `headindex` +- CLIENT reads from `tailindex` to `headindex` (CLIENT tracks its own tail) +- Wrap-around tracked via `headwrap` counter + +#### 3. cbXMTBUFF (Transmit Ring Buffer) + +```c +// cbhwlib.h line 991 +typedef struct { + UINT32 transmitted; // packets sent count + UINT32 headindex; // first empty position + UINT32 tailindex; // one past last emptied position + UINT32 last_valid_index; // max valid index + UINT32 bufferlen; // buffer length in uint32 units + UINT32 buffer[0]; // flexible array member +} cbXMTBUFF; +``` + +Used for both `XmtGlobal` (packets sent to device) and `XmtLocal` (IPC-only packets). +The `buffer` is a **flexible array member** -- actual size is set at creation time. + +- `XmtGlobal`: 5000 max-sized packet slots +- `XmtLocal`: 2000 max-sized packet slots + +#### 4. cbPcStatus (PC Status) + +```cpp +// cbhwlib.h line 1048 +class cbPcStatus { +public: + cbPKT_UNIT_SELECTION isSelection[cbMAXPROCS]; // [4] +private: + INT32 m_iBlockRecording; + UINT32 m_nPCStatusFlags; + UINT32 m_nNumFEChans; + UINT32 m_nNumAnainChans; + UINT32 m_nNumAnalogChans; + UINT32 m_nNumAoutChans; + UINT32 m_nNumAudioChans; + UINT32 m_nNumAnalogoutChans; + UINT32 m_nNumDiginChans; + UINT32 m_nNumSerialChans; + UINT32 m_nNumDigoutChans; + UINT32 m_nNumTotalChans; + NSP_STATUS m_nNspStatus[cbMAXPROCS]; // [4] + UINT32 m_nNumNTrodesPerInstrument[cbMAXPROCS]; // [4] + UINT32 m_nGeminiSystem; + APP_WORKSPACE m_icAppWorkspace[cbMAXAPPWORKSPACES]; // [10] workspace config +}; +``` + +**Note**: This is a **C++ class** (not a plain C struct), which is fragile for cross-compiler +shared memory. The `public`/`private` labels don't affect memory layout in practice, but this +is still a portability concern. + +The `APP_WORKSPACE` array at the end is only used by Central's GUI. + +#### 5. cbSPKBUFF (Spike Cache) + +```c +// cbhwlib.h line 944 +typedef struct { + UINT32 chid; + UINT32 pktcnt; // 400 + UINT32 pktsize; + UINT32 head; + UINT32 valid; + cbPKT_SPK spkpkt[cbPKT_SPKCACHEPKTCNT]; // [400] +} cbSPKCACHE; + +typedef struct { + UINT32 flags; + UINT32 chidmax; + UINT32 linesize; + UINT32 spkcount; + cbSPKCACHE cache[cbPKT_SPKCACHELINECNT]; // [cbMAXCHANS] = [880] +} cbSPKBUFF; +``` + +Performance optimization: caches last 400 spikes per channel to avoid scanning the full +768 MB receive buffer. + +#### 6. cbSIGNALevent (Synchronization) + +- **Windows**: Named manual-reset event (`CreateEvent`/`SetEvent`/`WaitForSingleObject`) +- **POSIX**: Named semaphore (`sem_open`/`sem_post`/`sem_timedwait`) + +STANDALONE signals after writing packets. CLIENTs wait on signal to wake up and read. + +#### 7. System Mutex + +Named mutex (`cbSharedDataMutex` + instance suffix) used by STANDALONE to claim ownership. +CLIENT checks this mutex to detect if a STANDALONE is running. + +## Multi-Instrument Channel Numbering + +When multiple NSPs connect within one instance, channels get expanded global numbers. + +`cbGetExpandedChannelNumber()` (cbHwlibHi.cpp line 1202): sums `chancount` from preceding +instruments to offset local channel numbers into global space. + +`cbGetChanInstrument()` (line 1227): reads `chaninfo[ch-1].cbpkt_header.instrument` to +determine which NSP a global channel belongs to. + +## Size Summary + +| Segment | Approximate Size | +|------------------------|---------------------------------------------| +| cbCFGBUFF | Several MB (depends on packet struct sizes) | +| cbRECBUFF | ~768 MB | +| XmtGlobal | ~290 MB | +| XmtLocal | ~116 MB | +| cbPcStatus | Few KB | +| cbSPKBUFF | Large (400 spikes * 880 channels) | +| **Total per instance** | **~1.2 GB** | + +## Creation Flow + +### STANDALONE (cbOpen with bStandAlone=TRUE) + +1. Acquire system mutex (`cbSharedDataMutex[N]`) +2. Find first free slot in `cb_library_initialized[]` -> `nIdx` +3. Set `cb_library_index[nInstance] = nIdx` +4. Call `CreateSharedObjects(nInstance)`: + - `CreateFileMapping()` for each segment with instance-suffixed name + - `MapViewOfFile()` to get pointers + - Zero-initialize all buffers + - Set up spike cache, color tables, transmit buffer lengths + - `CreateEvent()` for signal event + +### CLIENT (cbOpen with bStandAlone=FALSE) + +1. Check system mutex exists (Central must be running) +2. Find first free slot -> `nIdx` +3. `OpenFileMapping()` for each segment +4. `MapViewOfFile()` (read-only for rec/cfg, read-write for xmt/status/spk) +5. Set `cb_library_index[nInstance] = nIdx` +6. `cbMakePacketReadingBeginNow()` -- set tail to current head position + +## References + +- `Central-Suite/cbhwlib/cbhwlib.h` -- Structure definitions, constants +- `Central-Suite/cbhwlib/cbhwlib.cpp` -- cbOpen(), CreateSharedObjects(), buffer management +- `Central-Suite/cbhwlib/cbHwlibHi.cpp` -- High-level helpers (channel mapping, etc.) +- `cbproto/include/cbproto/cbproto.h` -- Protocol constants (cbMAXOPEN, cbNSP1, cbPKT_HEADER) +- `Central-Suite/cbmex/cbsdk.cpp` -- SDK layer (SdkApp, g_app[] array) +- `Central-Suite/CentralCommon/BmiApp.cpp` -- `--instance` command-line parsing diff --git a/docs/shared_memory_architecture.md b/docs/shared_memory_architecture.md new file mode 100644 index 00000000..af8f5a75 --- /dev/null +++ b/docs/shared_memory_architecture.md @@ -0,0 +1,612 @@ +# CereLink Shared Memory Architecture + +## Overview + +CereLink supports two shared memory modes: + +- **Native mode** (first-class): CereLink creates its own per-device shared memory segments. + Lean, right-sized buffers. No instance index -- devices identified by type. Packets are + always in the current protocol format. + +- **Central compat mode** (fallback): CereLink attaches to Central's existing shared memory + as a CLIENT. Uses `CentralLegacyCFGBUFF` to match Central's exact binary layout (which + differs from CereLink's `cbConfigBuffer`). Instrument filtering extracts only the + requested device's packets from Central's shared receive buffer. Protocol translation + handles older Central formats (3.11, 4.0, 4.1) automatically. + +Mode is auto-detected at startup: if Central's shared memory exists, use compat mode; +otherwise, use native mode. + +**See also**: [Central's shared memory layout](central_shared_memory_layout.md) for the +upstream layout that compat mode interoperates with. + +## Device Identification + +CereLink identifies devices by **type**, not by numeric instance index. Each device type is +a singleton with fixed, well-known network configuration: + +| DeviceType | IP Address | Recv Port | FE Channels | +|--------------|-----------------|-----------|--------------| +| `LEGACY_NSP` | 192.168.137.128 | 51002 | 256 | +| `NSP` | 192.168.137.128 | 51001 | 256 | +| `HUB1` | 192.168.137.200 | 51002 | 256 | +| `HUB2` | 192.168.137.201 | 51003 | 256 | +| `HUB3` | 192.168.137.202 | 51004 | 256 | +| `NPLAY` | 127.0.0.1 | -- | 256 | + +There is no instance index. The device type **is** the identifier. A client opens a session +for a specific device type and receives only that device's data. + +``` +// No numeric instance index to remember +auto session = SdkSession::open(DeviceType::HUB1); +``` + +## Architecture Diagram (Native Mode) + +``` ++------------------------------------------------------------------------------------+ +| CEREBUS DEVICE | +| (NSP Hardware - UDP Protocol) | ++--------------------+-----------------------------------+---------------------------+ + | | + | UDP Packets | UDP Packets + | (Port varies by device) | (Port varies by device) + v ^ ++-------------------------------------------------------------------------------------+ +| STANDALONE PROCESS (owns device) | +| +--------------------------------------------------------------------------------+ | +| | THREADS | | +| | | | +| | +----------------+ +----------------+ +-----------------+ | | +| | | UDP Receive | | UDP Send | | Callback | | | +| | | Thread | | Thread | | Dispatcher | | | +| | | (cbdev) | | (cbdev) | | Thread | | | +| | +--------+-------+ +--------^-------+ +--------^-------+ | | +| | | | | | | +| | | Packets | Dequeue | Process | | +| | | (translated to | packets | callbacks | | +| | | current protocol) | | | | +| | v | | | | +| | +--------+-----------------------+-----------------------+-------+ | | +| | | onPacketsReceivedFromDevice() | | | +| | | | | | +| | | 1. storePacket() -> rec buffer (ring buffer) | | | +| | | 2. storePacket() -> cfg buffer (config updates) | | | +| | | 3. signalData() -> signal event <-----------+ | | | +| | | 4. Enqueue to local SPSC queue | SIGNAL! | | | +| | +--------------------------------------------------+ | | | +| +--------------------------------------------------------------------------------+ | ++------------------+----------------------------------------------------------------------+ + | + | Writes to (Producer) + | + +==============v==================================================================+ + || NATIVE SHARED MEMORY (per device) || + || Named: cbshm_{device}_{segment} || + || || + || 1. cbshm_hub1_cfg | Config for 1 device (284 channels, ~1 MB) || + || 2. cbshm_hub1_rec | Receive ring buffer (~256 MB) || + || 3. cbshm_hub1_xmt | Transmit queue (~5 MB) || + || 4. cbshm_hub1_xmt_local | Local transmit queue (~2 MB) || + || 5. cbshm_hub1_status | Device status (~few KB) || + || 6. cbshm_hub1_spk | Spike cache (272 analog channels) || + || 7. cbshm_hub1_signal | Data availability signal || + || || + || All packets stored in CURRENT protocol format (translation at cbdev layer) || + || Config sized for 1 instrument (no [4] arrays) || + || Client pays only for devices it opens || + +==================================================================================+ + | + | Reads from (Consumer) + | ++------------------v-----------------------------------------------------------------------+ +| CLIENT PROCESS (attaches to shmem) | +| +-------------------------------------------------------------------------------------+ | +| | Shared Memory Receive Thread | | +| | | | +| | while (running) { | | +| | waitForData(250ms) <-- wakeup from signal | | +| | if (signaled) { | | +| | readReceiveBuffer() <-- all packets are this device's | | +| | packet_callback(packets, count) <-- direct invocation, no queue | | +| | } | | +| | } | | +| +-------------------------------------------------------------------------------------+ | ++-----------------------------------------------------------------------------------------+ +``` + +## Native Mode + +### Segment Naming + +Each device gets its own set of shared memory segments: + +``` +cbshm_{device}_{segment} + +Examples: + cbshm_nsp_cfg cbshm_hub1_cfg cbshm_hub2_cfg + cbshm_nsp_rec cbshm_hub1_rec cbshm_hub2_rec + cbshm_nsp_xmt cbshm_hub1_xmt cbshm_hub2_xmt + cbshm_nsp_xmt_local cbshm_hub1_xmt_local cbshm_hub2_xmt_local + cbshm_nsp_status cbshm_hub1_status cbshm_hub2_status + cbshm_nsp_spk cbshm_hub1_spk cbshm_hub2_spk + cbshm_nsp_signal cbshm_hub1_signal cbshm_hub2_signal +``` + +On POSIX, names are prefixed with `/` (e.g., `/cbshm_hub1_rec`). + +### Per-Device Config Buffer (Native) + +Since each device is a single NSP, the config buffer drops all multi-instrument arrays +to scalars: + +```c +typedef struct { + uint32_t version; + uint32_t device_type; // DeviceType enum value + + cbPKT_SYSINFO sysinfo; + cbPKT_PROCINFO procinfo; // was [4], now 1 + cbPKT_BANKINFO bankinfo[NATIVE_MAXBANKS]; // 1 instrument's banks + cbPKT_GROUPINFO groupinfo[NATIVE_MAXGROUPS]; // 1 instrument's groups + cbPKT_FILTINFO filtinfo[NATIVE_MAXFILTS]; // 1 instrument's filters + cbPKT_ADAPTFILTINFO adaptinfo; // was [4], now 1 + cbPKT_REFELECFILTINFO refelecinfo; // was [4], now 1 + cbPKT_LNC isLnc; // was [4], now 1 + + cbPKT_CHANINFO chaninfo[NATIVE_MAXCHANS]; // 284 channels (1 NSP) + + NativeSPIKE_SORTING isSortingOptions; // sized for 284 channels + cbPKT_NTRODEINFO isNTrodeInfo[cbMAXNTRODES]; + cbPKT_AOUT_WAVEFORM isWaveform[AOUT_NUM_GAIN_CHANS][cbMAX_AOUT_TRIGGER]; + cbPKT_NPLAY isNPlay; + cbVIDEOSOURCE isVideoSource[cbMAXVIDEOSOURCE]; + cbTRACKOBJ isTrackObj[cbMAXTRACKOBJ]; + cbPKT_FILECFG fileinfo; +} NativeConfigBuffer; +``` + +Channel count per device (`NATIVE_MAXCHANS` with `cbMAXPROCS=1`): + +| Type | Count | Formula | +|----------------|---------|------------------| +| Front-end | 256 | `cbNUM_FE_CHANS` | +| Analog input | 16 | `16 * 1` | +| Analog output | 4 | `4 * 1` | +| Audio output | 2 | `2 * 1` | +| Digital input | 1 | `1 * 1` | +| Serial | 1 | `1 * 1` | +| Digital output | 4 | `4 * 1` | +| **Total** | **284** | | + +### Per-Device Memory Footprint + +| Segment | Central-compat (4 instruments) | Native (1 device) | Savings | +|----------------|--------------------------------|---------------------------|----------| +| Config buffer | ~4 MB (880 ch, `[4]` arrays) | ~1 MB (284 ch, scalars) | ~75% | +| Receive buffer | ~768 MB (768 FE ch) | ~256 MB (256 FE ch) | ~67% | +| XmtGlobal | ~290 MB (5000 * max-UDP-size) | ~5 MB (5000 * 1024 bytes) | ~98% | +| XmtLocal | ~116 MB (2000 * max-UDP-size) | ~2 MB (2000 * 1024 bytes) | ~98% | +| Spike cache | large (832 analog ch) | ~1/3 (272 analog ch) | ~67% | +| Status | ~few KB | ~few KB | -- | +| **Total** | **~1.2 GB** | **~265 MB** | **~78%** | + +The transmit buffers are dramatically smaller because they carry only config/command packets +(max 1024 bytes each), not max-UDP-sized packets. Central's XmtGlobal is drained at 4 +packets per 10ms; the buffer only needs capacity for burst scenarios like CCF file loading. + +### Packet Format in Native Shared Memory + +All packets in native shared memory are in **current protocol format** (4.2+). Protocol +translation happens at the cbdev layer before packets reach shared memory: + +``` +Device (any protocol) --> cbdev DeviceSession wrapper --> translates to current format + --> writes current-format packets to native shmem + --> CLIENTs read current format (no translation needed) +``` + +This means CLIENT processes never need to know what protocol the device speaks. + +## Central Compat Mode + +When CereLink detects that Central is running (its shared memory segments exist), it +attaches as a CLIENT using the `CENTRAL_COMPAT` shared memory layout. This layout uses +`CentralLegacyCFGBUFF` -- a struct matching Central's exact binary field order -- instead +of CereLink's own `cbConfigBuffer` (which has incompatible field ordering). + +### Segment Names (Central's) + +Central uses instance-0 bare names: +- `cbCFGbuffer`, `cbRECbuffer`, `XmtGlobal`, `XmtLocal`, `cbSTATUSbuffer`, `cbSPKbuffer`, + `cbSIGNALevent` + +CereLink only supports Central instance 0. Instances 1-3 are not supported. + +### DeviceType to Instrument Index Mapping + +Central's config buffer has `[4]` arrays for up to 4 instruments. CereLink must map +`DeviceType` to the correct instrument index in Central's arrays. + +The mapping is hardcoded based on Central's compile-time `GEMSTART` setting. The current +Central build uses `GEMSTART == 2`: + +| Instrument Index | Device | IP Address | Port | +|------------------|--------|-----------------|-------| +| 0 | Hub 1 | 192.168.137.200 | 51002 | +| 1 | Hub 2 | 192.168.137.201 | 51003 | +| 2 | Hub 3 | 192.168.137.202 | 51004 | +| 3 | NSP | 192.168.137.128 | 51001 | + +**Limitation**: This mapping assumes Central was compiled with `GEMSTART == 2`. An alternate +build (`GEMSTART == 1`) uses a different ordering: NSP=0, Hub1=1, Hub2=2. CereLink does +not currently detect or adapt to alternate GEMSTART configurations. If a future Central +build changes GEMSTART, this mapping must be updated. A runtime discovery mechanism (e.g., +scanning `procinfo` slots for identifying data) could be added if needed. + +For legacy (non-Gemini) NSP systems, only instrument index 0 is used. + +### Receive Buffer Filtering + +In Central's shared memory, ALL devices' packets go into ONE receive ring buffer. CereLink's +`setInstrumentFilter()` method configures `readReceiveBuffer()` to filter by instrument: + +1. `SdkSession::create()` sets the instrument filter based on DeviceType → instrument index +2. `readReceiveBuffer()` reads all packets from `cbRECbuffer` (advances the ring buffer tail) +3. For each packet, checks `cbpkt_header.instrument` against the filter +4. Packets not matching the filter are consumed (tail advances) but not delivered to the caller + +```cpp +// Set automatically by SdkSession::create() in compat mode +shmem_session.setInstrumentFilter(getCentralInstrumentIndex(config.device_type)); + +// readReceiveBuffer() internally skips non-matching packets +session.readReceiveBuffer(packets, max_count, packets_read); +// packets_read only includes packets matching the instrument filter +``` + +This is less efficient than native mode (where the receive buffer only contains one device's +packets), but the large buffer size (~768 MB) makes this a negligible cost. + +### Protocol Translation in Compat Mode (Phase 3 - Complete) + +When CereLink attaches to Central's shared memory, Central may be running an older protocol +(3.11, 4.0, or 4.1). Central stores raw device packets in `cbRECbuffer` without translation. +CereLink detects the protocol version and translates packets on-the-fly. + +**Protocol detection** reads `procinfo[0].version` from `CentralLegacyCFGBUFF`: +- `version = (major << 16) | minor` (MAKELONG format) +- major < 4 → Protocol 3.11 (8-byte headers, 32-bit timestamps) +- major=4, minor=0 → Protocol 4.0 (16-byte headers, different field layout) +- major=4, minor=1 → Protocol 4.1 (16-byte headers, current layout) +- major=4, minor≥2 → Current protocol (no translation needed) + +**Receive path** (`readReceiveBuffer`): Parses the protocol-specific header to extract +`dlen`, copies raw bytes from the ring buffer, translates header + payload to current +format using `PacketTranslator`, then applies the instrument filter on the translated +header. + +**Transmit path** (`enqueuePacket`): Translates current-format packets to the legacy +format before writing to the transmit ring buffer. + +Translation reuses the same `PacketTranslator` class that `cbdev` uses for direct UDP +connections. `PacketTranslator` was moved from `cbdev` to `cbproto` (the shared protocol +library) so that both `cbshm` and `cbdev` can access it. + +### Config Buffer Access in Compat Mode + +Central's `cbCFGBUFF` has a different field layout than CereLink's `NativeConfigBuffer` or +`cbConfigBuffer` (see "Differences from Central" section below). The `CENTRAL_COMPAT` layout +uses a `CentralLegacyCFGBUFF` struct that matches Central's exact field order to read the +config buffer correctly. + +All `ShmemSession` accessor methods (`getProcInfo`, `setBankInfo`, `getFilterInfo`, etc.) +dispatch on the layout and use the correct struct: + +```cpp +// In CENTRAL_COMPAT mode, accessors use legacyCfg() +if (layout == ShmemLayout::CENTRAL_COMPAT) + return legacyCfg()->procinfo[idx]; // CentralLegacyCFGBUFF +else if (layout == ShmemLayout::NATIVE) + return nativeCfg()->procinfo; // NativeConfigBuffer (scalar) +else + return centralCfg()->procinfo[idx]; // cbConfigBuffer +``` + +Instrument status in compat mode: +- `isInstrumentActive()` always returns **true** (Central has no `instrument_status` field; + if the shared memory exists, instruments are configured by Central) +- `setInstrumentActive()` returns **error** (read-only in compat mode) +- `getConfigBuffer()` returns **nullptr** (wrong struct type for compat mode) +- `getLegacyConfigBuffer()` returns the `CentralLegacyCFGBUFF*` pointer + +## Mode Auto-Detection + +``` +SdkSession::open(DeviceType::HUB1) + | + +-- Can open Central's "cbSharedDataMutex" (instance 0)? + | | + | YES --> Central Compat Mode (CENTRAL_COMPAT layout) + | - Map Central's 7 instance-0 segments + | - Use CentralLegacyCFGBUFF for config buffer (Central's binary layout) + | - Use GEMSTART==2 mapping: Hub1 = instrument index 0 + | - Set instrument filter (setInstrumentFilter) for receive buffer + | - Index into [4] arrays for config access via legacyCfg() + | - Detect protocol version, translate packets if non-current + | + +-- NO --> Can open "cbshm_hub1_signal"? + | | + | YES --> Native Client Mode + | - Map cbshm_hub1_* segments (read-only) + | - All packets are Hub1's (no filtering) + | - Packets in current format (no translation) + | - Config is single-instrument (no indexing) + | + +-- NO --> Native Standalone Mode + - Create cbshm_hub1_* segments + - Start cbdev (protocol auto-detect + translation) + - Write current-format packets to native shmem + - Other CLIENTs can attach via Native Client Mode +``` + +## Key Data Flow Paths + +### Device -> STANDALONE -> Shared Memory -> CLIENT + +1. **NSP device** sends UDP packet +2. **STANDALONE UDP receive thread** catches packet +3. **cbdev** translates packet to current protocol format (if device uses older protocol) +4. **onPacketsReceivedFromDevice()**: + - `storePacket(pkt)` writes to receive buffer `[headindex]` + - `headindex++` (advance writer position) + - `signalData()` -> Set signal event (wake up CLIENTs!) + - Enqueue to SPSC queue for callback dispatcher thread +5. **CLIENT shmem receive thread** wakes up from `waitForData()` +6. **CLIENT** calls `readReceiveBuffer()`: + - Read from receive buffer `[tailindex]` to `[headindex]` + - `tailindex += packets_read` (advance reader position) + - Parse packets from ring buffer (handle wraparound) +7. **CLIENT** invokes user callback **directly** (no queue, no 2nd thread needed) + +### CLIENT -> Shared Memory -> STANDALONE -> Device + +1. **Client app** calls `sendPacket(command)` +2. `enqueuePacket()` writes to transmit queue +3. **STANDALONE send thread** dequeues from transmit queue +4. **Send thread** transmits packet via UDP to device + +## Synchronization + +### STANDALONE (Producer) +- Calls `signalData()` after writing packets +- **Windows**: `SetEvent()` (manual-reset event) +- **POSIX**: `sem_post()` (increment semaphore) + +### CLIENT (Consumer) +- Calls `waitForData(250ms)` before reading +- **Windows**: `WaitForSingleObject()` with timeout +- **POSIX**: `sem_timedwait()` (Linux) or polling `sem_trywait()` (macOS) + +### Efficiency +- CLIENT sleeps until signaled (no CPU-burning polling!) +- Immediate wakeup when new data arrives + +## Ring Buffer Tracking + +### Overview +- Receive buffer is a ring buffer with wrap-around capability +- Native mode: 256 * 65536 * 4 - 1 dwords (~256 MB per device) +- Central compat: 768 * 65536 * 4 - 1 dwords (~768 MB, all devices combined) + +### Writer (STANDALONE) +- Updates `headindex` - current write position +- Updates `headwrap` - increments each time buffer wraps around + +### Reader (CLIENT) +- Tracks own `tailindex` - current read position +- Tracks own `tailwrap` - increments each time reader wraps around +- Each CLIENT maintains independent read position + +### Synchronization Logic +- **No new data**: `tailwrap == headwrap && tailindex == headindex` +- **Data available**: `tailindex` < `headindex` (same wrap) or different wrap counters +- **Buffer overrun**: `headwrap > tailwrap + 1` (writer lapped reader - data lost!) + +### Packet Size Calculation +- Packet size = `header_32size + dlen` (read from the packet header, NOT the first dword) +- Header size depends on protocol: 2 dwords (3.11), 4 dwords (4.0+) +- `dlen` is extracted from the correct offset within the protocol-specific header struct +- Variable-length packets +- Handles wraparound mid-packet (copy in two parts) + +## Thread Architecture + +### STANDALONE Process Threads +1. **UDP Receive Thread** (cbdev) - Receives packets from device, translates protocol +2. **UDP Send Thread** (cbdev) - Sends packets to device +3. **Callback Dispatcher Thread** - Decouples fast UDP receive from slow user callbacks +4. **Main Thread** - User application + +**Why separate callback thread?** UDP packets arrive at high rate and OS buffer is limited. +We must dequeue UDP packets quickly to avoid drops. User callbacks can be slow, so we use +an SPSC queue to buffer between fast UDP receive and slow callback processing. + +### CLIENT Process Threads (Optimized) +1. **Shared Memory Receive Thread** - Reads from receive buffer AND invokes callbacks directly +2. **Main Thread** - User application + +**Why no separate callback thread?** Reading from the receive buffer is not time-critical +(large buffer provides ample buffering). We invoke user callbacks directly, eliminating: +- One extra thread (simpler architecture, less overhead) +- One extra data copy (receive buffer -> callback, no packet_queue needed) + +## Differences from Central-Suite's Shared Memory Layout + +CereLink's current `cbConfigBuffer` struct is **NOT binary-compatible** with Central's +`cbCFGBUFF`. For a complete description of Central's layout, see +[central_shared_memory_layout.md](central_shared_memory_layout.md). + +### cbCFGbuffer / Config Buffer (INCOMPATIBLE) + +Field ordering is changed and fields are added/removed: + +| # | Central `cbCFGBUFF` | CereLink `cbConfigBuffer` | +|----|----------------------|----------------------------------| +| 1 | `version` | `version` | +| 2 | `sysflags` | `sysflags` | +| 3 | **`optiontable`** | **`instrument_status[4]`** (NEW) | +| 4 | **`colortable`** | `sysinfo` | +| 5 | `sysinfo` | `procinfo[4]` | +| 6 | `procinfo[4]` | `bankinfo[4][30]` | +| 7 | `bankinfo[4][30]` | `groupinfo[4][8]` | +| 8 | `groupinfo[4][8]` | `filtinfo[4][32]` | +| 9 | `filtinfo[4][32]` | `adaptinfo[4]` | +| 10 | `adaptinfo[4]` | `refelecinfo[4]` | +| 11 | `refelecinfo[4]` | **`isLnc[4]`** (MOVED earlier) | +| 12 | `chaninfo[880]` | `chaninfo[880]` | +| 13 | `isSortingOptions` | `isSortingOptions` | +| 14 | `isNTrodeInfo[..]` | `isNTrodeInfo[..]` | +| 15 | `isWaveform[..][..]` | `isWaveform[..][..]` | +| 16 | **`isLnc[4]`** | `isNPlay` | +| 17 | `isNPlay` | `isVideoSource[..]` | +| 18 | `isVideoSource[..]` | `isTrackObj[..]` | +| 19 | `isTrackObj[..]` | `fileinfo` | +| 20 | `fileinfo` | **`optiontable`** (MOVED later) | +| 21 | **`hwndCentral`** | **`colortable`** (MOVED later) | +| 22 | -- | (hwndCentral OMITTED) | + +Central compat mode requires a separate `CentralLegacyCFGBUFF` struct matching Central's +exact layout to read the config buffer correctly. + +### cbSTATUSbuffer / PC Status (PARTIALLY COMPATIBLE) + +| Difference | Central `cbPcStatus` | CereLink `CentralPCStatus` | +|---------------------|-----------------------------------------------|-----------------------------| +| Type | C++ class (private/public) | Plain C struct | +| `APP_WORKSPACE[10]` | Present (at end) | **Omitted** | +| Overlap | Fields match in order up to `m_nGeminiSystem` | Same | + +CereLink's struct is a subset -- safe to read, safe to write (omitted field is at the end). + +### cbRECbuffer, XmtGlobal, XmtLocal (COMPATIBLE) + +Same field layout: +- `received`, `lasttime`, `headwrap`, `headindex`, `buffer[N]` (receive) +- `transmitted`, `headindex`, `tailindex`, `last_valid_index`, `bufferlen`, `buffer[N]` (transmit) + +Central uses flexible array member (`buffer[0]`), CereLink uses fixed-size. Binary layout +matches as long as allocated sizes match. + +### cbSPKbuffer, cbSIGNALevent (COMPATIBLE) + +Same structure layouts and mechanisms. + +### Compatibility Summary + +| Segment | Compatible? | Notes | +|----------------|--------------|--------------------------------------------------| +| cbCFGbuffer | **NO** | Field order differs; need `CentralLegacyCFGBUFF` | +| cbRECbuffer | Yes | Same layout | +| XmtGlobal | Yes | Same layout | +| XmtLocal | Yes | Same layout | +| cbSTATUSbuffer | Partial | CereLink is a subset (missing workspace at end) | +| cbSPKbuffer | Yes | Same layout | +| cbSIGNALevent | Yes | Same mechanism | + +## Implementation Status + +### Core Infrastructure (Complete) + +- All 7 shared memory segments implemented for both Central-compat and native layouts +- `ShmemLayout` enum (`CENTRAL` / `CENTRAL_COMPAT` / `NATIVE`) controls buffer sizes and struct interpretation +- cbSIGNALevent synchronization working +- Ring buffer reading logic complete +- CLIENT mode shared memory receive thread implemented +- STANDALONE mode signaling to CLIENT processes +- Thread lifecycle management (start/stop) +- Optimized CLIENT mode (1 thread, 1 data copy) + +### Native Mode (Complete) + +- [x] Define `NativeConfigBuffer` struct (single-instrument, 284 channels) +- [x] Define native spike sorting structs (284-channel arrays) +- [x] Define `NativeTransmitBuffer` / `NativeTransmitBufferLocal` (1024-byte slots) +- [x] Define `NativeSpikeCache` / `NativeSpikeBuffer` (272 analog channels) +- [x] Define `NativePCStatus` (single instrument) +- [x] Implement `ShmemLayout` parameter in `ShmemSession::create()` +- [x] Implement dual-layout buffer sizing (`computeBufferSizes()`) +- [x] Implement `initNativeBuffers()` for STANDALONE initialization +- [x] All accessor methods branch on layout (procinfo, bankinfo, filtinfo, chaninfo, etc.) +- [x] Runtime `rec_buffer_len` replaces hardcoded receive buffer length +- [x] `getConfigBuffer()` returns nullptr if NATIVE; `getNativeConfigBuffer()` returns nullptr if CENTRAL +- [x] Implement `cbshm_{device}_{segment}` naming in SdkSession +- [x] Implement three-way mode auto-detection: Central CLIENT → Native CLIENT → Native STANDALONE +- [x] 34 new unit tests (20 NativeShmemSession + 14 NativeTypes), all passing +- [x] All existing Central-mode tests unaffected (no regressions) + +### Central Compat Mode (Complete - Phase 2) + +- [x] Define `CentralLegacyCFGBUFF` struct matching Central's exact binary layout +- [x] Add `ShmemLayout::CENTRAL_COMPAT` layout mode using `CentralLegacyCFGBUFF` +- [x] All accessor methods dispatch on `CENTRAL_COMPAT` (use `legacyCfg()` instead of `centralCfg()`) +- [x] Instrument status: `isInstrumentActive()` returns true, `setInstrumentActive()` returns error +- [x] `getLegacyConfigBuffer()` accessor for direct access to `CentralLegacyCFGBUFF*` +- [x] Implement receive buffer instrument filtering (`setInstrumentFilter()`) +- [x] `SdkSession::create()` uses `CENTRAL_COMPAT` for Central CLIENT with instrument filter auto-set +- [x] `getCentralInstrumentIndex()` maps DeviceType → instrument index (GEMSTART==2) +- [x] 16 new unit tests (14 CentralCompat + 2 CentralLegacyTypes), all passing +- [x] All existing tests unaffected (no regressions) + +### Protocol Translation (Complete - Phase 3) + +- [x] Move `PacketTranslator` from `cbdev` to `cbproto` (shared protocol library) +- [x] Convert `cbproto` from header-only (INTERFACE) to static library (STATIC) +- [x] Detect Central's protocol version from `procinfo[0].version` +- [x] Fix `readReceiveBuffer` to parse packet size from header (`header_32size + dlen`) + instead of reading the first dword (which is the `time` field, not packet size) +- [x] Protocol-aware receive path: parse protocol-specific headers, translate to current format +- [x] Protocol-aware transmit path: translate current format to legacy before writing +- [x] 10 new protocol translation unit tests (version detection, read/transmit round-trip, + instrument filtering with current protocol), all passing +- [x] All existing tests updated and passing (no regressions) + +### TODO + +- [ ] Runtime GEMSTART detection (currently hardcoded GEMSTART==2) + +## Code Locations + +- **Shared Memory**: `src/cbshm/` + - `include/cbshm/shmem_session.h` - Public API (ShmemSession class) + - `src/shmem_session.cpp` - Implementation + - `include/cbshm/central_types.h` - Central-compatible buffer structures + `CentralLegacyCFGBUFF` + - `include/cbshm/native_types.h` - Native-mode buffer structures (single-instrument) + - `include/cbshm/config_buffer.h` - Configuration buffer struct definition (`cbConfigBuffer`) + +- **SDK Integration**: `src/cbsdk/` + - `src/sdk_session.cpp` - High-level SDK, mode auto-detection, segment naming + - Threads, callbacks, packet routing + +- **Protocol**: `src/cbproto/` + - `include/cbproto/instrument_id.h` - Type-safe InstrumentId (1-based internally) + - `include/cbproto/types.h` - Per-NSP constants (cbMAXPROCS=1, cbNUM_FE_CHANS=256) + - `include/cbproto/cbproto.h` - Protocol packet definitions + - `include/cbproto/connection.h` - Protocol version enum + - `include/cbproto/packet_translator.h` - Bidirectional packet translation between protocol versions + - `src/packet_translator.cpp` - PacketTranslator implementation (used by both cbshm and cbdev) + +- **Device Layer**: `src/cbdev/` + - UDP communication with NSP hardware + - Protocol detection (`src/protocol_detector.cpp`) + - Per-version session wrappers (`src/device_session_{311,400,410}.cpp`) + - Used only in STANDALONE mode + +## References + +- [Central's shared memory layout](central_shared_memory_layout.md) +- Central Suite source: `Central-Suite/cbhwlib/cbhwlib.h` and `cbhwlib.cpp` +- Central instrument assignment: `Central-Suite/CentralCommon/CentralDlg.cpp` (GEMSTART) +- Cerebus Protocol Specification diff --git a/examples/CCFTest/CMakeLists.txt b/examples/CCFTest/CMakeLists.txt new file mode 100644 index 00000000..434b099b --- /dev/null +++ b/examples/CCFTest/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(ccf_test ccf_test.cpp) +target_link_libraries(ccf_test PRIVATE cbsdk) diff --git a/examples/CCFTest/ccf_test.cpp b/examples/CCFTest/ccf_test.cpp new file mode 100644 index 00000000..28b8beeb --- /dev/null +++ b/examples/CCFTest/ccf_test.cpp @@ -0,0 +1,142 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file ccf_test.cpp +/// @brief Test CCF save/load with a real device +/// +/// Usage: +/// ccf_test +/// ccf_test HUB1 hub1-64ch-1k.ccf hub1-raw128.ccf +/// +/// The test loads CCF A, saves the device config, loads CCF B, saves again, +/// and compares the two saved files to verify the load actually changed the device. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include + +using namespace cbsdk; + +DeviceType parseDeviceType(const std::string& s) { + if (s == "NSP") return DeviceType::LEGACY_NSP; + if (s == "GEMINI_NSP") return DeviceType::NSP; + if (s == "HUB1") return DeviceType::HUB1; + if (s == "HUB2") return DeviceType::HUB2; + if (s == "HUB3") return DeviceType::HUB3; + if (s == "NPLAY") return DeviceType::NPLAY; + std::cerr << "Unknown device type: " << s << "\n"; + std::exit(1); +} + +void printChannelSummary(SdkSession& session, const char* label, int n = 5) { + std::cout << "=== " << label << " (channels 1-" << n << ") ===\n"; + for (uint32_t ch = 1; ch <= (uint32_t)n; ++ch) { + const auto* info = session.getChanInfo(ch); + if (info) { + std::cout << " ch " << std::setw(3) << ch + << ": spkopts=0x" << std::hex << std::setw(5) << std::setfill('0') << info->spkopts + << " ainpopts=0x" << std::setw(4) << info->ainpopts + << std::dec << std::setfill(' ') + << " smpfilter=" << info->smpfilter + << " smpgroup=" << info->smpgroup + << " lncrate=" << info->lncrate + << " label=" << info->label + << "\n"; + } + } +} + +int main(int argc, char* argv[]) { + if (argc < 4) { + std::cerr << "Usage: ccf_test \n" + << " Loads A, saves, loads B, saves, then compares.\n" + << " Example: ccf_test HUB1 hub1-64ch-1k.ccf hub1-raw128.ccf\n"; + return 1; + } + + DeviceType device_type = parseDeviceType(argv[1]); + std::string ccf_a = argv[2]; + std::string ccf_b = argv[3]; + + // Create session + SdkConfig config; + config.device_type = device_type; + config.autorun = true; + + std::cout << "Creating session...\n"; + auto result = SdkSession::create(config); + if (result.isError()) { + std::cerr << "ERROR: " << result.error() << "\n"; + return 1; + } + auto session = std::move(result.value()); + + // Wait for config to be fully populated + std::this_thread::sleep_for(std::chrono::seconds(2)); + std::cout << "Connected.\n\n"; + + printChannelSummary(session, "Before any load"); + + // Step 1: Load CCF A + std::cout << "\n--- Loading " << ccf_a << " ---\n"; + auto r1 = session.loadCCF(ccf_a); + if (r1.isError()) { std::cerr << "Load A failed: " << r1.error() << "\n"; return 1; } + std::this_thread::sleep_for(std::chrono::seconds(2)); + + printChannelSummary(session, ("After loading " + ccf_a).c_str()); + + // Save device state after A + auto s1 = session.saveCCF("ccf_after_A.ccf"); + if (s1.isError()) { std::cerr << "Save after A failed: " << s1.error() << "\n"; return 1; } + std::cout << " Saved device state -> ccf_after_A.ccf\n\n"; + + // Step 2: Load CCF B + std::cout << "--- Loading " << ccf_b << " ---\n"; + auto r2 = session.loadCCF(ccf_b); + if (r2.isError()) { std::cerr << "Load B failed: " << r2.error() << "\n"; return 1; } + std::this_thread::sleep_for(std::chrono::seconds(2)); + + printChannelSummary(session, ("After loading " + ccf_b).c_str()); + + // Save device state after B + auto s2 = session.saveCCF("ccf_after_B.ccf"); + if (s2.isError()) { std::cerr << "Save after B failed: " << s2.error() << "\n"; return 1; } + std::cout << " Saved device state -> ccf_after_B.ccf\n\n"; + + // Compare: read both saved CCFs back and compare channel fields + std::cout << "=== Comparison ===\n"; + { + std::ifstream fa("ccf_after_A.ccf", std::ios::ate); + std::ifstream fb("ccf_after_B.ccf", std::ios::ate); + if (fa.is_open() && fb.is_open()) { + auto sizeA = fa.tellg(); + auto sizeB = fb.tellg(); + std::cout << " ccf_after_A.ccf: " << sizeA << " bytes\n"; + std::cout << " ccf_after_B.ccf: " << sizeB << " bytes\n"; + } + } + + // Field-by-field comparison using in-memory state + // (After load B, the device has B's config; we compare to after-A save) + // Re-read saved CCF A to compare against current device state + std::cout << "\n Field differences (ch 1-5) between saved A and current (B) state:\n"; + for (uint32_t ch = 1; ch <= 5; ++ch) { + const auto* info = session.getChanInfo(ch); + if (!info) continue; + std::cout << " ch " << ch << ":" + << " spkopts=0x" << std::hex << info->spkopts + << " ainpopts=0x" << info->ainpopts << std::dec + << " smpfilter=" << info->smpfilter + << " smpgroup=" << info->smpgroup + << " lncrate=" << info->lncrate + << "\n"; + } + + session.stop(); + std::cout << "\nDone.\n"; + return 0; +} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f1c895af..48df406e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,38 +1,55 @@ -# Define example programs +# Example programs for the modular architecture # Format: "target_name:SourceDir/source_file.cpp" -set(SIMPLE_EXAMPLES - "simple_cbsdk:SimpleCBSDK/simple_cbsdk.cpp" - "simple_ccf:SimpleCCF/simple_ccf.cpp" - "simple_io:SimpleIO/simple_io.cpp" - "simple_callback:SimpleIO/simple_callback.cpp" - "simple_comments:SimpleComments/simple_comments.cpp" - "simple_analog_out:SimpleAnalogOut/simple_analog_out.cpp" + +# cbsdk examples (link against cbsdk) +set(CBSDK_EXAMPLES + "gemini_example:GeminiExample/gemini_example.cpp" + "simple_device:SimpleDevice/simple_device.cpp" + "central_client:CentralClient/central_client.cpp" + "ccf_test:CCFTest/ccf_test.cpp" + "recording_test:RecordingTest/recording_test.cpp" +) + +# cbdev examples (link against cbdev only - no cbshm, no cbsdk) +set(CBDEV_EXAMPLES + "check_protocol_version:CheckProtocolVersion/check_protocol_version.cpp" ) set(SAMPLE_TARGET_LIST) -# Create executables for simple examples (just link against the library) -foreach(example ${SIMPLE_EXAMPLES}) +foreach(example ${CBSDK_EXAMPLES}) string(REPLACE ":" ";" example_parts ${example}) list(GET example_parts 0 target_name) list(GET example_parts 1 source_file) add_executable(${target_name} ${source_file}) - target_link_libraries(${target_name} ${LIB_NAME_STATIC}) + target_link_libraries(${target_name} cbsdk) list(APPEND SAMPLE_TARGET_LIST ${target_name}) endforeach() -list(APPEND INSTALL_TARGET_LIST ${SAMPLE_TARGET_LIST}) +foreach(example ${CBDEV_EXAMPLES}) + string(REPLACE ":" ";" example_parts ${example}) + list(GET example_parts 0 target_name) + list(GET example_parts 1 source_file) + add_executable(${target_name} ${source_file}) + target_link_libraries(${target_name} cbdev) + list(APPEND SAMPLE_TARGET_LIST ${target_name}) +endforeach() + +# Set RPATH for Unix/macOS if(NOT CMAKE_INSTALL_RPATH) set(LIBDIR "../lib") - foreach(test_app ${TEST_SAMPLE_LIST}) + foreach(sample_app ${SAMPLE_TARGET_LIST}) if(APPLE) - set_property(TARGET ${test_app} APPEND - PROPERTY INSTALL_RPATH "@executable_path/;@executable_path/${LIBDIR};@executable_path/../Frameworks") + set_property(TARGET ${sample_app} APPEND + PROPERTY INSTALL_RPATH "@executable_path/;@executable_path/${LIBDIR};@executable_path/../Frameworks") elseif(UNIX) - set_property(TARGET ${test_app} - PROPERTY INSTALL_RPATH "\$ORIGIN:\$ORIGIN/${LIBDIR}") + set_property(TARGET ${sample_app} + PROPERTY INSTALL_RPATH "\$ORIGIN:\$ORIGIN/${LIBDIR}") endif(APPLE) endforeach() -endif(NOT CMAKE_INSTALL_RPATH) \ No newline at end of file +endif(NOT CMAKE_INSTALL_RPATH) + +# Export target list to parent scope for installation +set(SAMPLE_TARGET_LIST ${SAMPLE_TARGET_LIST} PARENT_SCOPE) diff --git a/examples/CentralClient/central_client.cpp b/examples/CentralClient/central_client.cpp new file mode 100644 index 00000000..5475f8d9 --- /dev/null +++ b/examples/CentralClient/central_client.cpp @@ -0,0 +1,228 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file central_client.cpp +/// @brief Diagnostic tool for testing CENTRAL_COMPAT CLIENT mode +/// +/// Attaches to Central's shared memory directly (bypassing SDK auto-detection) +/// and prints diagnostic info to verify struct layout compatibility. +/// +/// Usage: +/// central_client [instance] +/// central_client # Default: instance 0 +/// central_client 1 # Instance 1 (for multi-instance setups) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace cbshm; + +std::atomic g_running{true}; + +void signalHandler(int) { g_running = false; } + +static std::string makeName(const char* base, int instance) { + if (instance == 0) return base; + return std::string(base) + std::to_string(instance); +} + +int main(int argc, char* argv[]) { + int instance = 0; + if (argc >= 2) instance = std::atoi(argv[1]); + + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + + std::cout << "==============================================\n"; + std::cout << " CereLink Central Client Diagnostic\n"; + std::cout << "==============================================\n\n"; + + // Print struct sizes for comparison with Central + std::cout << "=== Struct Size Verification ===\n"; + std::cout << " sizeof(CentralLegacyCFGBUFF): " << sizeof(CentralLegacyCFGBUFF) << "\n"; + std::cout << " sizeof(CentralReceiveBuffer): " << sizeof(CentralReceiveBuffer) << "\n"; + std::cout << " sizeof(CentralTransmitBuffer): " << sizeof(CentralTransmitBuffer) << "\n"; + std::cout << " sizeof(CentralTransmitBufferLocal): " << sizeof(CentralTransmitBufferLocal) << "\n"; + std::cout << " sizeof(CentralPCStatus): " << sizeof(CentralPCStatus) << "\n"; + std::cout << " sizeof(CentralSpikeBuffer): " << sizeof(CentralSpikeBuffer) << "\n"; + std::cout << " sizeof(CentralSpikeCache): " << sizeof(CentralSpikeCache) << "\n"; + std::cout << " sizeof(CentralAppWorkspace): " << sizeof(CentralAppWorkspace) << "\n"; + std::cout << "\n"; + + // Print key constants + std::cout << "=== Key Constants ===\n"; + std::cout << " CENTRAL_cbMAXPROCS: " << CENTRAL_cbMAXPROCS << "\n"; + std::cout << " CENTRAL_cbNUM_FE_CHANS: " << CENTRAL_cbNUM_FE_CHANS << "\n"; + std::cout << " CENTRAL_cbMAXCHANS: " << CENTRAL_cbMAXCHANS << "\n"; + std::cout << " CENTRAL_cbMAXBANKS: " << CENTRAL_cbMAXBANKS << "\n"; + std::cout << " CENTRAL_cbMAXNTRODES: " << CENTRAL_cbMAXNTRODES << "\n"; + std::cout << " CENTRAL_AOUT_NUM_GAIN_CHANS: " << CENTRAL_AOUT_NUM_GAIN_CHANS << "\n"; + std::cout << " CENTRAL_cbPKT_SPKCACHELINECNT: " << CENTRAL_cbPKT_SPKCACHELINECNT << "\n"; + std::cout << " CENTRAL_cbMAXAPPWORKSPACES: " << CENTRAL_cbMAXAPPWORKSPACES << "\n"; + std::cout << " sizeof(PROCTIME): " << sizeof(PROCTIME) << "\n"; + std::cout << "\n"; + + // Construct names for this instance + std::string cfg_name = makeName("cbCFGbuffer", instance); + std::string rec_name = makeName("cbRECbuffer", instance); + std::string xmt_name = makeName("XmtGlobal", instance); + std::string xmtl_name = makeName("XmtLocal", instance); + std::string status_name = makeName("cbSTATUSbuffer", instance); + std::string spk_name = makeName("cbSPKbuffer", instance); + std::string signal_name = makeName("cbSIGNALevent", instance); + + std::cout << "=== Attempting Central CLIENT mode (instance " << instance << ") ===\n"; + std::cout << " Config: " << cfg_name << "\n"; + std::cout << " Receive: " << rec_name << "\n"; + std::cout << " XmtGlob: " << xmt_name << "\n"; + std::cout << " XmtLoc: " << xmtl_name << "\n"; + std::cout << " Status: " << status_name << "\n"; + std::cout << " Spike: " << spk_name << "\n"; + std::cout << " Signal: " << signal_name << "\n\n"; + + auto result = ShmemSession::create( + cfg_name, rec_name, xmt_name, xmtl_name, + status_name, spk_name, signal_name, + Mode::CLIENT, ShmemLayout::CENTRAL_COMPAT); + + if (result.isError()) { + std::cerr << "FAILED to attach to Central's shared memory: " << result.error() << "\n"; + std::cerr << "\nIs Central running?\n"; + return 1; + } + + auto session = std::move(result.value()); + std::cout << "SUCCESS: Attached to Central's shared memory!\n\n"; + + // Read config buffer + auto* cfg = session.getLegacyConfigBuffer(); + if (!cfg) { + std::cerr << "ERROR: getLegacyConfigBuffer() returned null\n"; + return 1; + } + + std::cout << "=== Config Buffer Contents ===\n"; + std::cout << " version: " << cfg->version << "\n"; + std::cout << " sysflags: 0x" << std::hex << cfg->sysflags << std::dec << "\n"; + + // Read procinfo for each instrument + std::cout << "\n=== Processor Info ===\n"; + for (uint32_t i = 0; i < CENTRAL_cbMAXPROCS; ++i) { + auto& proc = cfg->procinfo[i]; + // procinfo version field = (major << 16) | minor + uint32_t ver = proc.cbpkt_header.type; // Version is stored in a known field + std::cout << " Proc[" << i << "]:" + << " time=" << proc.cbpkt_header.time + << " chid=" << proc.cbpkt_header.chid + << " type=0x" << std::hex << proc.cbpkt_header.type << std::dec + << " dlen=" << proc.cbpkt_header.dlen + << " inst=" << (int)proc.cbpkt_header.instrument + << "\n"; + } + + // Detect protocol version + auto proto = session.getCompatProtocolVersion(); + std::cout << "\n=== Detected Protocol ===\n"; + std::cout << " Protocol version: "; + switch (proto) { + case CBPROTO_PROTOCOL_311: std::cout << "3.11\n"; break; + case CBPROTO_PROTOCOL_400: std::cout << "4.0\n"; break; + case CBPROTO_PROTOCOL_410: std::cout << "4.1\n"; break; + case CBPROTO_PROTOCOL_CURRENT: std::cout << "CURRENT (4.2+)\n"; break; + default: std::cout << "UNKNOWN\n"; break; + } + + // Read status buffer + std::cout << "\n=== PC Status ===\n"; + auto num_total = session.getNumTotalChans(); + if (num_total.isOk()) { + std::cout << " Total channels: " << num_total.value() << "\n"; + } + for (uint32_t i = 0; i < CENTRAL_cbMAXPROCS; ++i) { + auto nsp = session.getNspStatus(cbproto::InstrumentId::fromIndex(i)); + if (nsp.isOk()) { + const char* status_str = "?"; + switch (nsp.value()) { + case NSPStatus::NSP_INIT: status_str = "INIT"; break; + case NSPStatus::NSP_NOIPADDR: status_str = "NOIPADDR"; break; + case NSPStatus::NSP_NOREPLY: status_str = "NOREPLY"; break; + case NSPStatus::NSP_FOUND: status_str = "FOUND"; break; + case NSPStatus::NSP_INVALID: status_str = "INVALID"; break; + } + std::cout << " NSP[" << i << "] status: " << status_str << "\n"; + } + } + auto gemini = session.isGeminiSystem(); + if (gemini.isOk()) { + std::cout << " Gemini system: " << (gemini.value() ? "YES" : "NO") << "\n"; + } + + // Read some channel info + std::cout << "\n=== Sample Channel Info ===\n"; + for (uint32_t ch = 0; ch < 5 && ch < CENTRAL_cbMAXCHANS; ++ch) { + auto ci = session.getChanInfo(ch); + if (ci.isOk()) { + auto& chan = ci.value(); + std::cout << " Chan[" << std::setw(3) << ch << "]: " + << " chid=" << chan.cbpkt_header.chid + << " type=0x" << std::hex << chan.cbpkt_header.type << std::dec + << " dlen=" << chan.cbpkt_header.dlen + << " smpgroup=" << chan.smpgroup + << " label=\"" << chan.label << "\"" + << "\n"; + } + } + + // Now monitor receive buffer for packets + std::cout << "\n=== Monitoring Receive Buffer ===\n"; + std::cout << "Waiting for packets (Ctrl+C to stop)...\n\n"; + + // Set instrument filter for Hub1 (index 0 in GEMSTART=2 mapping) + session.setInstrumentFilter(0); + + uint64_t total_packets = 0; + auto start = std::chrono::steady_clock::now(); + + while (g_running) { + auto wait_result = session.waitForData(500); + + cbPKT_GENERIC packets[64]; + size_t packets_read = 0; + auto read_result = session.readReceiveBuffer(packets, 64, packets_read); + + if (read_result.isOk() && packets_read > 0) { + total_packets += packets_read; + + // Print first packet details periodically + if (total_packets <= 10 || total_packets % 10000 == 0) { + auto& pkt = packets[0]; + std::cout << "[" << total_packets << "] " + << "time=" << pkt.cbpkt_header.time + << " chid=" << pkt.cbpkt_header.chid + << " type=0x" << std::hex << pkt.cbpkt_header.type << std::dec + << " dlen=" << pkt.cbpkt_header.dlen + << " inst=" << (int)pkt.cbpkt_header.instrument + << "\n"; + } + } + + auto now = std::chrono::steady_clock::now(); + int elapsed = std::chrono::duration_cast(now - start).count(); + if (elapsed > 0 && total_packets > 0) { + std::cout << "\r Packets: " << total_packets + << " (" << (total_packets / elapsed) << "/sec)" + << std::flush; + } + } + + std::cout << "\n\nTotal packets read: " << total_packets << "\n"; + std::cout << "Done.\n"; + return 0; +} diff --git a/examples/CheckProtocolVersion/check_protocol_version.cpp b/examples/CheckProtocolVersion/check_protocol_version.cpp new file mode 100644 index 00000000..cf0f9e93 --- /dev/null +++ b/examples/CheckProtocolVersion/check_protocol_version.cpp @@ -0,0 +1,174 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file check_protocol_version.cpp +/// @author CereLink Development Team +/// @date 2025-01-17 +/// +/// @brief Simple example demonstrating protocol version detection +/// +/// This example shows how to use the cbdev API to determine which protocol version a device +/// is using. It demonstrates: +/// - Creating a device session with automatic protocol detection +/// - Querying the detected protocol version +/// - Interpreting the detection result +/// - Handling detection errors +/// +/// Usage: +/// check_protocol_version [device_type] +/// +/// Arguments: +/// device_type - Device type to connect to (default: NSP) +/// Valid values: NSP, GEMINI_NSP, HUB1, HUB2, HUB3, NPLAY +/// +/// Examples: +/// check_protocol_version # Use defaults for NSP +/// check_protocol_version GEMINI_NSP # Connect to Gemini NSP +/// check_protocol_version NPLAY # Connect to nPlayServer +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include + +using namespace cbdev; + +void printUsage(const char* prog_name) { + std::cout << "Usage: " << prog_name << " [device_type]\n\n"; + std::cout << "Arguments:\n"; + std::cout << " device_type - Device type to connect to (default: NSP)\n"; + std::cout << " Valid values: NSP, GEMINI_NSP, HUB1, HUB2, HUB3, NPLAY\n\n"; + std::cout << "Examples:\n"; + std::cout << " " << prog_name << "\n"; + std::cout << " " << prog_name << " GEMINI_NSP\n"; + std::cout << " " << prog_name << " NPLAY\n"; +} + +DeviceType parseDeviceType(const char* str) { + std::string upper_str = str; + std::transform(upper_str.begin(), upper_str.end(), upper_str.begin(), + [](unsigned char c) { return std::toupper(c); }); + + if (upper_str == "NSP") return DeviceType::LEGACY_NSP; + if (upper_str == "GEMINI_NSP") return DeviceType::NSP; + if (upper_str == "HUB1") return DeviceType::HUB1; + if (upper_str == "HUB2") return DeviceType::HUB2; + if (upper_str == "HUB3") return DeviceType::HUB3; + if (upper_str == "NPLAY") return DeviceType::NPLAY; + + throw std::runtime_error("Invalid device type. Valid values: NSP, GEMINI_NSP, HUB1, HUB2, HUB3, NPLAY"); +} + +int main(int argc, char* argv[]) { + std::cout << "================================================\n"; + std::cout << " CereLink Protocol Version Detector\n"; + std::cout << "================================================\n\n"; + + // Parse command line arguments + DeviceType device_type = DeviceType::LEGACY_NSP; // Default to NSP + + if (argc > 1) { + if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) { + printUsage(argv[0]); + return 0; + } + + try { + device_type = parseDeviceType(argv[1]); + } catch (const std::exception& e) { + std::cerr << "ERROR: " << e.what() << "\n\n"; + printUsage(argv[0]); + return 1; + } + } + + // Create connection configuration for the specified device type + const ConnectionParams config = ConnectionParams::forDevice(device_type); + + // Display configuration + std::cout << "Configuration:\n"; + std::cout << " Device Type: "; + switch (device_type) { + case DeviceType::LEGACY_NSP: std::cout << "NSP (Legacy Neural Signal Processor)\n"; break; + case DeviceType::NSP: std::cout << "GEMINI_NSP (Gemini Neural Signal Processor)\n"; break; + case DeviceType::HUB1: std::cout << "HUB1 (Gemini Hub 1)\n"; break; + case DeviceType::HUB2: std::cout << "HUB2 (Gemini Hub 2)\n"; break; + case DeviceType::HUB3: std::cout << "HUB3 (Gemini Hub 3)\n"; break; + case DeviceType::NPLAY: std::cout << "NPLAY (nPlayServer)\n"; break; + default: std::cout << "CUSTOM\n"; break; + } + std::cout << " Device Address: " << config.device_address << "\n"; + std::cout << " Send Port: " << config.send_port << "\n"; + std::cout << " Client Address: " << config.client_address << "\n"; + std::cout << " Recv Port: " << config.recv_port << "\n\n"; + + // Create device session with automatic protocol detection + std::cout << "Detecting protocol version...\n"; + std::cout << " (Creating device session with auto-detection)\n\n"; + + auto result = createDeviceSession(config, ProtocolVersion::UNKNOWN); + + // Handle result + if (result.isError()) { + std::cerr << "ERROR: Device session creation failed\n"; + std::cerr << " Reason: " << result.error() << "\n\n"; + std::cerr << "Possible causes:\n"; + std::cerr << " - Device is not responding or is offline\n"; + std::cerr << " - Incorrect device type or network configuration\n"; + std::cerr << " - Network connectivity issue\n"; + std::cerr << " - Port already in use\n"; + return 1; + } + + // Query the detected protocol version + const auto device = std::move(result.value()); + const ProtocolVersion version = device->getProtocolVersion(); + + std::cout << "Protocol Detection Result:\n"; + std::cout << "==========================\n"; + std::cout << " Detected Version: " << protocolVersionToString(version) << "\n\n"; + + // Provide additional information based on version + switch (version) { + case ProtocolVersion::PROTOCOL_311: + std::cout << "Details:\n"; + std::cout << " - This device uses the legacy protocol 3.11\n"; + std::cout << " - Uses 32-bit timestamps and 8-bit packet types\n"; + std::cout << " - Requires special handling for compatibility\n"; + break; + + case ProtocolVersion::PROTOCOL_400: + std::cout << "Details:\n"; + std::cout << " - This device uses the legacy protocol 4.0\n"; + std::cout << " - Uses 64-bit timestamps and 8-bit packet types\n"; + std::cout << " - Requires special handling for compatibility\n"; + break; + + case ProtocolVersion::PROTOCOL_410: + std::cout << "Details:\n"; + std::cout << " - This device uses protocol 4.1\n"; + std::cout << " - Uses 64-bit timestamps and 16-bit packet types\n"; + std::cout << " - It differs only slightly from current. Upgrade recommended.\n"; + break; + + case ProtocolVersion::PROTOCOL_CURRENT: + std::cout << "Details:\n"; + std::cout << " - This device uses the current protocol (4.2+)\n"; + std::cout << " - Uses 64-bit timestamps and 16-bit packet types\n"; + std::cout << " - Recommended for all new development\n"; + break; + + case ProtocolVersion::UNKNOWN: + default: + std::cout << "Details:\n"; + std::cout << " - Device responded but protocol version could not be determined\n"; + std::cout << " - This may indicate an unsupported protocol version\n"; + break; + } + + std::cout << "\nDetection complete!\n"; + return 0; +} diff --git a/examples/GeminiExample/README.md b/examples/GeminiExample/README.md new file mode 100644 index 00000000..a87a9106 --- /dev/null +++ b/examples/GeminiExample/README.md @@ -0,0 +1,163 @@ +# Gemini Multi-Device Example + +This example demonstrates how to use the new cbsdk_v2 API to connect to multiple Gemini devices simultaneously. + +## Features + +- **Simplified Configuration**: Uses device type enums instead of manual IP/port configuration +- **Automatic Client Address Detection**: Platform-aware client address selection +- **Multi-Device Support**: Connects to both Gemini NSP and Hub1 +- **Packet Callbacks**: Real-time packet processing +- **Statistics Monitoring**: Track packets, queue depth, and errors + +## Building + +```bash +cd build +cmake .. +make gemini_example +``` + +## Running + +Simply run the executable - no command line arguments needed: + +```bash +./examples/gemini_example +``` + +The example will: +1. Create sessions for both Gemini NSP (192.168.137.128:51001) and Hub1 (192.168.137.200:51002) +2. Auto-detect the appropriate client address based on your platform: + - **macOS**: Binds to 0.0.0.0 (all interfaces) + - **Linux**: Searches for 192.168.137.x adapter, falls back to 0.0.0.0 + - **Windows**: Binds to 0.0.0.0 (safe default) +3. Start receiving packets from both devices +4. Display the first few packets from each device +5. Print statistics every 5 seconds +6. Run until you press Ctrl+C + +## Example Output + +``` +=========================================== + Gemini Multi-Device Example (cbsdk_v2) +=========================================== + +Configuring Gemini NSP... +Configuring Gemini Hub1... + +Creating NSP session... +Creating Hub1 session... + +Setting up packet callbacks... + +Starting NSP session... +Starting Hub1 session... + +=== Both devices connected! === +Press Ctrl+C to stop... + +[NSP] Packet type: 0x0001, dlen: 32 +[NSP] Packet type: 0x0010, dlen: 64 +[Hub1] Packet type: 0x0001, dlen: 32 +[Hub1] Packet type: 0x0020, dlen: 128 + +=== Packet Counts (via callbacks) === +NSP: 1234 packets +Hub1: 567 packets + +=== Gemini NSP Statistics === +Packets received: 1234 +Packets in shmem: 1234 +Packets queued: 1234 +Packets delivered: 1234 +Packets dropped: 0 +Queue depth: 45 / 128 (current/peak) +Errors (shmem): 0 +Errors (receive): 0 + +=== Gemini Hub1 Statistics === +Packets received: 567 +Packets in shmem: 567 +Packets queued: 567 +Packets delivered: 567 +Packets dropped: 0 +Queue depth: 23 / 89 (current/peak) +Errors (shmem): 0 +Errors (receive): 0 +``` + +## Key Code Sections + +### Simple Device Configuration + +```cpp +// Gemini NSP - just specify the device type! +cbsdk::SdkConfig nsp_config; +nsp_config.device_type = cbsdk::DeviceType::GEMINI_NSP; +nsp_config.shmem_name = "gemini_nsp"; + +// Gemini Hub1 +cbsdk::SdkConfig hub1_config; +hub1_config.device_type = cbsdk::DeviceType::GEMINI_HUB1; +hub1_config.shmem_name = "gemini_hub1"; +``` + +No need to specify IP addresses, ports, or client addresses - they're all auto-configured! + +### Packet Callbacks + +```cpp +nsp_session.setPacketCallback([&counter](const cbPKT_GENERIC* pkts, size_t count) { + counter.fetch_add(count); + // Process packets... +}); +``` + +### Error Handling + +```cpp +nsp_session.setErrorCallback([](const std::string& error) { + std::cerr << "[NSP ERROR] " << error << "\n"; +}); +``` + +## Advanced: Custom Configuration + +If you need non-standard network configuration, you can override the defaults: + +```cpp +cbsdk::SdkConfig config; +config.device_type = cbsdk::DeviceType::GEMINI_NSP; // Start with defaults + +// Override specific values +config.custom_device_address = "192.168.1.100"; // Custom device IP +config.custom_client_address = "192.168.1.50"; // Custom client IP +config.custom_device_port = 52001; // Custom port +``` + +## Device Types + +Available device types: +- `LEGACY_NSP` - Legacy NSP (192.168.137.128, ports 51001/51002) +- `GEMINI_NSP` - Gemini NSP (192.168.137.128, port 51001) +- `GEMINI_HUB1` - Gemini Hub 1 (192.168.137.200, port 51002) +- `GEMINI_HUB2` - Gemini Hub 2 (192.168.137.201, port 51003) +- `GEMINI_HUB3` - Gemini Hub 3 (192.168.137.202, port 51004) +- `NPLAY` - NPlay loopback (127.0.0.1, port 51001) + +## Troubleshooting + +**No packets received:** +- Ensure devices are powered on and network cables connected +- Check that you're on the 192.168.137.x subnet +- Verify firewall isn't blocking UDP ports 51001-51004 + +**Permission errors:** +- On Linux, you may need `sudo` for raw socket access +- On macOS, check System Preferences → Security & Privacy + +**Build errors:** +- Ensure you built cbsdk_v2: `make cbsdk_v2` +- Check that CMake configuration completed successfully diff --git a/examples/GeminiExample/gemini_example.cpp b/examples/GeminiExample/gemini_example.cpp new file mode 100644 index 00000000..e3b20354 --- /dev/null +++ b/examples/GeminiExample/gemini_example.cpp @@ -0,0 +1,327 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file gemini_example.cpp +/// @author CereLink Development Team +/// @date 2025-11-12 +/// +/// @brief Example demonstrating cbsdk_v2 with Gemini devices +/// +/// This example shows: +/// - Auto-discovery of all connected Gemini devices (NSP, Hub1, Hub2, Hub3) +/// - Simplified device configuration using DeviceType enum +/// - Automatic client address detection +/// - Setting up packet callbacks with timestamp analysis +/// - Monitoring statistics for all connected devices +/// - Proper shared memory cleanup +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// POSIX shared memory cleanup (macOS/Linux) +#ifndef _WIN32 + #include +#endif + +// Need full protocol definitions for device verification +extern "C" { + #include // Upstream version with full packet types +} + +// Global flag for graceful shutdown +std::atomic g_running{true}; + +void signalHandler(int signal) { + std::cout << "\nReceived signal " << signal << ", shutting down...\n"; + g_running.store(false); +} + +// Timestamp analysis structure +struct TimestampStats { + std::mutex mutex; + uint64_t last_timestamp = 0; + uint64_t interval_sum = 0; + uint64_t interval_sum_sq = 0; + uint64_t interval_count = 0; + + void addTimestamp(uint64_t ts) { + std::lock_guard lock(mutex); + if (last_timestamp > 0 && ts > last_timestamp) { + uint64_t interval = ts - last_timestamp; + interval_sum += interval; + interval_sum_sq += interval * interval; + interval_count++; + } + last_timestamp = ts; + } + + std::pair getMeanAndStdDev() { + std::lock_guard lock(mutex); + if (interval_count == 0) { + return {0.0, 0.0}; + } + double mean = static_cast(interval_sum) / interval_count; + double variance = (static_cast(interval_sum_sq) / interval_count) - (mean * mean); + double stddev = std::sqrt(variance); + return {mean, stddev}; + } +}; + +void printStats(const cbsdk::SdkSession& session, const std::string& name, TimestampStats* ts_stats = nullptr) { + auto stats = session.getStats(); + + std::cout << "\n=== " << name << " Statistics ===\n"; + std::cout << "Packets received: " << stats.packets_received_from_device << "\n"; + std::cout << "Packets in shmem: " << stats.packets_stored_to_shmem << "\n"; + std::cout << "Packets queued: " << stats.packets_queued_for_callback << "\n"; + std::cout << "Packets delivered: " << stats.packets_delivered_to_callback << "\n"; + std::cout << "Packets dropped: " << stats.packets_dropped << "\n"; + std::cout << "Queue depth: " << stats.queue_current_depth + << " / " << stats.queue_max_depth << " (current/peak)\n"; + std::cout << "Errors (shmem): " << stats.shmem_store_errors << "\n"; + std::cout << "Errors (receive): " << stats.receive_errors << "\n"; + + // Print timestamp interval statistics if available + if (ts_stats != nullptr) { + auto [mean, stddev] = ts_stats->getMeanAndStdDev(); + if (mean > 0) { + // Expected interval for 30k pkt/s: 1e9/30000 ≈ 33,333 ns + double expected_interval = 1e9 / 30000.0; + std::cout << "\nSample group (0x0006) timestamp intervals:\n"; + std::cout << " Mean: " << std::fixed << std::setprecision(1) << mean << " ns\n"; + std::cout << " Std Dev: " << std::fixed << std::setprecision(1) << stddev << " ns\n"; + std::cout << " Expected: " << std::fixed << std::setprecision(1) << expected_interval << " ns (for 30k pkt/s)\n"; + std::cout << " Rate from timestamps: " << std::fixed << std::setprecision(1) << (1e9 / mean) << " pkt/s\n"; + } + } +} + +// Device information structure +struct DeviceInfo { + std::string name; + cbsdk::DeviceType type; + std::unique_ptr session; + std::atomic packet_count{0}; + TimestampStats timestamps; + uint64_t last_count = 0; // For rate calculation +}; + +int main(int argc, char* argv[]) { + std::cout << "===========================================\n"; + std::cout << " Gemini Device Monitor (cbsdk_v2)\n"; + std::cout << "===========================================\n\n"; + + // Set up signal handler for graceful shutdown + std::signal(SIGINT, signalHandler); + std::signal(SIGTERM, signalHandler); + + // Define all possible Gemini devices to try + std::vector> devices; + + auto nsp = std::make_unique(); + nsp->name = "Gemini NSP"; + nsp->type = cbsdk::DeviceType::NSP; + devices.push_back(std::move(nsp)); + + auto hub1 = std::make_unique(); + hub1->name = "Gemini Hub1"; + hub1->type = cbsdk::DeviceType::HUB1; + devices.push_back(std::move(hub1)); + + auto hub2 = std::make_unique(); + hub2->name = "Gemini Hub2"; + hub2->type = cbsdk::DeviceType::HUB2; + devices.push_back(std::move(hub2)); + + auto hub3 = std::make_unique(); + hub3->name = "Gemini Hub3"; + hub3->type = cbsdk::DeviceType::HUB3; + devices.push_back(std::move(hub3)); + + try { + // === Clean up any stale shared memory segments === +#ifndef _WIN32 + // Forcefully unlink all possible shared memory segments to ensure STANDALONE mode + // POSIX requires shared memory names to start with "/" + // Use Central-compatible names: "cbCFGbuffer", "cbRECbuffer", "XmtGlobal", etc. + for (const auto& device : devices) { + std::string cfg_name; + std::string rec_name; + std::string xmt_name; + + // Map device type to Central-compatible shared memory names + switch (device->type) { + case cbsdk::DeviceType::LEGACY_NSP: + case cbsdk::DeviceType::NSP: + case cbsdk::DeviceType::NPLAY: + // Instance 0 uses base names without suffix + cfg_name = "cbCFGbuffer"; + rec_name = "cbRECbuffer"; + xmt_name = "XmtGlobal"; + break; + case cbsdk::DeviceType::HUB1: + cfg_name = "cbCFGbuffer1"; + rec_name = "cbRECbuffer1"; + xmt_name = "XmtGlobal1"; + break; + case cbsdk::DeviceType::HUB2: + cfg_name = "cbCFGbuffer2"; + rec_name = "cbRECbuffer2"; + xmt_name = "XmtGlobal2"; + break; + case cbsdk::DeviceType::HUB3: + cfg_name = "cbCFGbuffer3"; + rec_name = "cbRECbuffer3"; + xmt_name = "XmtGlobal3"; + break; + } + + std::string posix_cfg_name = "/" + cfg_name; + std::string posix_rec_name = "/" + rec_name; + std::string posix_xmt_name = "/" + xmt_name; + shm_unlink(posix_cfg_name.c_str()); // Ignore errors + shm_unlink(posix_rec_name.c_str()); // Ignore errors + shm_unlink(posix_xmt_name.c_str()); // Ignore errors + } +#endif + + // === Try to connect to all Gemini devices === + std::cout << "\nScanning for Gemini devices...\n\n"; + + std::vector active_devices; + + for (auto& device : devices) { + std::cout << "Trying " << device->name << "...\n"; + + cbsdk::SdkConfig config; + config.device_type = device->type; + + auto result = cbsdk::SdkSession::create(config); + if (result.isError()) { + std::cout << " ✗ Failed: " << result.error() << "\n\n"; + continue; + } + + device->session = std::make_unique(std::move(result.value())); + + // Set up packet callback + device->session->registerPacketCallback([dev = device.get()](const cbPKT_GENERIC& pkt) { + dev->packet_count.fetch_add(1); + + // Track timestamps for interval analysis (only sample group packets - type 0x0006) + if (pkt.cbpkt_header.type == 0x0006) { + dev->timestamps.addTimestamp(pkt.cbpkt_header.time); + } + + // Detect SYSREP packets (type 0x10-0x1F) + if ((pkt.cbpkt_header.type & 0xF0) == 0x10) { + const cbPKT_SYSINFO* sysinfo = reinterpret_cast(&pkt); + std::cout << "[" << dev->name << "] SYSREP packet received - runlevel: " + << sysinfo->runlevel << "\n"; + } + + // Print first few packets for demonstration (including instrument ID) + static std::map printed_counts; + if (printed_counts[dev->name] < 5) { + std::cout << "[" << dev->name << "] Packet type: 0x" + << std::hex << std::setw(4) << std::setfill('0') + << pkt.cbpkt_header.type << std::dec + << ", dlen: " << pkt.cbpkt_header.dlen + << ", instrument: " << static_cast(pkt.cbpkt_header.instrument) << "\n"; + printed_counts[dev->name]++; + } + }); + + // Set up error callback + device->session->setErrorCallback([name = device->name](const std::string& error) { + std::cerr << "[" << name << " ERROR] " << error << "\n"; + }); + + std::cout << " ✓ Connected and running\n\n"; + active_devices.push_back(device.get()); + } + + if (active_devices.empty()) { + std::cerr << "No Gemini devices found!\n"; + return 1; + } + + std::cout << "=== Found " << active_devices.size() << " device(s) ===\n"; + for (const auto* dev : active_devices) { + std::cout << " - " << dev->name << "\n"; + } + std::cout << "\nPress Ctrl+C to stop...\n\n"; + + // === Main Loop - Print Statistics Every 5 Seconds === + auto last_print = std::chrono::steady_clock::now(); + + while (g_running.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - last_print); + + if (elapsed.count() >= 5000) { + double elapsed_sec = elapsed.count() / 1000.0; + + // Print packet counts and rates + std::cout << "\n=== Packet Counts ===\n"; + for (auto* dev : active_devices) { + uint64_t current_count = dev->packet_count.load(); + double rate = (current_count - dev->last_count) / elapsed_sec; + std::cout << dev->name << ": " << current_count << " packets (" + << std::fixed << std::setprecision(1) << rate << " pkt/s)\n"; + } + + // Print detailed statistics for each device + for (auto* dev : active_devices) { + printStats(*dev->session, dev->name, &dev->timestamps); + } + + std::cout << "\nPress Ctrl+C to stop...\n"; + + // Update for next iteration + last_print = now; + for (auto* dev : active_devices) { + dev->last_count = dev->packet_count.load(); + } + } + } + + // === Clean Shutdown === + std::cout << "\nStopping sessions...\n"; + for (auto* dev : active_devices) { + dev->session->stop(); + } + + // Final statistics + std::cout << "\n=== Final Statistics ===\n"; + std::cout << "Total packets received:\n"; + for (auto* dev : active_devices) { + std::cout << " " << dev->name << ": " << dev->packet_count.load() << "\n"; + } + + for (auto* dev : active_devices) { + printStats(*dev->session, dev->name + " (Final)", &dev->timestamps); + } + + } catch (const std::exception& e) { + std::cerr << "Exception: " << e.what() << "\n"; + return 1; + } + + std::cout << "\nShutdown complete.\n"; + return 0; +} diff --git a/examples/Python/audio_monitor_chan.py b/examples/Python/audio_monitor_chan.py deleted file mode 100644 index 4bdc327a..00000000 --- a/examples/Python/audio_monitor_chan.py +++ /dev/null @@ -1,17 +0,0 @@ -# Make sure the CereLink project folder is not on the path, -# otherwise it will attempt to import cerebus from there, instead of the installed python package. -from cerebus import cbpy - - -MONITOR_CHAN = 2 # 1-based -AOUT_CHAN = 277 - -res, con_info = cbpy.open(parameter=cbpy.defaultConParams()) - -# Reset all analog output channels to not monitor anything -for chan_ix in range(273, 278): - res = cbpy.analog_out(chan_ix, None, track_last=False) - -# Set one analog output chan (277 = audio1) to monitor a single channel. -res = cbpy.analog_out(AOUT_CHAN, MONITOR_CHAN, track_last=False) -print(res) diff --git a/examples/Python/fetch_data_continuous.py b/examples/Python/fetch_data_continuous.py deleted file mode 100644 index 47d33571..00000000 --- a/examples/Python/fetch_data_continuous.py +++ /dev/null @@ -1,50 +0,0 @@ -# Make sure the CereLink/bindings/Python folder is not on the path, -# otherwise it will attempt to import cerelink from there, instead of the installed python package. -import time - -import numpy as np -from cerelink import cbpy - - -def main(): - group_idx = 6 - con_info = cbpy.open(parameter=cbpy.defaultConParams()) - - for g in range(1, 7): - # Disable all channels in all groups - chan_infos = cbpy.get_sample_group(g) - for ch_info in chan_infos: - cbpy.set_channel_config(ch_info["chan"], chaninfo={"smpgroup": 0}) - - for ch in range(1, 9): - # Enable first 8 channels in group 6, no dc offset - cbpy.set_channel_config(ch, chaninfo={"smpgroup": group_idx, "ainpopts": 0}) - - time.sleep(1.0) # Wait for settings to take effect - - chan_infos = cbpy.get_sample_group(group_idx) - n_chans = len(chan_infos) - n_buffer = 102400 - timestamps_buffer = np.zeros(n_buffer, dtype=np.uint64) - samples_buffer = np.zeros((n_buffer, n_chans), dtype=np.int16) - - cbpy.trial_config(activate=True, n_continuous=-1) - try: - while True: - # Subsequent calls: reuse the allocated buffers - data = cbpy.trial_continuous( - reset_clock=False, - seek=True, - group=group_idx, - timestamps=timestamps_buffer, - samples=samples_buffer, - num_samples=n_buffer, - ) - if data["num_samples"] > 0: - print(f"Fetched {data['num_samples']} samples, latest timestamp: {data['timestamps'][-1]}") - except KeyboardInterrupt: - cbpy.close() - - -if __name__ == "__main__": - main() diff --git a/examples/Python/fetch_data_events.py b/examples/Python/fetch_data_events.py deleted file mode 100644 index cd2d07da..00000000 --- a/examples/Python/fetch_data_events.py +++ /dev/null @@ -1,31 +0,0 @@ -# Make sure the CereLink project folder is not on the path, -# otherwise it will attempt to import cerebus from there, instead of the installed python package. -from cerebus import cbpy - - -res, con_info = cbpy.open(parameter=cbpy.defaultConParams()) -res, reset = cbpy.trial_config( - reset=True, - buffer_parameter={}, - range_parameter={}, - noevent=0, - nocontinuous=1, - nocomment=1 -) - -try: - spk_cache = {} - while True: - result, data = cbpy.trial_event(reset=True) - if len(data) > 0: - for ev in data: - chid = ev[0] - ev_dict = ev[1] - timestamps = ev_dict["timestamps"] - print(f"Ch {chid} unit 0 has {len(timestamps[0])} events.") - if chid not in spk_cache: - spk_cache[chid] = cbpy.SpikeCache(channel=chid) - temp_wfs, unit_ids = spk_cache[chid].get_new_waveforms() - print(f"Waveform shape: {temp_wfs.shape} on unit_ids {unit_ids}") -except KeyboardInterrupt: - cbpy.close() diff --git a/examples/Python/read_ccf.py b/examples/Python/read_ccf.py new file mode 100644 index 00000000..4377e940 --- /dev/null +++ b/examples/Python/read_ccf.py @@ -0,0 +1,230 @@ +""" +Read a Cerebus Configuration File (.ccf) offline — no device or pycbsdk needed. + +CCF files are XML. Each channel's configuration lives under +``/CCF/ChanInfo/ChanInfo_item``. This script shows common patterns for +extracting channel config fields, filtering by channel type, and bulk- +reading a single field across many channels. + +Usage: + python read_ccf.py path/to/config.ccf +""" + +from __future__ import annotations + +import sys +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from typing import Optional + + +# --------------------------------------------------------------------------- +# Channel-type classification (mirrors cbproto capability flags) +# --------------------------------------------------------------------------- + +# Capability flag bits (from cbproto) +_CHAN_EXISTS = 0x01 +_CHAN_CONNECTED = 0x02 +_CHAN_ISOLATED = 0x04 +_CHAN_AINP = 0x100 +_CHAN_AOUT = 0x200 +_CHAN_DINP = 0x400 +_CHAN_DOUT = 0x800 +_AOUT_AUDIO = 0x40 +_DINP_SERIAL = 0x0000FF00 # serial-capable mask + + +def classify_channel(chancaps: int, ainpcaps: int = 0, aoutcaps: int = 0, + dinpcaps: int = 0) -> Optional[str]: + """Classify a channel by its capability flags. + + Returns one of: "FRONTEND", "ANALOG_IN", "ANALOG_OUT", "AUDIO", + "DIGITAL_IN", "SERIAL", "DIGITAL_OUT", or None if not connected. + """ + if (_CHAN_EXISTS | _CHAN_CONNECTED) != (chancaps & (_CHAN_EXISTS | _CHAN_CONNECTED)): + return None + if (_CHAN_AINP | _CHAN_ISOLATED) == (chancaps & (_CHAN_AINP | _CHAN_ISOLATED)): + return "FRONTEND" + if _CHAN_AINP == (chancaps & (_CHAN_AINP | _CHAN_ISOLATED)): + return "ANALOG_IN" + if chancaps & _CHAN_AOUT: + return "AUDIO" if (aoutcaps & _AOUT_AUDIO) else "ANALOG_OUT" + if chancaps & _CHAN_DINP: + return "SERIAL" if (dinpcaps & _DINP_SERIAL) else "DIGITAL_IN" + if chancaps & _CHAN_DOUT: + return "DIGITAL_OUT" + return None + + +# --------------------------------------------------------------------------- +# XML helpers +# --------------------------------------------------------------------------- + +def _int_text(elem: Optional[ET.Element], default: int = 0) -> int: + """Get integer text content from an XML element.""" + return int(elem.text) if elem is not None and elem.text else default + + +def _str_text(elem: Optional[ET.Element], default: str = "") -> str: + """Get string text content from an XML element.""" + return elem.text if elem is not None and elem.text else default + + +# --------------------------------------------------------------------------- +# Channel info extraction +# --------------------------------------------------------------------------- + +@dataclass +class ChanInfo: + """Subset of channel config fields from a CCF file.""" + chan: int # 1-based channel number + label: str + bank: int + term: int + chancaps: int + ainpcaps: int + aoutcaps: int + dinpcaps: int + channel_type: Optional[str] + smpgroup: int # 0=disabled, 1-6 + smpfilter: int + spkfilter: int + ainpopts: int + spkopts: int + spkthrlevel: int + lncrate: int + refelecchan: int + position: tuple[int, int, int, int] + + +def parse_chaninfo(item: ET.Element) -> ChanInfo: + """Parse a single ``ChanInfo_item`` XML element.""" + chan = _int_text(item.find("chan")) + label = _str_text(item.find("label")) + bank = _int_text(item.find("bank")) + term = _int_text(item.find("term")) + + caps = item.find("caps") + chancaps = _int_text(caps.find("chancaps")) if caps is not None else 0 + ainpcaps = _int_text(caps.find("ainpcaps")) if caps is not None else 0 + aoutcaps = _int_text(caps.find("aoutcaps")) if caps is not None else 0 + dinpcaps = _int_text(caps.find("dinpcaps")) if caps is not None else 0 + + opts = item.find("options") + ainpopts = _int_text(opts.find("ainpopts")) if opts is not None else 0 + spkopts = _int_text(opts.find("spkopts")) if opts is not None else 0 + + sample = item.find("sample") + smpgroup = _int_text(sample.find("group")) if sample is not None else 0 + smpfilter = _int_text(sample.find("filter")) if sample is not None else 0 + + spike = item.find("spike") + spkfilter = _int_text(spike.find("filter")) if spike is not None else 0 + thr = spike.find("threshold") if spike is not None else None + spkthrlevel = _int_text(thr.find("level")) if thr is not None else 0 + + lnc = item.find("lnc") + lncrate = _int_text(lnc.find("rate")) if lnc is not None else 0 + + refelecchan = _int_text(item.find("refelecchan")) + + pos_elem = item.find("position") + if pos_elem is not None: + pos_items = pos_elem.findall("position_item") + position = tuple(_int_text(p) for p in pos_items[:4]) + while len(position) < 4: + position = position + (0,) + else: + position = (0, 0, 0, 0) + + return ChanInfo( + chan=chan, label=label, bank=bank, term=term, + chancaps=chancaps, ainpcaps=ainpcaps, aoutcaps=aoutcaps, + dinpcaps=dinpcaps, + channel_type=classify_channel(chancaps, ainpcaps, aoutcaps, dinpcaps), + smpgroup=smpgroup, smpfilter=smpfilter, spkfilter=spkfilter, + ainpopts=ainpopts, spkopts=spkopts, spkthrlevel=spkthrlevel, + lncrate=lncrate, refelecchan=refelecchan, position=position, + ) + + +def read_ccf(filepath: str) -> list[ChanInfo]: + """Parse all channel configs from a CCF file.""" + tree = ET.parse(filepath) + root = tree.getroot() + channels = [] + for item in root.findall("ChanInfo/ChanInfo_item"): + channels.append(parse_chaninfo(item)) + return channels + + +# --------------------------------------------------------------------------- +# Bulk field extraction (analogous to cbsdk get_channels_field) +# --------------------------------------------------------------------------- + +def get_channels_by_type(channels: list[ChanInfo], + channel_type: str) -> list[ChanInfo]: + """Filter channels by type string (e.g. "FRONTEND").""" + return [ch for ch in channels if ch.channel_type == channel_type] + + +def get_field(channels: list[ChanInfo], field: str) -> list: + """Extract a single field from a list of ChanInfo objects. + + Args: + channels: List of ChanInfo (e.g. from get_channels_by_type). + field: Attribute name (e.g. "smpgroup", "spkthrlevel", "label"). + + Returns: + List of field values, one per channel. + """ + return [getattr(ch, field) for ch in channels] + + +# --------------------------------------------------------------------------- +# Example usage +# --------------------------------------------------------------------------- + +def main(): + if len(sys.argv) < 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + + filepath = sys.argv[1] + channels = read_ccf(filepath) + print(f"Loaded {len(channels)} channels from {filepath}\n") + + # Show channel type distribution + type_counts: dict[Optional[str], int] = {} + for ch in channels: + type_counts[ch.channel_type] = type_counts.get(ch.channel_type, 0) + 1 + print("Channel types:") + for ct, n in sorted(type_counts.items(), key=lambda x: (x[0] is None, x[0] or "")): + print(f" {ct or '(disconnected)':15s} {n}") + print() + + # Example: get smpgroup for all FRONTEND channels + fe = get_channels_by_type(channels, "FRONTEND") + if fe: + groups = get_field(fe, "smpgroup") + print(f"FRONTEND channels ({len(fe)}):") + print(f" sample groups: {groups}") + print(f" labels: {get_field(fe, 'label')}") + + # Spike thresholds + thresholds = get_field(fe, "spkthrlevel") + print(f" spike thresholds: {thresholds}") + + # Positions + positions = get_field(fe, "position") + nonzero = [(ch.chan, ch.position) for ch in fe if any(p != 0 for p in ch.position)] + if nonzero: + print(f" channels with positions: {len(nonzero)}") + for chan_id, pos in nonzero[:5]: + print(f" ch {chan_id}: {pos}") + else: + print(" no positions set") + + +if __name__ == "__main__": + main() diff --git a/examples/Python/session_info.py b/examples/Python/session_info.py new file mode 100644 index 00000000..ecf31323 --- /dev/null +++ b/examples/Python/session_info.py @@ -0,0 +1,49 @@ +"""Connect to a device and print session info. + +Usage: + python session_info.py [device_type] + python session_info.py NPLAY + python session_info.py HUB1 +""" + +import sys +import time + +sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parents[1] / "pycbsdk" / "src")) +from pycbsdk import Session, DeviceType, ProtocolVersion +device_type = sys.argv[1] if len(sys.argv) > 1 else "NPLAY" + +print(f"Connecting to {device_type}...") +with Session(device_type=device_type) as session: + # Print a few raw timestamps from data packets + state = {"count": 0, "max": 10} + + @session.on_group() + def on_group(header, data): + if state["count"] < state["max"]: + t = header.time + print(f" group pkt: time={t:>20d} chid={header.chid} " + f"(as seconds: {t/1e9:.3f}s if ns, {t/30000:.3f}s if 30kHz counts)") + state["count"] += 1 + + @session.on_event(channel_type=None) + def on_event(header, data): + if state["count"] < state["max"]: + t = header.time + print(f" event pkt: time={t:>20d} chid={header.chid} type=0x{header.type:02x} " + f"(as seconds: {t/1e9:.3f}s if ns, {t/30000:.3f}s if 30kHz counts)") + state["count"] += 1 + + # Wait for handshake / clock sync + time.sleep(2) + + print(f"\nSession info:") + print(f" running: {session.running}") + print(f" protocol_version: {session.protocol_version!r}") + print(f" proc_ident: {session.proc_ident!r}") + print(f" runlevel: {session.runlevel}") + print(f" spike_length: {session.spike_length}") + print(f" spike_pretrigger: {session.spike_pretrigger}") + print(f" clock_offset_ns: {session.clock_offset_ns}") + print(f" clock_uncertainty: {session.clock_uncertainty_ns}") + print(f" stats: {session.stats}") diff --git a/examples/Python/test_recording.py b/examples/Python/test_recording.py new file mode 100644 index 00000000..ec670f52 --- /dev/null +++ b/examples/Python/test_recording.py @@ -0,0 +1,44 @@ +"""Test Central recording start/stop via CereLink. + +Requires: + - Central running with a device connected + - libcbsdk.dll built (CBSDK_LIB_PATH env var or on PATH) + +Usage: + python test_recording.py [device_type] + python test_recording.py HUB1 +""" + +import sys +import time + +sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parents[1] / "pycbsdk" / "src")) +from pycbsdk import Session + +device_type = sys.argv[1] if len(sys.argv) > 1 else "HUB1" + +print(f"Creating session ({device_type})...") +session = Session(device_type=device_type) +time.sleep(2) +print(f"Connected. running={session.running}\n") + +# Start recording +filename = "cerelink_test" +comment = "CereLink recording test" +print(f"Starting Central recording: filename='{filename}', comment='{comment}'") +session.start_central_recording(filename, comment) +print(" -> start_central_recording() returned OK") +print(" Check Central: file.exe should be recording now.\n") + +# Record for 5 seconds +print("Recording for 5 seconds...") +time.sleep(5) + +# Stop recording +print("Stopping Central recording...") +session.stop_central_recording() +print(" -> stop_central_recording() returned OK") +print(" Check Central: file.exe should have stopped.\n") + +session.close() +print("Done.") diff --git a/examples/RecordingTest/recording_test.cpp b/examples/RecordingTest/recording_test.cpp new file mode 100644 index 00000000..ecc11b27 --- /dev/null +++ b/examples/RecordingTest/recording_test.cpp @@ -0,0 +1,199 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file recording_test.cpp +/// @brief Test Central recording start/stop +/// +/// Usage: +/// recording_test [device_type] +/// recording_test HUB1 +/// +/// Requires Central to be running with a device connected. +/// +/// Uses the two-step sequence from the old cbsdk/cbpy: +/// 1. openCentralFileDialog() — sends cbFILECFG_OPT_OPEN +/// 2. sleep 250ms — Central needs time to open the dialog +/// 3. startCentralRecording() — sends recording=1 +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace cbsdk; + +DeviceType parseDeviceType(const std::string& s) { + if (s == "NSP") return DeviceType::LEGACY_NSP; + if (s == "GEMINI_NSP") return DeviceType::NSP; + if (s == "HUB1") return DeviceType::HUB1; + if (s == "HUB2") return DeviceType::HUB2; + if (s == "HUB3") return DeviceType::HUB3; + if (s == "NPLAY") return DeviceType::NPLAY; + std::cerr << "Unknown device type: " << s << "\n"; + std::exit(1); +} + +static const char* filecfgOptName(uint32_t opt) { + switch (opt) { + case cbFILECFG_OPT_NONE: return "NONE"; + case cbFILECFG_OPT_KEEPALIVE: return "KEEPALIVE"; + case cbFILECFG_OPT_REC: return "REC"; + case cbFILECFG_OPT_STOP: return "STOP"; + case cbFILECFG_OPT_NMREC: return "NMREC"; + case cbFILECFG_OPT_CLOSE: return "CLOSE"; + case cbFILECFG_OPT_SYNCH: return "SYNCH"; + case cbFILECFG_OPT_OPEN: return "OPEN"; + case cbFILECFG_OPT_TIMEOUT: return "TIMEOUT"; + case cbFILECFG_OPT_PAUSE: return "PAUSE"; + default: return "UNKNOWN"; + } +} + +/// Helper: wait for a REPFILECFG response up to timeout_ms +static bool waitForFileCfgResponse(std::atomic& count, int initial, int timeout_ms) { + for (int i = 0; i < timeout_ms / 100; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (count.load() > initial) + return true; + } + return false; +} + +int main(int argc, char* argv[]) { + std::string device = argc > 1 ? argv[1] : "HUB1"; + DeviceType dt = parseDeviceType(device); + + SdkConfig config; + config.device_type = dt; + config.autorun = true; + + std::cerr << "Creating session (" << device << ")...\n"; + auto result = SdkSession::create(config); + if (result.isError()) { + std::cerr << "ERROR: " << result.error() << "\n"; + return 1; + } + auto session = std::move(result.value()); + + // Register callback for REPFILECFG to verify Central's response + std::atomic filecfg_count{0}; + std::mutex filecfg_mutex; + std::string last_filecfg_info; + + auto filecfg_handle = session.registerConfigCallback(cbPKTTYPE_REPFILECFG, + [&](const cbPKT_GENERIC& pkt) { + cbPKT_FILECFG filecfg; + std::memcpy(&filecfg, &pkt, std::min(sizeof(filecfg), sizeof(pkt))); + filecfg_count++; + std::lock_guard lock(filecfg_mutex); + last_filecfg_info = std::string("options=") + filecfgOptName(filecfg.options) + + " recording=" + std::to_string(filecfg.recording) + + " filename='" + std::string(filecfg.filename, strnlen(filecfg.filename, sizeof(filecfg.filename))) + "'"; + }); + + // Also count total packets to verify we're receiving data + std::atomic total_packets{0}; + auto pkt_handle = session.registerPacketCallback([&](const cbPKT_GENERIC&) { + total_packets++; + }); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + std::cerr << "Connected. isRunning=" << session.isRunning() << "\n"; + std::cerr << "Packets received in 2s: " << total_packets.load() << "\n"; + std::cerr << "FILECFG responses so far: " << filecfg_count.load() << "\n\n"; + + // Step 1: Open file dialog (required by Central before starting recording) + std::cerr << "Step 1: Opening Central File Storage dialog...\n"; + auto r0 = session.openCentralFileDialog(); + if (r0.isError()) { + std::cerr << "openCentralFileDialog FAILED: " << r0.error() << "\n"; + session.stop(); + return 1; + } + + int initial_count = filecfg_count.load(); + bool got_response = waitForFileCfgResponse(filecfg_count, initial_count, 5000); + if (got_response) { + std::lock_guard lock(filecfg_mutex); + std::cerr << " -> Central responded: " << last_filecfg_info << "\n"; + } else { + std::cerr << " -> No REPFILECFG response after 5s (continuing anyway).\n"; + } + + // Step 2: Wait for dialog to initialize (empirically required, 250ms minimum) + std::cerr << " Waiting 500ms for dialog...\n"; + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // Step 3: Start recording (use user's home directory to avoid write permission issues) + std::string rec_filename = "cerelink_test"; + const char* userprofile = getenv("USERPROFILE"); + if (userprofile) { + rec_filename = std::string(userprofile) + "\\Documents\\cerelink_test"; + } + std::cerr << "\nStep 2: Starting Central recording (filename='" << rec_filename << "')...\n"; + initial_count = filecfg_count.load(); + auto r1 = session.startCentralRecording(rec_filename, "CereLink C++ test"); + if (r1.isError()) { + std::cerr << "startCentralRecording FAILED: " << r1.error() << "\n"; + session.stop(); + return 1; + } + + got_response = waitForFileCfgResponse(filecfg_count, initial_count, 5000); + if (got_response) { + std::lock_guard lock(filecfg_mutex); + std::cerr << " -> Central responded: " << last_filecfg_info << "\n\n"; + } else { + std::cerr << " -> No REPFILECFG response after 5s.\n"; + std::cerr << " Total packets received: " << total_packets.load() << "\n\n"; + } + + std::cerr << "Recording for 5 seconds...\n"; + std::this_thread::sleep_for(std::chrono::seconds(5)); + + // Stop recording + std::cerr << "Step 3: Stopping Central recording...\n"; + initial_count = filecfg_count.load(); + auto r2 = session.stopCentralRecording(); + if (r2.isError()) { + std::cerr << "stopCentralRecording FAILED: " << r2.error() << "\n"; + session.stop(); + return 1; + } + + got_response = waitForFileCfgResponse(filecfg_count, initial_count, 5000); + if (got_response) { + std::lock_guard lock(filecfg_mutex); + std::cerr << " -> Central responded: " << last_filecfg_info << "\n\n"; + } else { + std::cerr << " -> No REPFILECFG response after 5s.\n\n"; + } + + // Step 4: Try to close the File Storage dialog + std::cerr << "Step 4: Closing Central File Storage dialog...\n"; + initial_count = filecfg_count.load(); + auto r3 = session.closeCentralFileDialog(); + if (r3.isError()) { + std::cerr << "closeCentralFileDialog FAILED: " << r3.error() << "\n"; + } else { + got_response = waitForFileCfgResponse(filecfg_count, initial_count, 5000); + if (got_response) { + std::lock_guard lock(filecfg_mutex); + std::cerr << " -> Central responded: " << last_filecfg_info << "\n\n"; + } else { + std::cerr << " -> No REPFILECFG response after 5s.\n\n"; + } + } + + std::cerr << "Total packets received: " << total_packets.load() << "\n"; + std::cerr << "Total FILECFG responses: " << filecfg_count.load() << "\n"; + + session.unregisterCallback(filecfg_handle); + session.unregisterCallback(pkt_handle); + session.stop(); + std::cerr << "Done.\n"; + return 0; +} diff --git a/examples/SDKSample/SDKSample.cpp b/examples/SDKSample/SDKSample.cpp deleted file mode 100755 index c10ea960..00000000 --- a/examples/SDKSample/SDKSample.cpp +++ /dev/null @@ -1,77 +0,0 @@ -// =STS=> SDKSample.cpp[4270].aa00 closed SMID:1 -///////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2011 - 2012 Blackrock Microsystems -// -// $Workfile: SDKSample.cpp $ -// $Archive: /Cerebus/Human/WindowsApps/SDKSample/SDKSample.cpp $ -// $Revision: 1 $ -// $Date: 3/29/11 9:23a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -///////////////////////////////////////////////////////////////////////////// -// -// PURPOSE: cbmex SDK example application -// -///////////////////////////////////////////////////////////////////////////// - -#include "stdafx.h" -#include -#include "SDKSample.h" -#include "SDKSampleDlg.h" - -#ifdef _DEBUG -#define new DEBUG_NEW -#undef THIS_FILE -static char THIS_FILE[] = __FILE__; -#endif - -// CSDKSampleApp - -BEGIN_MESSAGE_MAP(CSDKSampleApp, CWinApp) - ON_COMMAND(ID_HELP, &CWinApp::OnHelp) -END_MESSAGE_MAP() - - -// CSDKSampleApp construction - -CSDKSampleApp::CSDKSampleApp() -{ - // TODO: add construction code here, - // Place all significant initialization in InitInstance -} - - -// The one and only CExampleApp object - -CSDKSampleApp theApp; - - -// CSDKSampleApp initialization - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Initialize SDK example application, -// and make sure at most one instance runs. -// Outputs: -// returns the error code -BOOL CSDKSampleApp::InitInstance() -{ - CMutex cbSDKSampleAppMutex(TRUE, "cbSDKSampleAppMutex"); - CSingleLock cbSDKSampleLock(&cbSDKSampleAppMutex); - - // We let only one instance of nPlay GUI - if (!cbSDKSampleLock.Lock(0)) - return FALSE; - - CWinApp::InitInstance(); - - CSDKSampleDlg dlg; - m_pMainWnd = &dlg; - dlg.DoModal(); - - // Since the dialog has been closed, return FALSE so that we exit the - // application, rather than start the application's message pump. - return FALSE; -} diff --git a/examples/SDKSample/SDKSample.h b/examples/SDKSample/SDKSample.h deleted file mode 100755 index 051f3a72..00000000 --- a/examples/SDKSample/SDKSample.h +++ /dev/null @@ -1,49 +0,0 @@ -/* =STS=> SDKSample.h[4271].aa00 closed SMID:1 */ -///////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2011 - 2012 Blackrock Microsystems -// -// $Workfile: SDKSample.cpp $ -// $Archive: /Cerebus/Human/WindowsApps/SDKSample/SDKSample.h $ -// $Revision: 1 $ -// $Date: 3/29/11 9:23a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -///////////////////////////////////////////////////////////////////////////// -// -// PURPOSE: cbmex SDK Sample application -// -///////////////////////////////////////////////////////////////////////////// - - -#ifndef SDKSAMPLE_H_INCLUDED -#define SDKSAMPLE_H_INCLUDED - -#if _MSC_VER > 1000 -#pragma once - -#endif - -#ifndef __AFXWIN_H__ - #error "include 'stdafx.h' before including this file for PCH" -#endif - -#include "resource.h" // main symbols - -class CSDKSampleApp : public CWinApp -{ -public: - CSDKSampleApp(); - -// Overrides - public: - virtual BOOL InitInstance(); - -// Implementation - - DECLARE_MESSAGE_MAP() -}; - -#endif // #ifndef SDKSAMPLE_H_INCLUDED diff --git a/examples/SDKSample/SDKSample.rc b/examples/SDKSample/SDKSample.rc deleted file mode 100755 index 6e5973a3..00000000 --- a/examples/SDKSample/SDKSample.rc +++ /dev/null @@ -1,205 +0,0 @@ -// =STS=> SDKSample.rc[4272].aa00 closed SMID:1 -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "afxres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (U.S.) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -#ifdef _WIN32 -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) -#endif //_WIN32 - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""afxres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "#define _AFX_NO_SPLITTER_RESOURCES\r\n" - "#define _AFX_NO_OLE_RESOURCES\r\n" - "#define _AFX_NO_TRACKER_RESOURCES\r\n" - "#define _AFX_NO_PROPERTY_RESOURCES\r\n" - "\r\n" - "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\r\n" - "LANGUAGE 9, 1\r\n" - "#pragma code_page(1252)\r\n" - "#include ""res\\SDKSample.rc2"" // non-Microsoft Visual C++ edited resources\r\n" - "#include ""afxres.rc"" // Standard components\r\n" - "#endif\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDR_MAINFRAME ICON "res\\SDKSample.ico" - -///////////////////////////////////////////////////////////////////////////// -// -// Dialog -// - -IDD_EXAMPLE_DIALOG DIALOGEX 0, 0, 300, 200 -STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU -EXSTYLE WS_EX_APPWINDOW -CAPTION "SDKSample" -FONT 8, "MS Shell Dlg", 0, 0, 0x1 -BEGIN - LTEXT "",IDC_STATIC_STATUS,7,183,286,10,SS_SUNKEN - PUSHBUTTON "Connect",IDC_BTN_CONNECT,7,7,50,14 - PUSHBUTTON "Disconnect",IDC_BTN_DISCONNECT,65,7,50,14 - PUSHBUTTON "Spikes",IDC_BTN_SPIKES,123,7,50,14 - CONTROL "",IDC_PICT_SPIKES,"Static",SS_BLACKRECT,27,33,266,142,WS_EX_CLIENTEDGE - LTEXT "Channel",IDC_STATIC,181,9,27,8,SS_CENTERIMAGE - COMBOBOX IDC_CHIDCOMBO,216,7,55,312,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - CONTROL "",IDC_THRESHOLD_SLIDER,"msctls_trackbar32",TBS_VERT | TBS_NOTICKS | WS_TABSTOP,7,27,15,153 -END - -IDD_ABOUTBOX DIALOGEX 0, 0, 207, 70 -STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "About SDK Sample" -FONT 8, "MS Sans Serif", 0, 0, 0x1 -BEGIN - ICON IDR_MAINFRAME,IDC_STATIC,5,10,20,20,SS_SUNKEN - CTEXT "cbmex SDK",IDC_STATIC_APP_VERSION,30,5,150,10,SS_NOPREFIX | SS_CENTERIMAGE - PUSHBUTTON "OK",IDOK,180,10,20,20 - CTEXT "built with Hardware Library",IDC_STATIC_LIB_VERSION,30,15,150,10,SS_NOPREFIX | SS_CENTERIMAGE - CTEXT "Copyright (C) 2011-2012 Blackrock Microsystems",IDC_STATIC,30,30,160,10,SS_NOPREFIX | SS_CENTERIMAGE - CTEXT "NSP Firmware",IDC_STATIC_NSP_APP_VERSION,30,45,150,10,SS_NOPREFIX | SS_CENTERIMAGE - CTEXT "build with Hardware Library",IDC_STATIC_NSP_LIB_VERSION,30,55,150,10,SS_NOPREFIX | SS_CENTERIMAGE -END - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,0,0,1 - PRODUCTVERSION 1,0,0,1 - FILEFLAGSMASK 0x3fL -#ifdef _DEBUG - FILEFLAGS 0x1L -#else - FILEFLAGS 0x0L -#endif - FILEOS 0x4L - FILETYPE 0x1L - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "Blackrock Microsystems" - VALUE "FileDescription", "cbmex SDK Sample" - VALUE "FileVersion", "1.0.0.1" - VALUE "InternalName", "SDKSample.exe" - VALUE "LegalCopyright", "Copyright Blackrock Microsystems 2011" - VALUE "OriginalFilename", "SDKSample.exe" - VALUE "ProductName", "cbmex SDK Sample" - VALUE "ProductVersion", "1.0.0.1" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - - -///////////////////////////////////////////////////////////////////////////// -// -// DESIGNINFO -// - -#ifdef APSTUDIO_INVOKED -GUIDELINES DESIGNINFO -BEGIN - IDD_EXAMPLE_DIALOG, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 293 - TOPMARGIN, 7 - BOTTOMMARGIN, 193 - END - - IDD_ABOUTBOX, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 179 - TOPMARGIN, 7 - END -END -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// String Table -// - -STRINGTABLE -BEGIN - IDS_ABOUTBOX "&About SDK Sample..." -END - -#endif // English (U.S.) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// -#define _AFX_NO_SPLITTER_RESOURCES -#define _AFX_NO_OLE_RESOURCES -#define _AFX_NO_TRACKER_RESOURCES -#define _AFX_NO_PROPERTY_RESOURCES - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE 9, 1 -#pragma code_page(1252) -#include "res\SDKSample.rc2" // non-Microsoft Visual C++ edited resources -#include "afxres.rc" // Standard components -#endif - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - diff --git a/examples/SDKSample/SDKSample.vcproj b/examples/SDKSample/SDKSample.vcproj deleted file mode 100755 index 2b17d3ba..00000000 --- a/examples/SDKSample/SDKSample.vcproj +++ /dev/null @@ -1,480 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/SDKSample/SDKSampleDlg.cpp b/examples/SDKSample/SDKSampleDlg.cpp deleted file mode 100755 index 86da4381..00000000 --- a/examples/SDKSample/SDKSampleDlg.cpp +++ /dev/null @@ -1,513 +0,0 @@ -// =STS=> SDKSampleDlg.cpp[4274].aa05 open SMID:5 -///////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2011 - 2012 Blackrock Microsystems -// -// $Workfile: SDKSampleDlg.cpp $ -// $Archive: /Cerebus/Human/WindowsApps/SDKSample/SDKSampleDlg.cpp $ -// $Revision: 1 $ -// $Date: 3/29/11 9:23a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -///////////////////////////////////////////////////////////////////////////// -// -// PURPOSE: Cerebus SDK example dialog -// -///////////////////////////////////////////////////////////////////////////// - -#include "stdafx.h" -#include "SDKSample.h" -#include "SDKSampleDlg.h" -#include - -#ifdef _DEBUG -#define new DEBUG_NEW -#endif - -// Author & Date: Almut Branner 12 May 2003 -// Purpose: Replace this static control with this window -// Inputs: -// rcParentWindow - the window this is going to be placed into -// rcNewWindow - the window that is to be created -// nControlID - the ID of the control (from the resource editor) -void ReplaceWindowControl(const CWnd &rcParentWindow, CWnd &rcNewWindow, int nControlID) -{ - CWnd *pStatic = rcParentWindow.GetDlgItem(nControlID); - - // For debug mode - ASSERT(pStatic != 0); - - // For released code - if (pStatic == 0) - return; - - CRect rctWindowSize; - - DWORD frmstyle = pStatic->GetStyle(); - DWORD frmexstyle = pStatic->GetExStyle(); - - pStatic->GetWindowRect(rctWindowSize); // Get window coord. - rcParentWindow.ScreenToClient(rctWindowSize); // change to client coord. - pStatic->DestroyWindow(); - - CWnd *pParent = const_cast(&rcParentWindow); - rcNewWindow.CreateEx(frmexstyle, NULL, NULL, frmstyle, rctWindowSize, pParent, nControlID); - - // Use for debugging - // AllocConsole(); -} - - -CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD) -{ -} - -void CAboutDlg::DoDataExchange(CDataExchange* pDX) -{ - CDialog::DoDataExchange(pDX); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Show version information in about dialog -BOOL CAboutDlg::OnInitDialog() -{ - CString strOld, strNew; - CDialog::OnInitDialog(); - cbSdkVersion ver; - cbSdkResult res = cbSdkGetVersion(0, &ver); - - GetDlgItemText(IDC_STATIC_APP_VERSION, strOld); - strNew.Format("%s v%u.%02u.%02u.%02u", strOld, ver.major, ver.minor, ver.release, ver.beta); - SetDlgItemText(IDC_STATIC_APP_VERSION, strNew); - - GetDlgItemText(IDC_STATIC_LIB_VERSION, strOld); - strNew.Format("%s v%u.%02u", strOld, ver.majorp, ver.minorp); - SetDlgItemText(IDC_STATIC_LIB_VERSION, strNew); - - GetDlgItemText(IDC_STATIC_NSP_APP_VERSION, strOld); - - cbSdkConnectionType conType; - cbSdkInstrumentType instType; - // Return the actual openned connection - if (res == CBSDKRESULT_SUCCESS) - { - res = cbSdkGetType(0, &conType, &instType); - if (res == CBSDKRESULT_SUCCESS) - { - if (instType != CBSDKINSTRUMENT_LOCALNSP && instType != CBSDKINSTRUMENT_NSP) - strOld = "nPlay"; - if (conType == CBSDKCONNECTION_CENTRAL) - strOld += "(Central)"; - } - strNew.Format("%s v%u.%02u.%02u.%02u", strOld, ver.nspmajor, ver.nspminor, ver.nsprelease, ver.nspbeta); - SetDlgItemText(IDC_STATIC_NSP_APP_VERSION, strNew); - - GetDlgItemText(IDC_STATIC_NSP_LIB_VERSION, strOld); - strNew.Format("%s v%u.%02u", strOld, ver.nspmajorp, ver.nspminorp); - SetDlgItemText(IDC_STATIC_NSP_LIB_VERSION, strNew); - } else { - SetDlgItemText(IDC_STATIC_NSP_APP_VERSION, strOld + " not connected"); - SetDlgItemText(IDC_STATIC_NSP_LIB_VERSION, ""); - } - - return TRUE; -} - -BEGIN_MESSAGE_MAP(CAboutDlg, CDialog) -END_MESSAGE_MAP() - -// Author & Date: Ehsan Azar 29 March 2011 -// CSDKSampleDlg dialog constructor -CSDKSampleDlg::CSDKSampleDlg(CWnd* pParent /*=NULL*/) - : CDialog(CSDKSampleDlg::IDD, pParent), - m_channel(1), m_threshold(-65 * 4) -{ - m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); -} - -void CSDKSampleDlg::DoDataExchange(CDataExchange* pDX) -{ - CDialog::DoDataExchange(pDX); - DDX_Control(pDX, IDC_STATIC_STATUS, m_status); - DDX_Control(pDX, IDC_CHIDCOMBO, m_cboChannel); - DDX_Control(pDX, IDC_THRESHOLD_SLIDER, m_sldThresh); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Show error message -void CSDKSampleDlg::PrintError(cbSdkResult res) -{ - switch(res) - { - case CBSDKRESULT_WARNCLOSED: - SetStatusWindow("Library is already closed"); - break; - case CBSDKRESULT_WARNOPEN: - SetStatusWindow("Library is already opened"); - break; - case CBSDKRESULT_SUCCESS: - SetStatusWindow("Success"); - break; - case CBSDKRESULT_NOTIMPLEMENTED: - SetStatusWindow("Not implemented"); - break; - case CBSDKRESULT_INVALIDPARAM: - SetStatusWindow("Invalid parameter"); - break; - case CBSDKRESULT_CLOSED: - SetStatusWindow("Interface is closed cannot do this operation"); - break; - case CBSDKRESULT_OPEN: - SetStatusWindow("Interface is open cannot do this operation"); - break; - case CBSDKRESULT_NULLPTR: - SetStatusWindow("Null pointer"); - break; - case CBSDKRESULT_ERROPENCENTRAL: - SetStatusWindow("NUnable to open Central interface"); - break; - case CBSDKRESULT_ERROPENUDP: - SetStatusWindow("Unable to open UDP interface (might happen if default)"); - break; - case CBSDKRESULT_ERROPENUDPPORT: - SetStatusWindow("Unable to open UDP port"); - break; - case CBSDKRESULT_ERRMEMORYTRIAL: - SetStatusWindow("Unable to allocate RAM for trial cache data"); - break; - case CBSDKRESULT_ERROPENUDPTHREAD: - SetStatusWindow("Unable to open UDP timer thread"); - break; - case CBSDKRESULT_ERROPENCENTRALTHREAD: - SetStatusWindow("Unable to open Central communication thread"); - break; - case CBSDKRESULT_INVALIDCHANNEL: - SetStatusWindow("Invalid channel number"); - break; - case CBSDKRESULT_INVALIDCOMMENT: - SetStatusWindow("Comment too long or invalid"); - break; - case CBSDKRESULT_INVALIDFILENAME: - SetStatusWindow("Filename too long or invalid"); - break; - case CBSDKRESULT_INVALIDCALLBACKTYPE: - SetStatusWindow("Invalid callback type"); - break; - case CBSDKRESULT_CALLBACKREGFAILED: - SetStatusWindow("Callback register/unregister failed"); - break; - case CBSDKRESULT_ERRCONFIG: - SetStatusWindow("Trying to run an unconfigured method"); - break; - case CBSDKRESULT_INVALIDTRACKABLE: - SetStatusWindow("Invalid trackable id, or trackable not present"); - break; - case CBSDKRESULT_INVALIDVIDEOSRC: - SetStatusWindow("Invalid video source id, or video source not present"); - break; - case CBSDKRESULT_UNKNOWN: - SetStatusWindow("Unknown error"); - break; - case CBSDKRESULT_ERROPENFILE: - SetStatusWindow("Cannot open file"); - break; - case CBSDKRESULT_ERRFORMATFILE: - SetStatusWindow("Wrong file format"); - break; - case CBSDKRESULT_OPTERRUDP: - SetStatusWindow("Socket option error (possibly permission issue)"); - break; - case CBSDKRESULT_MEMERRUDP: - SetStatusWindow("Unable to assign UDP interface memory"); - break; - case CBSDKRESULT_INVALIDINST: - SetStatusWindow("Invalid range or instrument address"); - break; - case CBSDKRESULT_ERRMEMORY: - SetStatusWindow("Memory allocation error"); - break; - case CBSDKRESULT_ERRINIT: - SetStatusWindow("Initialization error"); - break; - case CBSDKRESULT_TIMEOUT: - SetStatusWindow("Conection timeout error"); - break; - case CBSDKRESULT_BUSY: - SetStatusWindow("Resource is busy"); - break; - default: - SetStatusWindow("Unexpected error"); - } -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Set last status message -void CSDKSampleDlg::SetStatusWindow(const char * statusmsg) -{ - m_status.SetWindowText(statusmsg); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Set channel to listen to -// Inputs: -// channel - channel number (1-based) -void CSDKSampleDlg::SetChannel(UINT16 channel) -{ - m_channel = channel; - cbPKT_CHANINFO chaninfo; - cbSdkResult res; - res = cbSdkGetChannelConfig(0, channel, &chaninfo); - if (res != CBSDKRESULT_SUCCESS) - { - PrintError(res); - return; - } - m_threshold = chaninfo.spkthrlevel; - m_sldThresh.SetPos(-m_threshold / 4); - UINT32 spklength; - res = cbSdkGetSysConfig(0, &spklength); - m_spkPict.m_spklength = spklength; - if (res != CBSDKRESULT_SUCCESS) - { - PrintError(res); - return; - } -} - -// Author & Date: Ehsan Azar 26 April 2012 -// Purpose: Add spike to the the cache to be drawn later -// Inputs: -// spk - spike packet -void CSDKSampleDlg::AddSpike(cbPKT_SPK & spk) -{ - // Only for one channel draw the spikes - if (spk.chid == m_channel) - m_spkPict.AddSpike(spk); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Set threshold based on slider position -// Inputs: -// pos - threshold slider position -void CSDKSampleDlg::SetThreshold(int pos) -{ - INT32 newthreshold = pos * 4; - if (m_threshold == newthreshold) - return; - cbPKT_CHANINFO chaninfo; - cbSdkResult res; - res = cbSdkGetChannelConfig(0, m_channel, &chaninfo); - if (res != CBSDKRESULT_SUCCESS) - { - PrintError(res); - return; - } - chaninfo.spkthrlevel = newthreshold; - res = cbSdkSetChannelConfig(0, m_channel, &chaninfo); - if (res != CBSDKRESULT_SUCCESS) - { - PrintError(res); - return; - } - m_threshold = newthreshold; -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Callback to receive spike data -// Inputs: -// type - type of the event -// pEventData - points to a cbPkt_* structure depending on the type -// pCallbackData - is what is used to register the callback -void CSDKSampleDlg::SpikesCallback(UINT32 nInstacne, const cbSdkPktType type, const void* pEventData, void* pCallbackData) -{ - CSDKSampleDlg * pDlg = reinterpret_cast(pCallbackData); - switch(type) - { - case cbSdkPkt_PACKETLOST: - break; - case cbSdkPkt_SPIKE: - if (pDlg && pEventData) - { - cbPKT_SPK spk = *reinterpret_cast(pEventData); - // Note: Callback should return fast, so it is better to buffer it here - // and use another thread to handle data - pDlg->AddSpike(spk); - } - break; - default: - break; - } - return; -} - -BEGIN_MESSAGE_MAP(CSDKSampleDlg, CDialog) - ON_WM_SYSCOMMAND() - ON_WM_VSCROLL() - ON_BN_CLICKED(IDC_BTN_CONNECT, &CSDKSampleDlg::OnBtnConnect) - ON_BN_CLICKED(IDC_BTN_DISCONNECT, &CSDKSampleDlg::OnBtnClose) - ON_BN_CLICKED(IDC_BTN_SPIKES, &CSDKSampleDlg::OnBtnSpikes) - ON_CBN_SELCHANGE(IDC_CHIDCOMBO, &CSDKSampleDlg::OnSelchangeComboChannel) -END_MESSAGE_MAP() - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Handle vertical slider control message -void CSDKSampleDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) -{ - switch (pScrollBar->GetDlgCtrlID()) - { - case IDC_THRESHOLD_SLIDER: - if (nSBCode == SB_ENDSCROLL) - SetThreshold(-m_sldThresh.GetPos()); - break; - } - - CDialog::OnVScroll(nSBCode, nPos, pScrollBar); -} - -// Author & Date: Ehsan Azar 30 March 2011 -// Purpose: Current channel changed -void CSDKSampleDlg::OnSelchangeComboChannel() -{ - UINT16 newchan = m_cboChannel.GetCurSel() + 1; - if (newchan != m_channel) - SetChannel(newchan); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Open and connect to the library -void CSDKSampleDlg::OnBtnConnect() -{ - cbSdkResult res = cbSdkOpen(0); - if (res != CBSDKRESULT_SUCCESS) - { - PrintError(res); - return; - } - cbSdkConnectionType conType; - cbSdkInstrumentType instType; - // Return the actual openned connection - res = cbSdkGetType(0, &conType, &instType); - if (res != CBSDKRESULT_SUCCESS) - { - SetStatusWindow("Unable to determine connection type"); - return; - } - cbSdkVersion ver; - res = cbSdkGetVersion(0, &ver); - if (res != CBSDKRESULT_SUCCESS) - { - SetStatusWindow("Unable to determine nsp version"); - return; - } - - if (conType < 0 || conType > CBSDKCONNECTION_CLOSED) - conType = CBSDKCONNECTION_CLOSED; - if (instType < 0 || instType > CBSDKINSTRUMENT_COUNT) - instType = CBSDKINSTRUMENT_COUNT; - - char strConnection[CBSDKCONNECTION_CLOSED + 1][8] = {"Default", "Central", "Udp", "Closed"}; - char strInstrument[CBSDKINSTRUMENT_COUNT + 1][13] = {"NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"}; - CString strStatus; - SetStatusWindow(strStatus); - strStatus.Format("%s real-time interface to %s (%d.%02d.%02d.%02d) successfully initialized\n", strConnection[conType], strInstrument[instType], ver.nspmajor, ver.nspminor, ver.nsprelease, ver.nspbeta); - SetStatusWindow(strStatus); - - // Slider shows analog threshold - m_sldThresh.SetRange(-255, 255, TRUE); - m_sldThresh.SetPageSize(5); - - m_cboChannel.ResetContent(); - char label[cbLEN_STR_LABEL + 1]; - for(UINT16 chan = 1; chan <= cbNUM_ANALOG_CHANS; chan++) - { - label[cbLEN_STR_LABEL] = 0; - res = cbSdkGetChannelLabel(0, chan, NULL, label, NULL, NULL); - if (res == CBSDKRESULT_SUCCESS) - m_cboChannel.AddString(label); - } - if (m_cboChannel.GetCount() > 0) - m_cboChannel.SetCurSel(m_channel - 1); - SetChannel(m_channel); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Close the library -void CSDKSampleDlg::OnBtnClose() -{ - cbSdkResult res = cbSdkClose(0); - if (res != CBSDKRESULT_SUCCESS) - { - PrintError(res); - return; - } - SetStatusWindow("Interface closed successfully"); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Listen to the spike packets -void CSDKSampleDlg::OnBtnSpikes() -{ - cbSdkResult res = cbSdkRegisterCallback(0, CBSDKCALLBACK_SPIKE, SpikesCallback, this); - if (res != CBSDKRESULT_SUCCESS) - { - PrintError(res); - return; - } - SetStatusWindow("Successfully listening to the spikes"); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Initialize the dialog -BOOL CSDKSampleDlg::OnInitDialog() -{ - CDialog::OnInitDialog(); - - // Add "About..." menu item to system menu. - - // IDM_ABOUTBOX must be in the system command range. - ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); - ASSERT(IDM_ABOUTBOX < 0xF000); - - CMenu* pSysMenu = GetSystemMenu(FALSE); - if (pSysMenu != NULL) - { - CString strAboutMenu; - strAboutMenu.LoadString(IDS_ABOUTBOX); - if (!strAboutMenu.IsEmpty()) - { - pSysMenu->AppendMenu(MF_SEPARATOR); - pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); - } - } - - // Set the icon for this dialog. The framework does this automatically - // when the application's main window is not a dialog - SetIcon(m_hIcon, TRUE); // Set big icon - SetIcon(m_hIcon, FALSE); // Set small icon - - // Use a better window for drawing - ::ReplaceWindowControl(*this, m_spkPict, IDC_PICT_SPIKES); - - // Initialize the channel combo box and refresh channel display - m_cboChannel.InitStorage(cbNUM_ANALOG_CHANS, cbLEN_STR_LABEL + 1); - - - return TRUE; // return TRUE unless you set the focus to a control -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: System menu command -void CSDKSampleDlg::OnSysCommand(UINT nID, LPARAM lParam) -{ - if ((nID & 0xFFF0) == IDM_ABOUTBOX) - { - CAboutDlg dlgAbout; - dlgAbout.DoModal(); - } - else - { - CDialog::OnSysCommand(nID, lParam); - } -} diff --git a/examples/SDKSample/SDKSampleDlg.h b/examples/SDKSample/SDKSampleDlg.h deleted file mode 100755 index fd69259f..00000000 --- a/examples/SDKSample/SDKSampleDlg.h +++ /dev/null @@ -1,93 +0,0 @@ -/* =STS=> SDKSampleDlg.h[4275].aa02 open SMID:2 */ -///////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2011 - 2012 Blackrock Microsystems -// -// $Workfile: SDKSampleDlg.h $ -// $Archive: /Cerebus/Human/WindowsApps/SDKSample/SDKSampleDlg.h $ -// $Revision: 1 $ -// $Date: 3/29/11 9:23a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -///////////////////////////////////////////////////////////////////////////// -// -// PURPOSE: Cerebus SDK sample dialog -// -///////////////////////////////////////////////////////////////////////////// - -#ifndef SDKSAMPLEDLG_H_INCLUDED -#define SDKSAMPLEDLG_H_INCLUDED - -#if _MSC_VER > 1000 -#pragma once - -#endif - -#include -#include "SpkDisp.h" - -// CSDKSampleDlg dialog -class CSDKSampleDlg : public CDialog -{ -// Construction -public: - CSDKSampleDlg(CWnd* pParent = NULL); // standard constructor - -// Dialog Data - enum { IDD = IDD_EXAMPLE_DIALOG }; - -protected: - virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support - -// Implementation -protected: - void PrintError(cbSdkResult res); - void SetStatusWindow(const char * statusmsg); - void SetChannel(UINT16 channel); - void AddSpike(cbPKT_SPK & spk); - void SetThreshold(int pos); - static void SpikesCallback(UINT32 nInstance, const cbSdkPktType type, const void* pEventData, void* pCallbackData); - -protected: - HICON m_hIcon; - CStatic m_status; - CSpkDisp m_spkPict; - CComboBox m_cboChannel; - CSliderCtrl m_sldThresh; - -protected: - UINT16 m_channel; - INT32 m_threshold; // digital threshold - - // event handling functions - virtual BOOL OnInitDialog(); - afx_msg void OnSysCommand(UINT nID, LPARAM lParam); - afx_msg void OnBtnConnect(); - afx_msg void OnBtnClose(); - afx_msg void OnBtnSpikes(); - afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); - afx_msg void OnSelchangeComboChannel(); - DECLARE_MESSAGE_MAP() -}; - -// CAboutDlg dialog -class CAboutDlg : public CDialog -{ -public: - CAboutDlg(); - -// Dialog Data - enum { IDD = IDD_ABOUTBOX }; - -protected: - virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support - virtual BOOL OnInitDialog(); - -// Implementation -protected: - DECLARE_MESSAGE_MAP() -}; - -#endif //SDKSAMPLEDLG_H_INCLUDED diff --git a/examples/SDKSample/SpkDisp.cpp b/examples/SDKSample/SpkDisp.cpp deleted file mode 100755 index ae636301..00000000 --- a/examples/SDKSample/SpkDisp.cpp +++ /dev/null @@ -1,185 +0,0 @@ -// =STS=> SpkDisp.cpp[4902].aa01 open SMID:1 -///////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2012-2013 Blackrock Microsystems -// -// $Workfile: SpkDisp.h $ -// $Archive: /Cerebus/WindowsApps/SDKSample/SpkDisp.h $ -// $Revision: 15 $ -// $Date: 4/26/12 9:50a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -///////////////////////////////////////////////////////////////////////////// - - -#include "stdafx.h" -#include "SpkDisp.h" -#include - -#ifdef _DEBUG -#undef THIS_FILE -static char THIS_FILE[] = __FILE__; -#endif - -///////////////////////////////////////////////////////////////////////////// -// CSpkDisp - -CSpkDisp::CSpkDisp() : - m_spklength(48) -{ - m_count = 0; - m_cacheIdxRead = m_cacheIdxWrite = 0; -} - -CSpkDisp::~CSpkDisp() -{ -} - - -BEGIN_MESSAGE_MAP(CSpkDisp, CWnd) - //{{AFX_MSG_MAP(CSpkDisp) - ON_WM_PAINT() - ON_WM_ERASEBKGND() - ON_WM_CREATE() - ON_WM_CLOSE() - //}}AFX_MSG_MAP -END_MESSAGE_MAP() - - -///////////////////////////////////////////////////////////////////////////// -// CSpkDisp message handlers - - -// Author & Date: Ehsan Azar 26 April 2012 -// Purpose: Specialized DC for drawing -BOOL CSpkDisp::PreCreateWindow(CREATESTRUCT& cs) -{ - WNDCLASS wc; - wc.style = CS_OWNDC | CS_DBLCLKS; - wc.lpfnWndProc = AfxWndProc; - wc.cbClsExtra = NULL; - wc.cbWndExtra = NULL; - wc.hInstance = AfxGetInstanceHandle(); - wc.hIcon = NULL; - wc.hCursor = AfxGetApp()->LoadCursor(IDC_ARROW); - wc.hbrBackground= NULL; - wc.lpszMenuName = NULL; - wc.lpszClassName= "cbSpikeDisplay"; - if (!AfxRegisterClass( &wc )) - return FALSE; - - cs.lpszClass = "cbSpikeDisplay"; - return CWnd::PreCreateWindow(cs); -} - -// Author & Date: Ehsan Azar 26 April 2012 -// Purpose: Specialized DC for drawing -int CSpkDisp::OnCreate(LPCREATESTRUCT lpCreateStruct) -{ - if (CWnd::OnCreate(lpCreateStruct) == -1) - return -1; - - RECT rect_client; - GetClientRect(&rect_client); - m_iClientSizeX = rect_client.right; - m_iClientSizeY = rect_client.bottom; - // Get picture area - CRect rectFrame; - GetClientRect(&rectFrame); - m_width = rectFrame.Width(); - m_height = rectFrame.Height(); - - m_dispunit[0] = RGB(192,192,192); - m_dispunit[1] = RGB(255, 51,153); - m_dispunit[2] = RGB( 0,255,255); - m_dispunit[3] = RGB(255,255, 0); - m_dispunit[4] = RGB(153, 0,204); - m_dispunit[5] = RGB( 0,255, 0); - - return 0; -} - -BOOL CSpkDisp::OnEraseBkgnd(CDC* pDC) -{ - // clear background - pDC->FillSolidRect(0, 0, m_iClientSizeX, m_iClientSizeY, RGB(0, 0, 0)); - - return TRUE; -} - -// Author & Date: Ehsan Azar 26 April 2012 -// Purpose: Add spike to the the cache to be drawn later -// Inputs: -// spk - spike packet -void CSpkDisp::AddSpike(cbPKT_SPK & spk) -{ - int idx = m_cacheIdxWrite + 1; - if (idx == 500) - idx = 0; - - if (idx == m_cacheIdxRead) - return; // We are behind on drawing! - - m_spkCache[m_cacheIdxWrite] = spk; - if (m_cacheIdxWrite < 499) - m_cacheIdxWrite++; - else - m_cacheIdxWrite = 0; - // Begin drawing - Invalidate(false); -} - -// Author & Date: Ehsan Azar 29 March 2011 -// Purpose: Draw spike to the screen -// Inputs: -// spk - spike packet -void CSpkDisp::DrawSpikes(CDC & rcDC) -{ - // Draw at most these many spikes from cache in one shot - for (int nSpikes = 0; nSpikes < 100; ++nSpikes) - { - if (m_cacheIdxRead == m_cacheIdxWrite) - return; - - cbPKT_SPK & spk = m_spkCache[m_cacheIdxRead]; - - UINT8 unit = spk.unit; - if (unit > cbMAXUNITS) - unit = 0; - if (m_count == 0) - rcDC.FillSolidRect(0, 0, m_width, m_height, RGB(0, 0, 0)); - if (m_count++ > 100) - m_count = 0; - rcDC.SelectObject(GetStockObject(DC_PEN)); - rcDC.SetDCPenColor(m_dispunit[unit]); - int halfheight = m_height / 2; - rcDC.MoveTo(0, halfheight); - for (UINT32 i = 0; i < m_spklength; ++i) - { - int x = (int)(i * (float(m_width) / m_spklength)); - int y = halfheight - (int)(spk.wave[i] / (4.0 * 255) * halfheight); - if (y > m_height) - y = m_height; - else if (y < 0) - y = 0; - rcDC.LineTo(x, y); - } - if (m_cacheIdxRead < 499) - m_cacheIdxRead++; - else - m_cacheIdxRead = 0; - } -} - -// Author & Date: Ehsan Azar 26 April 2012 -// Purpose: PAint spikes from the cache -void CSpkDisp::OnPaint() -{ - CPaintDC dc(this); - - // Draw spike on screen - DrawSpikes(dc); -} - diff --git a/examples/SDKSample/SpkDisp.h b/examples/SDKSample/SpkDisp.h deleted file mode 100755 index a2dbbdaa..00000000 --- a/examples/SDKSample/SpkDisp.h +++ /dev/null @@ -1,60 +0,0 @@ -///////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2012-2013 Blackrock Microsystems -// -// $Workfile: SpkDisp.h $ -// $Archive: /Cerebus/WindowsApps/SDKSample/SpkDisp.h $ -// $Revision: 15 $ -// $Date: 4/26/12 9:50a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -///////////////////////////////////////////////////////////////////////////// - - -#ifndef SPKDISP_H_INCLUDED -#define SPKDISP_H_INCLUDED - -#include "cbhwlib.h" - -///////////////////////////////////////////////////////////////////////////// -// CSpkDisp window - -class CSpkDisp : public CWnd -{ -// Construction -public: - CSpkDisp(); - virtual ~CSpkDisp(); - -public: - void AddSpike(cbPKT_SPK & spk); - -protected: - virtual BOOL PreCreateWindow(CREATESTRUCT& cs); - -protected: - void DrawSpikes(CDC & rcDC); - -// Attributes -protected: - COLORREF m_dispunit[cbMAXUNITS + 1]; - int m_width, m_height; - int m_iClientSizeX; - int m_iClientSizeY; - int m_count; - cbPKT_SPK m_spkCache[500]; - int m_cacheIdxWrite; - int m_cacheIdxRead; -public: - UINT32 m_spklength; - -protected: - afx_msg void OnPaint(); - afx_msg BOOL OnEraseBkgnd(CDC* pDC); - afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); - DECLARE_MESSAGE_MAP() -}; - -#endif // include guard diff --git a/examples/SDKSample/project.vsprops b/examples/SDKSample/project.vsprops deleted file mode 100755 index 453d48f7..00000000 --- a/examples/SDKSample/project.vsprops +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/examples/SDKSample/res/SDKSample.ico b/examples/SDKSample/res/SDKSample.ico deleted file mode 100755 index d941d23d..00000000 Binary files a/examples/SDKSample/res/SDKSample.ico and /dev/null differ diff --git a/examples/SDKSample/res/SDKSample.rc2 b/examples/SDKSample/res/SDKSample.rc2 deleted file mode 100755 index 0fe2692e..00000000 --- a/examples/SDKSample/res/SDKSample.rc2 +++ /dev/null @@ -1,13 +0,0 @@ -// -// SDKSample.RC2 - resources Microsoft Visual C++ does not edit directly -// - -#ifdef APSTUDIO_INVOKED -#error this file is not editable by Microsoft Visual C++ -#endif //APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// Add manually edited resources here... - -///////////////////////////////////////////////////////////////////////////// diff --git a/examples/SDKSample/resource.h b/examples/SDKSample/resource.h deleted file mode 100755 index 54cce3b9..00000000 --- a/examples/SDKSample/resource.h +++ /dev/null @@ -1,35 +0,0 @@ -/* =STS=> resource.h[4269].aa01 open SMID:1 */ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by SDKSample.rc -// -#define IDM_ABOUTBOX 0x0010 -#define IDS_ABOUTBOX 101 -#define IDD_ABOUTBOX 101 -#define IDD_EXAMPLE_DIALOG 102 -#define IDR_MAINFRAME 128 -#define IDC_STATIC_NSP_ID 994 -#define IDS_COPYRIGHT_FROM 995 -#define IDS_APPNAME 996 -#define IDC_STATIC_APP_VERSION 997 -#define IDC_STATIC_LIB_VERSION 998 -#define IDC_STATIC_NSP_APP_VERSION 999 -#define IDC_STATIC_STATUS 1000 -#define IDC_STATIC_NSP_LIB_VERSION 1001 -#define IDC_BTN_CONNECT 1003 -#define IDC_BTN_DISCONNECT 1004 -#define IDC_BTN_SPIKES 1005 -#define IDC_PICT_SPIKES 1006 -#define IDC_THRESHOLD_SLIDER 1007 -#define IDC_CHIDCOMBO 1023 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 130 -#define _APS_NEXT_COMMAND_VALUE 32771 -#define _APS_NEXT_CONTROL_VALUE 1007 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/examples/SDKSample/src/SDKSample.sln b/examples/SDKSample/src/SDKSample.sln deleted file mode 100755 index 5301c9a8..00000000 --- a/examples/SDKSample/src/SDKSample.sln +++ /dev/null @@ -1,26 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 9.00 -# Visual Studio 2005 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SDKSample", "SDKSample.vcproj", "{0478F3CF-6ADD-45B1-95F3-03FA3492924A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Win32 = Debug|Win32 - Debug|x64 = Debug|x64 - Release|Win32 = Release|Win32 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0478F3CF-6ADD-45B1-95F3-03FA3492924A}.Debug|Win32.ActiveCfg = Debug|Win32 - {0478F3CF-6ADD-45B1-95F3-03FA3492924A}.Debug|Win32.Build.0 = Debug|Win32 - {0478F3CF-6ADD-45B1-95F3-03FA3492924A}.Debug|x64.ActiveCfg = Debug|x64 - {0478F3CF-6ADD-45B1-95F3-03FA3492924A}.Debug|x64.Build.0 = Debug|x64 - {0478F3CF-6ADD-45B1-95F3-03FA3492924A}.Release|Win32.ActiveCfg = Release|Win32 - {0478F3CF-6ADD-45B1-95F3-03FA3492924A}.Release|Win32.Build.0 = Release|Win32 - {0478F3CF-6ADD-45B1-95F3-03FA3492924A}.Release|x64.ActiveCfg = Release|x64 - {0478F3CF-6ADD-45B1-95F3-03FA3492924A}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/examples/SDKSample/stdafx.cpp b/examples/SDKSample/stdafx.cpp deleted file mode 100755 index 95900336..00000000 --- a/examples/SDKSample/stdafx.cpp +++ /dev/null @@ -1,8 +0,0 @@ -// =STS=> stdafx.cpp[4276].aa00 closed SMID:1 -// stdafx.cpp : source file that includes just the standard includes -// Example.pch will be the pre-compiled header -// stdafx.obj will contain the pre-compiled type information - -#include "stdafx.h" - - diff --git a/examples/SDKSample/stdafx.h b/examples/SDKSample/stdafx.h deleted file mode 100755 index 4248b4a3..00000000 --- a/examples/SDKSample/stdafx.h +++ /dev/null @@ -1,79 +0,0 @@ -/* =STS=> stdafx.h[4277].aa01 open SMID:1 */ -// stdafx.h : include file for standard system include files, -// or project specific include files that are used frequently, -// but are changed infrequently - -#pragma once - -#ifdef WIN32 -#define _CRT_SECURE_NO_DEPRECATE -#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1 -#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT 1 -#endif - -#ifndef _SECURE_ATL -#define _SECURE_ATL 1 -#endif - -#ifndef VC_EXTRALEAN -#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers -#endif - -// Modify the following defines if you have to target a platform prior to the ones specified below. -// Refer to MSDN for the latest info on corresponding values for different platforms. -#ifndef WINVER // Allow use of features specific to Windows XP or later. -#define WINVER 0x0501 // Change this to the appropriate value to target other versions of Windows. -#endif - -#ifndef _WIN32_WINNT // Allow use of features specific to Windows XP or later. -#define _WIN32_WINNT 0x0501 // Change this to the appropriate value to target other versions of Windows. -#endif - -#ifndef _WIN32_WINDOWS // Allow use of features specific to Windows 98 or later. -#define _WIN32_WINDOWS 0x0410 // Change this to the appropriate value to target Windows Me or later. -#endif - -#ifndef _WIN32_IE // Allow use of features specific to IE 6.0 or later. -#define _WIN32_IE 0x0600 // Change this to the appropriate value to target other versions of IE. -#endif - -#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // some CString constructors will be explicit - -// turns off MFC's hiding of some common and often safely ignored warning messages -#define _AFX_ALL_WARNINGS - -#include // MFC core and standard components -#include // MFC extensions - - - - - -#ifndef _AFX_NO_OLE_SUPPORT -#include // MFC support for Internet Explorer 4 Common Controls -#endif -#ifndef _AFX_NO_AFXCMN_SUPPORT -#include // MFC support for Windows Common Controls -#endif // _AFX_NO_AFXCMN_SUPPORT - - - - - - - - - -#ifdef _UNICODE -#if defined _M_IX86 -#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") -#elif defined _M_IA64 -#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\"") -#elif defined _M_X64 -#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") -#else -#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") -#endif -#endif - - diff --git a/examples/SimpleAnalogOut/simple_analog_out.cpp b/examples/SimpleAnalogOut/simple_analog_out.cpp deleted file mode 100644 index 83881a94..00000000 --- a/examples/SimpleAnalogOut/simple_analog_out.cpp +++ /dev/null @@ -1,278 +0,0 @@ -/////////////////////////////////////////////////////////////////////// -// -// Test SDK -// -// $Workfile: testsdk.cpp $ -// $Archive: /Cerebus/Human/LinuxApps/cbmex/testsdk.cpp $ -// $Revision: 1 $ -// $Date: 10/22/12 12:00a $ -// $Author: Ehsan $ -// -// Purpose: -// This is the test suite to run test stubs -// -// Note: -// Make sure only the SDK is used here, and not cbhwlib directly -// this will ensure SDK is capable of whatever test suite can do. -// Do not throw exceptions, catch possible exceptions and handle them the earliest possible in this library -// - -#include -#include - -#include - -#define INST 0 - - -void handleResult(const cbSdkResult res) -{ - switch (res) - { - case CBSDKRESULT_SUCCESS: - break; - case CBSDKRESULT_NOTIMPLEMENTED: - printf("Not implemented\n"); - break; - case CBSDKRESULT_INVALIDPARAM: - printf("Invalid parameter\n"); - break; - case CBSDKRESULT_WARNOPEN: - printf("Real-time interface already initialized\n"); - case CBSDKRESULT_WARNCLOSED: - printf("Real-time interface already closed\n"); - break; - case CBSDKRESULT_ERROPENCENTRAL: - printf("Unable to open library for Central interface\n"); - break; - case CBSDKRESULT_ERROPENUDP: - printf("Unable to open library for UDP interface\n"); - break; - case CBSDKRESULT_ERROPENUDPPORT: - printf("Unable to open UDP interface\n"); - break; - case CBSDKRESULT_OPTERRUDP: - printf("Unable to set UDP interface option\n"); - break; - case CBSDKRESULT_MEMERRUDP: - printf("Unable to assign UDP interface memory\n" - " Consider using sysctl -w net.core.rmem_max=8388608\n" - " or sysctl -w kern.ipc.maxsockbuf=8388608\n"); - break; - case CBSDKRESULT_INVALIDINST: - printf("Invalid UDP interface\n"); - break; - case CBSDKRESULT_ERRMEMORYTRIAL: - printf("Unable to allocate RAM for trial cache data\n"); - break; - case CBSDKRESULT_ERROPENUDPTHREAD: - printf("Unable to Create UDP thread\n"); - break; - case CBSDKRESULT_ERROPENCENTRALTHREAD: - printf("Unable to start Cerebus real-time data thread\n"); - break; - case CBSDKRESULT_ERRINIT: - printf("Library initialization error\n"); - break; - case CBSDKRESULT_ERRMEMORY: - printf("Library memory allocation error\n"); - break; - case CBSDKRESULT_TIMEOUT: - printf("Connection timeout error\n"); - break; - case CBSDKRESULT_ERROFFLINE: - printf("Instrument is offline\n"); - break; - default: - printf("Unexpected error\n"); - break; - } -} - - -cbSdkVersion getVersion() -{ - // Library version can be read even before library open (return value is a warning) - // actual NSP version however needs library to be open - cbSdkVersion ver; - const cbSdkResult res = cbSdkGetVersion(INST, &ver); - if (res != CBSDKRESULT_SUCCESS) - { - printf("Unable to determine instrument version\n"); - } - else { - printf("Initializing Cerebus real-time interface %d.%02d.%02d.%02d (protocol cb%d.%02d)...\n", ver.major, ver.minor, ver.release, ver.beta, ver.majorp, ver.minorp); - } - handleResult(res); - return ver; -} - -// Author & Date: Ehsan Azar 24 Oct 2012 -// Purpose: Test opening the library -cbSdkResult open() -{ - // Try to get the version. Should be a warning because we are not yet open. - getVersion(); - - // Open the device using default connection type. - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - cbSdkResult res = cbSdkOpen(INST, conType); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to open instrument connection.\n"); - handleResult(res); - - cbSdkInstrumentType instType; - if (res >= 0) - { - // Return the actual opened connection - res = cbSdkGetType(INST, &conType, &instType); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to determine connection type\n"); - handleResult(res); - // if (instType == CBSDKINSTRUMENT_NPLAY || instType == CBSDKINSTRUMENT_REMOTENPLAY) - // printf("Unable to open UDP interface to nPlay\n"); - - if (conType > CBSDKCONNECTION_CLOSED) - conType = CBSDKCONNECTION_COUNT; - if (instType > CBSDKINSTRUMENT_COUNT) - instType = CBSDKINSTRUMENT_COUNT; - - char strConnection[CBSDKCONNECTION_COUNT + 1][8] = {"Default", "Central", "Udp", "Closed", "Unknown"}; - char strInstrument[CBSDKINSTRUMENT_COUNT + 1][13] = {"NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"}; - - // Now that we are open, get the version again. - const cbSdkVersion ver = getVersion(); - - // Summary results. - printf("%s real-time interface to %s (%d.%02d.%02d.%02d) successfully initialized\n", strConnection[conType], strInstrument[instType], ver.nspmajor, ver.nspminor, ver.nsprelease, ver.nspbeta); - } - - return res; -} - - -void getConfig() -{ - uint32_t proc = 1; - uint32_t nChansInGroup; - uint16_t pGroupList[cbNUM_ANALOG_CHANS]; - for (uint32_t group_ix = 1; group_ix < 7; group_ix++) - { - const cbSdkResult res = cbSdkGetSampleGroupList(INST, proc, group_ix, &nChansInGroup, pGroupList); - if (res == CBSDKRESULT_SUCCESS) - { - printf("In sampling group %d, found %d channels.\n", group_ix, nChansInGroup); - } - handleResult(res); - } -} - -void setConfig() -{ - uint32_t bActive = false; - uint16_t Begchan = 0; - uint32_t Begmask = 0; - uint32_t Begval = 0; - uint16_t Endchan = 0; - uint32_t Endmask = 0; - uint32_t Endval = 0; - uint32_t uWaveforms = 0; - uint32_t uConts = 0; - uint32_t uEvents = 0; - uint32_t uComments = 0; - uint32_t uTrackings = 0; - cbSdkResult res = cbSdkGetTrialConfig( - INST, - &bActive, - &Begchan, &Begmask, &Begval, - &Endchan, &Endmask, &Endval, - &uWaveforms, - &uConts, - &uEvents, - &uComments, - &uTrackings - ); - handleResult(res); - - res = cbSdkSetTrialConfig( - INST, - 1, - 0, 0, 0, - 0, 0, 0, - 0, - 0, - 0, - 100, - 0 - ); - handleResult(res); -} - -void getTime() -{ - PROCTIME cbtime = 0; - const cbSdkResult res = cbSdkGetTime(INST, &cbtime); - if (res == CBSDKRESULT_SUCCESS) - { - printf("cbSdkGetTime returned %llu\n", static_cast(cbtime)); - } - handleResult(res); -} - -void setAnaout(const uint16_t channel) -{ - const cbSdkWaveformData * wf = nullptr; - const cbSdkAoutMon mon = { channel, false, false }; - const cbSdkResult res = cbSdkSetAnalogOutput(INST, 277, wf, &mon); - if (res != CBSDKRESULT_SUCCESS) - { - printf("Unable to cbSdkSetAnalogOutput audio1 to monitor channel %d.\n", channel); - } - handleResult(res); - -} - -// Author & Date: Ehsan Azar 25 Oct 2012 -// Purpose: Test closing the library -cbSdkResult close() -{ - const cbSdkResult res = cbSdkClose(INST); - if (res == CBSDKRESULT_SUCCESS) - { - printf("Interface closed successfully\n"); - } - else - { - printf("Unable to close interface.\n"); - handleResult(res); - } - return res; -} - -///////////////////////////////////////////////////////////////////////////// -// The test suite main entry -int main(int argc, char *argv[]) -{ - cbSdkResult res = open(); - if (res < 0) - printf("open failed (%d)!\n", res); - else - printf("open succeeded\n"); - - getConfig(); - - setConfig(); - - for (uint16_t chan_ix = 1; chan_ix < static_cast(1 + 128 + 16); chan_ix++) - { - setAnaout(chan_ix); - } - - res = close(); - if (res < 0) - printf("close failed (%d)!\n", res); - else - printf("close succeeded\n"); - - return 0; -} diff --git a/examples/SimpleCBSDK/simple_cbsdk.cpp b/examples/SimpleCBSDK/simple_cbsdk.cpp deleted file mode 100644 index 3eb4f714..00000000 --- a/examples/SimpleCBSDK/simple_cbsdk.cpp +++ /dev/null @@ -1,243 +0,0 @@ -/////////////////////////////////////////////////////////////////////// -// -// Test SDK -// -// $Workfile: testsdk.cpp $ -// $Archive: /Cerebus/Human/LinuxApps/cbmex/testsdk.cpp $ -// $Revision: 1 $ -// $Date: 10/22/12 12:00a $ -// $Author: Ehsan $ -// -// Purpose: -// This is the test suite to run test stubs -// -// Note: -// Make sure only the SDK is used here, and not cbhwlib directly -// this will ensure SDK is capable of whatever test suite can do -// Do not throw exceptions, catch possible exceptions and handle them the earliest possible in this library -// - -#include -#include -#include -#include - -#include - -#define INST 0 - - -void handleResult(cbSdkResult res) -{ - switch (res) - { - case CBSDKRESULT_SUCCESS: - break; - case CBSDKRESULT_CLOSED: - printf("Instrument closed\n"); - case CBSDKRESULT_NOTIMPLEMENTED: - printf("Not implemented\n"); - break; - case CBSDKRESULT_INVALIDPARAM: - printf("Invalid parameter\n"); - break; - case CBSDKRESULT_WARNOPEN: - printf("Real-time interface already initialized\n"); - case CBSDKRESULT_WARNCLOSED: - printf("Real-time interface already closed\n"); - break; - case CBSDKRESULT_ERROPENCENTRAL: - printf("Unable to open library for Central interface\n"); - break; - case CBSDKRESULT_ERROPENUDP: - printf("Unable to open library for UDP interface\n"); - break; - case CBSDKRESULT_ERROPENUDPPORT: - printf("Unable to open UDP interface\n"); - break; - case CBSDKRESULT_OPTERRUDP: - printf("Unable to set UDP interface option\n"); - break; - case CBSDKRESULT_MEMERRUDP: - printf("Unable to assign UDP interface memory\n" - " Consider using sysctl -w net.core.rmem_max=8388608\n" - " or sysctl -w kern.ipc.maxsockbuf=8388608\n"); - break; - case CBSDKRESULT_INVALIDINST: - printf("Invalid UDP interface\n"); - break; - case CBSDKRESULT_ERRMEMORYTRIAL: - printf("Unable to allocate RAM for trial cache data\n"); - break; - case CBSDKRESULT_ERROPENUDPTHREAD: - printf("Unable to Create UDP thread\n"); - break; - case CBSDKRESULT_ERROPENCENTRALTHREAD: - printf("Unable to start Cerebus real-time data thread\n"); - break; - case CBSDKRESULT_ERRINIT: - printf("Library initialization error\n"); - break; - case CBSDKRESULT_ERRMEMORY: - printf("Library memory allocation error\n"); - break; - case CBSDKRESULT_TIMEOUT: - printf("Connection timeout error\n"); - break; - case CBSDKRESULT_ERROFFLINE: - printf("Instrument is offline\n"); - break; - default: - printf("Unexpected error (%d)\n", res); - break; - } -} - - -cbSdkVersion getVersion() -{ - // Library version can be read even before library open (return value is a warning) - // actual NSP version however needs library to be open - cbSdkVersion ver; - const cbSdkResult res = cbSdkGetVersion(INST, &ver); - if (res != CBSDKRESULT_SUCCESS) - { - printf("Unable to determine instrument version\n"); - } - else { - printf("Initializing Cerebus real-time interface %d.%02d.%02d.%02d (protocol cb%d.%02d)...\n", - ver.major, ver.minor, ver.release, ver.beta, ver.majorp, ver.minorp); - } - handleResult(res); - return ver; -} - -// Author & Date: Ehsan Azar 24 Oct 2012 -// Purpose: Test opening the library -cbSdkResult open(const LPCSTR inst_ip, const int inst_port, const LPCSTR client_ip) -{ - // Try to get the version. Should be a warning because we are not yet open. - getVersion(); - - // Open the device using default connection type. - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - auto con = cbSdkConnection(); - con.szOutIP = inst_ip; - con.nOutPort = inst_port; - con.szInIP = client_ip; - con.nInPort = inst_port; - cbSdkResult res = cbSdkOpen(INST, conType, con); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to open instrument connection.\n"); - handleResult(res); - - cbSdkInstrumentType instType; - if (res >= 0) - { - // Return the actual opened connection - res = cbSdkGetType(INST, &conType, &instType); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to determine connection type\n"); - handleResult(res); - // if (instType == CBSDKINSTRUMENT_NPLAY || instType == CBSDKINSTRUMENT_REMOTENPLAY) - // printf("Unable to open UDP interface to nPlay\n"); - - - if (conType > CBSDKCONNECTION_CLOSED) - conType = CBSDKCONNECTION_COUNT; - if (instType > CBSDKINSTRUMENT_COUNT) - instType = CBSDKINSTRUMENT_COUNT; - - char strConnection[CBSDKCONNECTION_COUNT + 1][8] = {"Default", "Central", "Udp", "Closed", "Unknown"}; - char strInstrument[CBSDKINSTRUMENT_COUNT + 1][13] = {"NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"}; - - // Now that we are open, get the version again. - const cbSdkVersion ver = getVersion(); - - // Summary results. - printf("%s real-time interface to %s (%d.%02d.%02d.%02d; proto %d.%02d) successfully initialized\n", - strConnection[conType], strInstrument[instType], - ver.nspmajor, ver.nspminor, ver.nsprelease, ver.nspbeta, - ver.nspmajorp, ver.nspminorp); - } - - return res; -} - - -cbSdkResult getConfig() -{ - uint32_t proc = 1; - uint32_t nChansInGroup; - uint16_t pGroupList[cbNUM_ANALOG_CHANS]; - cbSdkResult res = CBSDKRESULT_SUCCESS; - for (uint32_t group_ix = 1; group_ix < 7; group_ix++) - { - res = cbSdkGetSampleGroupList(INST, proc, group_ix, &nChansInGroup, pGroupList); - if (res == CBSDKRESULT_SUCCESS) - { - printf("In sampling group %d, found %d channels.\n", group_ix, nChansInGroup); - } - handleResult(res); - } - return res; -} - -// Author & Date: Ehsan Azar 25 Oct 2012 -// Purpose: Test closing the library -cbSdkResult close() -{ - const cbSdkResult res = cbSdkClose(INST); - if (res == CBSDKRESULT_SUCCESS) - { - printf("Interface closed successfully\n"); - } - else - { - printf("Unable to close interface.\n"); - handleResult(res); - } - return res; -} - -///////////////////////////////////////////////////////////////////////////// -// The test suit main entry -int main(const int argc, char *argv[]) -{ - auto inst_ip = ""; - int inst_port = cbNET_UDP_PORT_CNT; - auto client_ip = ""; - if (argc > 1) {inst_ip = argv[1];} - if (argc > 2) { - try { - inst_port = std::stoi(argv[2]); - } catch (const std::exception&) { - printf("Error: Invalid port number '%s'\n", argv[2]); - return 1; - } - } - if (argc > 3) { client_ip = argv[3]; } - cbSdkResult res = open(inst_ip, inst_port, client_ip); - if (res < 0) - { - printf("open failed (%d)!\n", res); - return 0; - } - printf("open succeeded\n"); - - res = getConfig(); - if (res < 0) - { - printf("getConfig failed (%d)!\n", res); - return 0; - } - printf("getConfig succeeded\n"); - - res = close(); - if (res < 0) - printf("close failed (%d)!\n", res); - else - printf("close succeeded\n"); - - return 0; -} diff --git a/examples/SimpleCCF/simple_ccf.cpp b/examples/SimpleCCF/simple_ccf.cpp deleted file mode 100644 index e24a683d..00000000 --- a/examples/SimpleCCF/simple_ccf.cpp +++ /dev/null @@ -1,239 +0,0 @@ -/////////////////////////////////////////////////////////////////////// -// -// Test IO -// -// Purpose: -// This extends testcbsdk with explicit testing of data io. -// This is incomplete and only includes tests that were relevant to debugging specific problems. -// -// Call with -c to enable continuous data (not tested), -e to enable events (works ok), -// and -r to specify a fixed runtime (?not working). -// while running, press Esc key to stop. -// -// Note: -// Make sure only the SDK is used here, and not cbhwlib directly -// this will ensure SDK is capable of whatever test suite can do -// Do not throw exceptions, catch possible exceptions and handle them the earliest possible in this library -// - -#include -#include -#include -#include -#include - -#define INST 0 - -typedef struct cbsdk_config { - uint32_t nInstance; - uint32_t bActive; - uint16_t begchan; - uint32_t begmask; - uint32_t begval; - uint16_t endchan; - uint32_t endmask; - uint32_t endval; - bool bDouble; - uint32_t uWaveforms; - uint32_t uConts; - uint32_t uEvents; - uint32_t uComments; - uint32_t uTrackings; - bool bAbsolute; -} cbsdk_config; - -void handleResult(const cbSdkResult res) -{ - switch (res) - { - case CBSDKRESULT_SUCCESS: - break; - case CBSDKRESULT_NOTIMPLEMENTED: - printf("Not implemented\n"); - break; - case CBSDKRESULT_INVALIDPARAM: - printf("Invalid parameter\n"); - break; - case CBSDKRESULT_WARNOPEN: - printf("Real-time interface already initialized\n"); - case CBSDKRESULT_WARNCLOSED: - printf("Real-time interface already closed\n"); - break; - case CBSDKRESULT_ERROPENCENTRAL: - printf("Unable to open library for Central interface\n"); - break; - case CBSDKRESULT_ERROPENUDP: - printf("Unable to open library for UDP interface\n"); - break; - case CBSDKRESULT_ERROPENUDPPORT: - printf("Unable to open UDP interface\n"); - break; - case CBSDKRESULT_OPTERRUDP: - printf("Unable to set UDP interface option\n"); - break; - case CBSDKRESULT_MEMERRUDP: - printf("Unable to assign UDP interface memory\n" - " Consider using sysctl -w net.core.rmem_max=8388608\n" - " or sysctl -w kern.ipc.maxsockbuf=8388608\n"); - break; - case CBSDKRESULT_INVALIDINST: - printf("Invalid UDP interface\n"); - break; - case CBSDKRESULT_ERRMEMORYTRIAL: - printf("Unable to allocate RAM for trial cache data\n"); - break; - case CBSDKRESULT_ERROPENUDPTHREAD: - printf("Unable to Create UDP thread\n"); - break; - case CBSDKRESULT_ERROPENCENTRALTHREAD: - printf("Unable to start Cerebus real-time data thread\n"); - break; - case CBSDKRESULT_ERRINIT: - printf("Library initialization error\n"); - break; - case CBSDKRESULT_ERRMEMORY: - printf("Library memory allocation error\n"); - break; - case CBSDKRESULT_TIMEOUT: - printf("Connection timeout error\n"); - break; - case CBSDKRESULT_ERROFFLINE: - printf("Instrument is offline\n"); - break; - default: - printf("Unexpected error\n"); - break; - } -} - - -cbSdkVersion getVersion() -{ - // Library version can be read even before library open (return value is a warning) - // actual NSP version however needs library to be open - cbSdkVersion ver; - const cbSdkResult res = cbSdkGetVersion(INST, &ver); - if (res != CBSDKRESULT_SUCCESS) - { - printf("Unable to determine instrument version\n"); - } - else { - printf("Initializing Cerebus real-time interface %d.%02d.%02d.%02d (protocol cb%d.%02d)...\n", ver.major, ver.minor, ver.release, ver.beta, ver.majorp, ver.minorp); - } - handleResult(res); - return ver; -} - -// Author & Date: Ehsan Azar 24 Oct 2012 -// Purpose: Test opening the library -cbSdkResult open(const LPCSTR inst_ip, const int inst_port, const LPCSTR client_ip) -{ - // Try to get the version. Should be a warning because we are not yet open. - getVersion(); - - // Open the device using default connection type. - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - auto con = cbSdkConnection(); - con.szOutIP = inst_ip; - con.nOutPort = inst_port; - con.szInIP = client_ip; - con.nInPort = inst_port; - cbSdkResult res = cbSdkOpen(INST, conType, con); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to open instrument connection.\n"); - handleResult(res); - - cbSdkInstrumentType instType; - if (res >= 0) - { - // Return the actual opened connection - res = cbSdkGetType(INST, &conType, &instType); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to determine connection type\n"); - handleResult(res); - // if (instType == CBSDKINSTRUMENT_NPLAY || instType == CBSDKINSTRUMENT_REMOTENPLAY) - // printf("Unable to open UDP interface to nPlay\n"); - - - if (conType > CBSDKCONNECTION_CLOSED) - conType = CBSDKCONNECTION_COUNT; - if (instType > CBSDKINSTRUMENT_COUNT) - instType = CBSDKINSTRUMENT_COUNT; - - char strConnection[CBSDKCONNECTION_COUNT + 1][8] = {"Default", "Central", "Udp", "Closed", "Unknown"}; - char strInstrument[CBSDKINSTRUMENT_COUNT + 1][13] = {"NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"}; - - // Now that we are open, get the version again. - const cbSdkVersion ver = getVersion(); - - // Summary results. - printf("%s real-time interface to %s (%d.%02d.%02d.%02d) successfully initialized\n", strConnection[conType], strInstrument[instType], ver.nspmajor, ver.nspminor, ver.nsprelease, ver.nspbeta); - } - - return res; -} - -// Author & Date: Ehsan Azar 25 Oct 2012 -// Purpose: Test closing the library -cbSdkResult close() -{ - const cbSdkResult res = cbSdkClose(INST); - if (res == CBSDKRESULT_SUCCESS) - { - printf("Interface closed successfully\n"); - } - else - { - printf("Unable to close interface.\n"); - handleResult(res); - } - return res; -} - -///////////////////////////////////////////////////////////////////////////// -// The test suit main entry -int main(const int argc, char *argv[]) -{ - auto inst_ip = cbNET_UDP_ADDR_CNT; - int inst_port = cbNET_UDP_PORT_CNT; - const auto client_ip = ""; - if (argc < 2) { - std::cerr << "Usage: " << argv[0] << " " << std::endl; - return 1; - } - if (argc > 2) {inst_ip = argv[2];} - if (argc > 3) { - try { - inst_port = std::stoi(argv[3]); - } catch (const std::exception&) { - printf("Error: Invalid port number '%s'\n", argv[2]); - return 1; - } - } - cbSdkResult res = open(inst_ip, inst_port, client_ip); - handleResult(res); - - cbSdkCCF data; - res = cbSdkReadCCF( - INST, - &data, - argv[1], - true, - true, - false - ); - handleResult(res); - - if (res == CBSDKRESULT_SUCCESS) - printf("cbSdkReadCCF successfully read %s with CCF format version %d.\n", argv[1], data.ccfver); - else - printf("cbSdkReadCCF failed to read %s.\n", argv[1]); - - res = close(); - if (res < 0) - printf("close failed (%d)!\n", res); - else - printf("close succeeded\n"); - - return 0; -} diff --git a/examples/SimpleComments/simple_comments.cpp b/examples/SimpleComments/simple_comments.cpp deleted file mode 100644 index e6c55030..00000000 --- a/examples/SimpleComments/simple_comments.cpp +++ /dev/null @@ -1,288 +0,0 @@ -/////////////////////////////////////////////////////////////////////// -// -// Test SDK -// -// $Workfile: testsdk.cpp $ -// $Archive: /Cerebus/Human/LinuxApps/cbmex/testsdk.cpp $ -// $Revision: 1 $ -// $Date: 10/22/12 12:00a $ -// $Author: Ehsan $ -// -// Purpose: -// This is the test suite to run test stubs -// -// Note: -// Make sure only the SDK is used here, and not cbhwlib directly -// this will ensure SDK is capable of whatever test suite can do. -// Do not throw exceptions, catch possible exceptions and handle them the earliest possible in this library -// - -#include -#include -#include -#include - -#include - -#define INST 0 - - -void handleResult(const cbSdkResult res) -{ - switch (res) - { - case CBSDKRESULT_SUCCESS: - break; - case CBSDKRESULT_NOTIMPLEMENTED: - printf("Not implemented\n"); - break; - case CBSDKRESULT_INVALIDPARAM: - printf("Invalid parameter\n"); - break; - case CBSDKRESULT_WARNOPEN: - printf("Real-time interface already initialized\n"); - case CBSDKRESULT_WARNCLOSED: - printf("Real-time interface already closed\n"); - break; - case CBSDKRESULT_ERROPENCENTRAL: - printf("Unable to open library for Central interface\n"); - break; - case CBSDKRESULT_ERROPENUDP: - printf("Unable to open library for UDP interface\n"); - break; - case CBSDKRESULT_ERROPENUDPPORT: - printf("Unable to open UDP interface\n"); - break; - case CBSDKRESULT_OPTERRUDP: - printf("Unable to set UDP interface option\n"); - break; - case CBSDKRESULT_MEMERRUDP: - printf("Unable to assign UDP interface memory\n" - " Consider using sysctl -w net.core.rmem_max=8388608\n" - " or sysctl -w kern.ipc.maxsockbuf=8388608\n"); - break; - case CBSDKRESULT_INVALIDINST: - printf("Invalid UDP interface\n"); - break; - case CBSDKRESULT_ERRMEMORYTRIAL: - printf("Unable to allocate RAM for trial cache data\n"); - break; - case CBSDKRESULT_ERROPENUDPTHREAD: - printf("Unable to Create UDP thread\n"); - break; - case CBSDKRESULT_ERROPENCENTRALTHREAD: - printf("Unable to start Cerebus real-time data thread\n"); - break; - case CBSDKRESULT_ERRINIT: - printf("Library initialization error\n"); - break; - case CBSDKRESULT_ERRMEMORY: - printf("Library memory allocation error\n"); - break; - case CBSDKRESULT_TIMEOUT: - printf("Connection timeout error\n"); - break; - case CBSDKRESULT_ERROFFLINE: - printf("Instrument is offline\n"); - break; - default: - printf("Unexpected error\n"); - break; - } -} - - -cbSdkVersion getVersion() -{ - // Library version can be read even before library open (return value is a warning) - // actual NSP version however needs library to be open - cbSdkVersion ver; - const cbSdkResult res = cbSdkGetVersion(INST, &ver); - if (res != CBSDKRESULT_SUCCESS) - { - printf("Unable to determine instrument version\n"); - } - else { - printf("Initializing Cerebus real-time interface %d.%02d.%02d.%02d (protocol cb%d.%02d)...\n", ver.major, ver.minor, ver.release, ver.beta, ver.majorp, ver.minorp); - } - handleResult(res); - return ver; -} - -// Author & Date: Ehsan Azar 24 Oct 2012 -// Purpose: Test opening the library -cbSdkResult open() -{ - // Try to get the version. Should be a warning because we are not yet open. - cbSdkVersion ver = getVersion(); - - // Open the device using default connection type. - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - cbSdkResult res = cbSdkOpen(INST, conType); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to open instrument connection.\n"); - handleResult(res); - - cbSdkInstrumentType instType; - if (res >= 0) - { - // Return the actual opened connection - res = cbSdkGetType(INST, &conType, &instType); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to determine connection type\n"); - handleResult(res); - // if (instType == CBSDKINSTRUMENT_NPLAY || instType == CBSDKINSTRUMENT_REMOTENPLAY) - // printf("Unable to open UDP interface to nPlay\n"); - - - if (conType > CBSDKCONNECTION_CLOSED) - conType = CBSDKCONNECTION_COUNT; - if (instType > CBSDKINSTRUMENT_COUNT) - instType = CBSDKINSTRUMENT_COUNT; - - char strConnection[CBSDKCONNECTION_COUNT + 1][8] = {"Default", "Central", "Udp", "Closed", "Unknown"}; - char strInstrument[CBSDKINSTRUMENT_COUNT + 1][13] = {"NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"}; - - // Now that we are open, get the version again. - ver = getVersion(); - - // Summary results. - printf("%s real-time interface to %s (%d.%02d.%02d.%02d) successfully initialized\n", strConnection[conType], strInstrument[instType], ver.nspmajor, ver.nspminor, ver.nsprelease, ver.nspbeta); - } - - return res; -} - - -void getConfig() -{ - uint32_t proc = 1; - uint32_t nChansInGroup; - uint16_t pGroupList[cbNUM_ANALOG_CHANS]; - for (uint32_t group_ix = 1; group_ix < 7; group_ix++) - { - const cbSdkResult res = cbSdkGetSampleGroupList(INST, proc, group_ix, &nChansInGroup, pGroupList); - if (res == CBSDKRESULT_SUCCESS) - { - printf("In sampling group %d, found %d channels.\n", group_ix, nChansInGroup); - } - handleResult(res); - } -} - -void testSetConfig() -{ - // TODO: We completely ignore the current config so we can probably skip this - uint32_t bActive = false; - uint16_t Begchan = 0; - uint32_t Begmask = 0; - uint32_t Begval = 0; - uint16_t Endchan = 0; - uint32_t Endmask = 0; - uint32_t Endval = 0; - uint32_t uWaveforms = 0; - uint32_t uConts = 0; - uint32_t uEvents = 0; - uint32_t uComments = 0; - uint32_t uTrackings = 0; - cbSdkResult res = cbSdkGetTrialConfig(INST, &bActive, &Begchan, &Begmask, &Begval, &Endchan, &Endmask, &Endval, - &uWaveforms, &uConts, &uEvents, &uComments, &uTrackings); - handleResult(res); - - res = cbSdkSetTrialConfig(INST, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0); - handleResult(res); -} - -void getTime() -{ - PROCTIME cbtime = 0; - const cbSdkResult res = cbSdkGetTime(INST, &cbtime); - if (res == CBSDKRESULT_SUCCESS) - { - printf("cbSdkGetTime returned %llu\n", static_cast(cbtime)); - } - handleResult(res); -} - -void getComment() -{ - cbSdkTrialComment trialcomment = { 0, 0, nullptr, nullptr, nullptr, nullptr }; - //printf("cbSdkInitTrialData\n"); - //getTime(); - cbSdkResult res = cbSdkInitTrialData(INST, 0, nullptr, nullptr, &trialcomment, nullptr, 0); - //getTime(); - - if (trialcomment.num_samples > 0) - { - auto charsets = std::vector(trialcomment.num_samples); - auto rgbas = std::vector(trialcomment.num_samples); - auto timestamps = std::vector(trialcomment.num_samples); - trialcomment.charsets = charsets.data(); - trialcomment.rgbas = rgbas.data(); - trialcomment.timestamps = timestamps.data(); - - auto comments = std::vector(trialcomment.num_samples); - trialcomment.comments = comments.data(); - for (size_t comm_ix = 0; comm_ix < trialcomment.num_samples; comm_ix++) - { - trialcomment.comments[comm_ix] = new uint8_t[cbMAX_COMMENT]; - } - - printf("cbSdkGetTrialData - For %d comments\n", trialcomment.num_samples); - getTime(); - res = cbSdkGetTrialData(INST, 1, nullptr, nullptr, &trialcomment, nullptr); - getTime(); - - // TODO: Print comments. - for (size_t comm_ix = 0; comm_ix < trialcomment.num_samples; comm_ix++) - { - delete trialcomment.comments[comm_ix]; - trialcomment.comments[comm_ix] = nullptr; - } - } -} - -// Author & Date: Ehsan Azar 25 Oct 2012 -// Purpose: Test closing the library -cbSdkResult close() -{ - const cbSdkResult res = cbSdkClose(INST); - if (res == CBSDKRESULT_SUCCESS) - { - printf("Interface closed successfully\n"); - } - else - { - printf("Unable to close interface.\n"); - handleResult(res); - } - return res; -} - -///////////////////////////////////////////////////////////////////////////// -// The test suit main entry -int main(int argc, char *argv[]) -{ - cbSdkResult res = open(); - if (res < 0) - printf("open failed (%d)!\n", res); - else - printf("open succeeded\n"); - - getConfig(); - - testSetConfig(); - - for (size_t i = 0; i < 100; i++) - { - getComment(); - } - - res = close(); - if (res < 0) - printf("close failed (%d)!\n", res); - else - printf("close succeeded\n"); - - return 0; -} diff --git a/examples/SimpleDevice/simple_device.cpp b/examples/SimpleDevice/simple_device.cpp new file mode 100644 index 00000000..37bc0ae7 --- /dev/null +++ b/examples/SimpleDevice/simple_device.cpp @@ -0,0 +1,230 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file simple_device.cpp +/// @author CereLink Development Team +/// @date 2025-11-14 +/// +/// @brief Simple example demonstrating cbsdk_v2 SDK usage +/// +/// This example shows how to use the CereLink SDK. It demonstrates: +/// - Creating an SDK session +/// - Configuring device address and port for different device types +/// - Setting up a packet receive callback +/// - Automatic connection and handshaking +/// - Monitoring statistics +/// - Clean shutdown +/// +/// Usage: +/// simple_device [device_type] +/// +/// Device types: +/// NSP - Neural Signal Processor (legacy) +/// GEMINI_NSP - Gemini NSP +/// HUB1 - Hub 1 (legacy addressing) +/// HUB2 - Hub 2 (legacy addressing) +/// HUB3 - Hub 3 (legacy addressing) +/// NPLAY - nPlayServer +/// +/// Examples: +/// simple_device # Default: NSP +/// simple_device GEMINI_NSP # Gemini NSP +/// simple_device NPLAY # nPlay loopback +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace cbsdk; + +// Global flag for clean shutdown on Ctrl+C +std::atomic g_running{true}; + +void signalHandler(int signum) { + std::cout << "\nInterrupt signal (" << signum << ") received. Shutting down...\n"; + g_running = false; +} + +/// Print packet type name for common types +const char* getPacketTypeName(uint8_t type) { + switch (type) { + case cbPKTTYPE_SYSREP: return "SYSREP"; + case cbPKTTYPE_PROCREP: return "PROCREP"; + case cbPKTTYPE_BANKREP: return "BANKREP"; + case cbPKTTYPE_GROUPREP: return "GROUPREP"; + case cbPKTTYPE_FILTREP: return "FILTREP"; + case cbPKTTYPE_CHANREP: return "CHANREP"; + case cbPKTTYPE_CHANREPSPK: return "CHANREPSPK"; + default: + // Handle group packets (0x30-0x35) + if (type >= cbPKTTYPE_GROUPREP && type <= cbPKTTYPE_GROUPREP + 5) { + return "GROUP[0-5]"; + } + return "UNKNOWN"; + } +} + +/// Print packet information +void printPacket(const cbPKT_GENERIC& pkt) { + std::cout << " Type: 0x" << std::hex << std::setw(2) << std::setfill('0') + << (int)pkt.cbpkt_header.type << std::dec << " (" << getPacketTypeName(pkt.cbpkt_header.type) << ")" + << ", Inst: " << (int)pkt.cbpkt_header.instrument + << ", Chid: " << pkt.cbpkt_header.chid + << ", DLen: " << pkt.cbpkt_header.dlen << "\n"; +} + +/// Map device type string to DeviceType enum +DeviceType parseDeviceType(const std::string& type_str) { + if (type_str == "NSP") return DeviceType::LEGACY_NSP; + if (type_str == "GEMINI_NSP") return DeviceType::NSP; + if (type_str == "HUB1") return DeviceType::HUB1; + if (type_str == "HUB2") return DeviceType::HUB2; + if (type_str == "HUB3") return DeviceType::HUB3; + if (type_str == "NPLAY") return DeviceType::NPLAY; + + std::cerr << "ERROR: Unknown device type '" << type_str << "'\n"; + std::cerr << "Valid types: NSP, GEMINI_NSP, HUB1, HUB2, HUB3, NPLAY\n"; + std::exit(1); +} + +/// Get device type name string +const char* getDeviceTypeName(DeviceType type) { + switch (type) { + case DeviceType::LEGACY_NSP: return "LEGACY_NSP"; + case DeviceType::NSP: return "GEMINI_NSP"; + case DeviceType::HUB1: return "GEMINI_HUB1"; + case DeviceType::HUB2: return "GEMINI_HUB2"; + case DeviceType::HUB3: return "GEMINI_HUB3"; + case DeviceType::NPLAY: return "NPLAY"; + default: return "UNKNOWN"; + } +} + +int main(int argc, char* argv[]) { + std::cout << "================================================\n"; + std::cout << " CereLink Simple Device Example (cbdev only)\n"; + std::cout << "================================================\n\n"; + + // Parse command line arguments + DeviceType device_type = DeviceType::LEGACY_NSP; // Default to NSP + + if (argc >= 2) { + device_type = parseDeviceType(argv[1]); + } + + std::cout << "Configuration:\n"; + std::cout << " Device Type: " << getDeviceTypeName(device_type) << "\n"; + + // Register signal handler for clean shutdown + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + + // Step 1: Create SDK configuration from device type + SdkConfig config; + config.device_type = device_type; + config.autorun = true; // Automatically perform startup handshake + + std::cout << " Autorun: " << (config.autorun ? "enabled" : "disabled") << "\n\n"; + + // Step 2: Create SDK session (automatically starts and performs handshake if autorun=true) + std::cout << "Creating SDK session...\n"; + auto result = SdkSession::create(config); + if (result.isError()) { + std::cerr << "ERROR: Failed to create SDK session: " << result.error() << "\n"; + return 1; + } + auto session = std::move(result.value()); + std::cout << "SDK session created successfully!\n\n"; + + // Step 3: Set up packet callback + std::atomic packet_count{0}; + std::atomic spike_count{0}; + std::atomic config_count{0}; + + session.registerPacketCallback([&](const cbPKT_GENERIC& pkt) { + packet_count++; + + // Count spikes + if (pkt.cbpkt_header.type == cbPKTTYPE_CHANREPSPK) { + spike_count++; + } + + // Count config packets + if (pkt.cbpkt_header.chid == cbPKTCHAN_CONFIGURATION) { + config_count++; + + // Print first few config packets + if (config_count <= 10) { + printPacket(pkt); + } + } + }); + + std::cout << "Packet callback registered.\n\n"; + + // Step 4: Session is already running (auto-started by SDK) + std::cout << "SDK session is running and receiving packets...\n\n"; + + // Step 5: Run for specified duration, showing statistics + std::cout << "Receiving packets... (Press Ctrl+C to stop)\n\n"; + std::cout << "Statistics (updated every second):\n"; + std::cout << "-----------------------------------\n"; + + auto start_time = std::chrono::steady_clock::now(); + int seconds_elapsed = 0; + + while (g_running) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + // Get statistics + auto stats = session.getStats(); + auto now = std::chrono::steady_clock::now(); + seconds_elapsed = std::chrono::duration_cast(now - start_time).count(); + + // Print statistics + std::cout << "\r[" << std::setw(3) << seconds_elapsed << "s] " + << "Total: " << std::setw(8) << packet_count.load() + << " | Config: " << std::setw(6) << config_count.load() + << " | Spikes: " << std::setw(8) << spike_count.load() + << " | RX: " << std::setw(10) << stats.packets_received_from_device + << " pkts" + << " | Delivered: " << std::setw(10) << stats.packets_delivered_to_callback + << std::flush; + } + + std::cout << "\n\n"; + + // Step 7: Stop SDK session + std::cout << "Stopping SDK session...\n"; + session.stop(); + std::cout << "SDK session stopped.\n\n"; + + // Step 8: Print final statistics + auto final_stats = session.getStats(); + std::cout << "Final Statistics:\n"; + std::cout << "=================\n"; + std::cout << " Runtime: " << seconds_elapsed << " seconds\n"; + std::cout << " Packets from Device: " << final_stats.packets_received_from_device << "\n"; + std::cout << " Packets to Shmem: " << final_stats.packets_stored_to_shmem << "\n"; + std::cout << " Packets Queued: " << final_stats.packets_queued_for_callback << "\n"; + std::cout << " Packets Delivered: " << final_stats.packets_delivered_to_callback << "\n"; + std::cout << " Packets Dropped: " << final_stats.packets_dropped << "\n"; + std::cout << " Config Packets: " << config_count.load() << "\n"; + std::cout << " Spike Packets: " << spike_count.load() << "\n"; + std::cout << " Total Processed: " << packet_count.load() << "\n"; + std::cout << " Packets Sent to Device: " << final_stats.packets_sent_to_device << "\n"; + + if (final_stats.packets_received_from_device > 0 && seconds_elapsed > 0) { + double packets_per_sec = static_cast(final_stats.packets_received_from_device) / seconds_elapsed; + std::cout << " Average Rate: " << std::fixed << std::setprecision(1) + << packets_per_sec << " pkts/sec\n"; + } + + std::cout << "\nShutdown complete!\n"; + return 0; +} diff --git a/examples/SimpleIO/simple_callback.cpp b/examples/SimpleIO/simple_callback.cpp deleted file mode 100644 index 0fe332cb..00000000 --- a/examples/SimpleIO/simple_callback.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include -#include -#include -#include - - -#define INST 0 - - -void handleResult(const cbSdkResult res) -{ - switch (res) - { - case CBSDKRESULT_SUCCESS: - break; - case CBSDKRESULT_NOTIMPLEMENTED: - printf("Not implemented\n"); - break; - case CBSDKRESULT_INVALIDPARAM: - printf("Invalid parameter\n"); - break; - case CBSDKRESULT_WARNOPEN: - printf("Real-time interface already initialized\n"); - case CBSDKRESULT_WARNCLOSED: - printf("Real-time interface already closed\n"); - break; - case CBSDKRESULT_ERROPENCENTRAL: - printf("Unable to open library for Central interface\n"); - break; - case CBSDKRESULT_ERROPENUDP: - printf("Unable to open library for UDP interface\n"); - break; - case CBSDKRESULT_ERROPENUDPPORT: - printf("Unable to open UDP interface\n"); - break; - case CBSDKRESULT_OPTERRUDP: - printf("Unable to set UDP interface option\n"); - break; - case CBSDKRESULT_MEMERRUDP: - printf("Unable to assign UDP interface memory\n" - " Consider using sysctl -w net.core.rmem_max=8388608\n" - " or sysctl -w kern.ipc.maxsockbuf=8388608\n"); - break; - case CBSDKRESULT_INVALIDINST: - printf("Invalid UDP interface\n"); - break; - case CBSDKRESULT_ERRMEMORYTRIAL: - printf("Unable to allocate RAM for trial cache data\n"); - break; - case CBSDKRESULT_ERROPENUDPTHREAD: - printf("Unable to Create UDP thread\n"); - break; - case CBSDKRESULT_ERROPENCENTRALTHREAD: - printf("Unable to start Cerebus real-time data thread\n"); - break; - case CBSDKRESULT_ERRINIT: - printf("Library initialization error\n"); - break; - case CBSDKRESULT_ERRMEMORY: - printf("Library memory allocation error\n"); - break; - case CBSDKRESULT_TIMEOUT: - printf("Connection timeout error\n"); - break; - case CBSDKRESULT_ERROFFLINE: - printf("Instrument is offline\n"); - break; - default: - printf("Unexpected error\n"); - break; - } -} - - -void chaninfo_callback(uint32_t nInstance, const cbSdkPktType type, const void* pEventData, void* pCallbackData) -{ - if (type == cbSdkPkt_CHANINFO) { - auto chan_info = *(static_cast(pEventData)); - auto chan_label = static_cast*>(pCallbackData); - chan_label->first = chan_info.chan; - chan_label->second = chan_info.label; - printf("chaninfo_callback received CHANINFO for %u:%s\n", - chan_label->first, chan_label->second.c_str()); - } -} - - -int main(const int argc, char *argv[]) { - auto inst_ip = ""; - int inst_port = cbNET_UDP_PORT_CNT; - auto client_ip = ""; - // Parse command line arguments. - { - if (argc > 1 && argv[1][0] != '-') {inst_ip = argv[1];} - if (argc > 2 && argv[2][0] != '-') { - try { - inst_port = std::stoi(argv[2]); - } catch (const std::exception&) { - printf("Error: Invalid port number '%s'\n", argv[2]); - return 1; - } - } - if (argc > 3 && argv[3][0] != '-') { client_ip = argv[3]; } - } - - const cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - auto con = cbSdkConnection(); - con.szOutIP = inst_ip; - con.nOutPort = inst_port; - con.szInIP = client_ip; - con.nInPort = inst_port; - cbSdkResult res = cbSdkOpen(INST, conType, con); - handleResult(res); - if (res != CBSDKRESULT_SUCCESS) { - printf("Unable to open instrument connection.\n"); - return -1; - } - printf("cbSdkOpen succeeded\n"); - - std::pair chan_label_pair(0, ""); - cbSdkRegisterCallback( - INST, - cbSdkCallbackType::CBSDKCALLBACK_CHANINFO, - &chaninfo_callback, - &chan_label_pair - ); - std::cout << "Please use central to modify a channel label." << std::flush; - while (chan_label_pair.first == 0) - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - // Even if we reduce the sleep_for to a very small number, we probably still get 2 callback calls - // before we have a chance to unregister. - cbSdkUnRegisterCallback(INST, cbSdkCallbackType::CBSDKCALLBACK_CHANINFO); - printf("caller received chan_label for %u:%s\n", - chan_label_pair.first, chan_label_pair.second.c_str()); - res = cbSdkClose(INST); - if (res < 0) - printf("cbSdkClose failed (%d)!\n", res); - else - printf("cbSdkClose succeeded\n"); - handleResult(res); - -} \ No newline at end of file diff --git a/examples/SimpleIO/simple_io.cpp b/examples/SimpleIO/simple_io.cpp deleted file mode 100644 index 304dab1a..00000000 --- a/examples/SimpleIO/simple_io.cpp +++ /dev/null @@ -1,463 +0,0 @@ -/////////////////////////////////////////////////////////////////////// -// -// Test IO -// -// Purpose: -// This extends testcbsdk with explicit testing of data io. -// This is incomplete and only includes tests that were relevant to debugging specific problems. -// -// Call with -c to enable continuous data, -e to enable events, -// and -r to specify a fixed runtime. -// While running, press Ctrl+C to stop. -// -// Note: -// Make sure only the SDK is used here, and not cbhwlib directly -// this will ensure SDK is capable of whatever test suite can do. -// Do not throw exceptions, catch possible exceptions and handle them the earliest possible in this library -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#define INST 0 - -// Global flag to signal shutdown -static std::atomic g_bShutdown(false); - -// Signal handler for Ctrl+C -void signal_handler(const int signal) -{ - if (signal == SIGINT) - { - g_bShutdown = true; - printf("\nShutdown requested...\n"); - } -} - -typedef struct cbsdk_config { - uint32_t nInstance; - uint32_t bActive; - uint16_t begchan; - uint32_t begmask; - uint32_t begval; - uint16_t endchan; - uint32_t endmask; - uint32_t endval; - bool bDouble; - uint32_t uWaveforms; - uint32_t uConts; - uint32_t uEvents; - uint32_t uComments; - uint32_t uTrackings; - bool bAbsolute; -} cbsdk_config; - -void handleResult(const cbSdkResult res) -{ - switch (res) - { - case CBSDKRESULT_SUCCESS: - break; - case CBSDKRESULT_NOTIMPLEMENTED: - printf("Not implemented\n"); - break; - case CBSDKRESULT_INVALIDPARAM: - printf("Invalid parameter\n"); - break; - case CBSDKRESULT_WARNOPEN: - printf("Real-time interface already initialized\n"); - case CBSDKRESULT_WARNCLOSED: - printf("Real-time interface already closed\n"); - break; - case CBSDKRESULT_ERROPENCENTRAL: - printf("Unable to open library for Central interface\n"); - break; - case CBSDKRESULT_ERROPENUDP: - printf("Unable to open library for UDP interface\n"); - break; - case CBSDKRESULT_ERROPENUDPPORT: - printf("Unable to open UDP interface\n"); - break; - case CBSDKRESULT_OPTERRUDP: - printf("Unable to set UDP interface option\n"); - break; - case CBSDKRESULT_MEMERRUDP: - printf("Unable to assign UDP interface memory\n" - " Consider using sysctl -w net.core.rmem_max=8388608\n" - " or sysctl -w kern.ipc.maxsockbuf=8388608\n"); - break; - case CBSDKRESULT_INVALIDINST: - printf("Invalid UDP interface\n"); - break; - case CBSDKRESULT_ERRMEMORYTRIAL: - printf("Unable to allocate RAM for trial cache data\n"); - break; - case CBSDKRESULT_ERROPENUDPTHREAD: - printf("Unable to Create UDP thread\n"); - break; - case CBSDKRESULT_ERROPENCENTRALTHREAD: - printf("Unable to start Cerebus real-time data thread\n"); - break; - case CBSDKRESULT_ERRINIT: - printf("Library initialization error\n"); - break; - case CBSDKRESULT_ERRMEMORY: - printf("Library memory allocation error\n"); - break; - case CBSDKRESULT_TIMEOUT: - printf("Connection timeout error\n"); - break; - case CBSDKRESULT_ERROFFLINE: - printf("Instrument is offline\n"); - break; - default: - printf("Unexpected error\n"); - break; - } -} - -cbSdkVersion getVersion() -{ - // Library version can be read even before library open (return value is a warning) - // actual NSP version however needs library to be open - cbSdkVersion ver; - const cbSdkResult res = cbSdkGetVersion(INST, &ver); - if (res != CBSDKRESULT_SUCCESS) - { - printf("Unable to determine instrument version\n"); - } - else { - printf("Initializing Cerebus real-time interface %d.%02d.%02d.%02d (protocol cb%d.%02d)...\n", - ver.major, ver.minor, ver.release, ver.beta, ver.majorp, ver.minorp); - } - handleResult(res); - return ver; -} - -cbSdkResult open(const LPCSTR inst_ip, const int inst_port, const LPCSTR client_ip) -{ - // Try to get the version. Should be a warning because we are not yet open. - getVersion(); - - // Open the device using default connection type. - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - auto con = cbSdkConnection(); - con.szOutIP = inst_ip; - con.nOutPort = inst_port; - con.szInIP = client_ip; - con.nInPort = inst_port; - cbSdkResult res = cbSdkOpen(INST, conType, con); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to open instrument connection.\n"); - handleResult(res); - - cbSdkInstrumentType instType; - if (res >= 0) - { - // Return the actual opened connection - res = cbSdkGetType(INST, &conType, &instType); - if (res != CBSDKRESULT_SUCCESS) - printf("Unable to determine connection type\n"); - handleResult(res); - // if (instType == CBSDKINSTRUMENT_NPLAY || instType == CBSDKINSTRUMENT_REMOTENPLAY) - // printf("Unable to open UDP interface to nPlay\n"); - - if (conType > CBSDKCONNECTION_CLOSED) - conType = CBSDKCONNECTION_COUNT; - if (instType > CBSDKINSTRUMENT_COUNT) - instType = CBSDKINSTRUMENT_COUNT; - - char strConnection[CBSDKCONNECTION_COUNT + 1][8] = {"Default", "Central", "Udp", "Closed", "Unknown"}; - char strInstrument[CBSDKINSTRUMENT_COUNT + 1][13] = {"NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"}; - - // Now that we are open, get the version again. - const cbSdkVersion ver = getVersion(); - - // Summary results. - printf("%s real-time interface to %s (%d.%02d.%02d.%02d) successfully initialized\n", - strConnection[conType], strInstrument[instType], - ver.nspmajor, ver.nspminor, ver.nsprelease, ver.nspbeta); - } - return res; -} - -void setConfig(const bool bCont, bool bEv) -{ - uint32_t proc = 1; - uint32_t nChansInGroup; - uint16_t pGroupList[cbNUM_ANALOG_CHANS]; - for (uint32_t group_ix = 1; group_ix < 7; group_ix++) - { - const cbSdkResult res = cbSdkGetSampleGroupList(INST, proc, group_ix, &nChansInGroup, pGroupList); - if (res == CBSDKRESULT_SUCCESS) - { - printf("In sampling group %d, found %d channels.\n", group_ix, nChansInGroup); - } - handleResult(res); - } - if (bCont) - { - // Set sample group 3 and filter 7 on the first few channels. - // Also disable spiking. - for (int chan_ix = 0; chan_ix < cbNUM_ANALOG_CHANS; ++chan_ix) - { - cbSdkSetAinpSampling(INST, chan_ix + 1, 7, chan_ix < 2 ? 3 : 0); - cbSdkSetAinpSpikeOptions(INST, chan_ix + 1, 0, 2); - } - } - std::this_thread::sleep_for(std::chrono::seconds(2)); -} - -cbSdkResult close() -{ - const cbSdkResult res = cbSdkClose(INST); - if (res == CBSDKRESULT_SUCCESS) - { - printf("Interface closed successfully\n"); - } - else - { - printf("Unable to close interface.\n"); - handleResult(res); - } - return res; -} - -int main(const int argc, char *argv[]) -{ - auto inst_ip = ""; - int inst_port = cbNET_UDP_PORT_CNT; - auto client_ip = ""; - bool bCont = false; - bool bEv = false; - bool bComm = false; - uint32_t runtime = 30000; - - // Parse command line arguments. - { - if (argc > 1 && argv[1][0] != '-') {inst_ip = argv[1];} - if (argc > 2 && argv[2][0] != '-') { - try { - inst_port = std::stoi(argv[2]); - } catch (const std::exception&) { - printf("Error: Invalid port number '%s'\n", argv[2]); - return 1; - } - } - if (argc > 3 && argv[3][0] != '-') { client_ip = argv[3]; } - - for (size_t optind = 1; optind < argc; optind++) - { - if (argv[optind][0] == '-') - { - switch (const char option = argv[optind][1]) { - case 'c': bCont = true; break; - case 'e': bEv = true; break; - case 'i': bComm = true; break; - case 'r': runtime = 30000 * (argv[optind][2] - '0'); break; - case '\0': - printf("Error: Invalid option '-' without a flag character\n"); - printf("Usage: %s [inst_ip] [inst_port] [client_ip] [-c] [-e] [-i] [-rN]\n", argv[0]); - printf(" -c: Enable continuous data\n"); - printf(" -e: Enable event data\n"); - printf(" -i: Enable comment data\n"); - printf(" -rN: Set runtime (N * 30000 ticks)\n"); - return 1; - default: - printf("Error: Unrecognized option '-%c'\n", option); - printf("Usage: %s [inst_ip] [inst_port] [client_ip] [-c] [-e] [-i] [-rN]\n", argv[0]); - printf(" -c: Enable continuous data\n"); - printf(" -e: Enable event data\n"); - printf(" -i: Enable comment data\n"); - printf(" -rN: Set runtime (N * 30000 ticks)\n"); - return 1; - } - } - } - } - - // Install signal handler for Ctrl+C - std::signal(SIGINT, signal_handler); - - // Connect to the device. - cbSdkResult res = open(inst_ip, inst_port, client_ip); - if (res < 0) - { - printf("open failed (%d)!\n", res); - return -1; - } - printf("open succeeded\n"); - - setConfig(bCont, bEv); - - cbsdk_config cfg; - cbSdkGetTrialConfig( - INST, - &cfg.bActive, - &cfg.begchan, &cfg.begmask, &cfg.begval, - &cfg.endchan, &cfg.endmask, &cfg.endval, - &cfg.uWaveforms, - &cfg.uConts, - &cfg.uEvents, - &cfg.uComments, - &cfg.uTrackings - ); - cbSdkSetTrialConfig( - INST, - 1, - 0, 0, 0, - 0, 0, 0, - cfg.uWaveforms, - bCont ? cbSdk_CONTINUOUS_DATA_SAMPLES : 0, - bEv ? cbSdk_EVENT_DATA_SAMPLES : 0, - cfg.uComments, - cfg.uTrackings - ); - - auto trialEvent = std::unique_ptr(nullptr); - if (bEv) - trialEvent = std::make_unique(); - auto trialCont = std::unique_ptr(nullptr); - if (bCont) { - trialCont = std::make_unique(); - trialCont->group = 3; - } - auto trialComm = std::unique_ptr(nullptr); - if (bComm) - trialComm = std::make_unique(); - - // Continuous data: single contiguous buffer for all channels and samples - std::vector cont_samples; // [num_samples * count] contiguous array - std::vector cont_timestamps; // [num_samples] timestamps - // Event data - std::vector event_ts; // [num_events] flat timestamp array - std::vector event_channels; // [num_events] flat channel array - std::vector event_units; // [num_events] flat unit array - - PROCTIME start_time; - PROCTIME elapsed_time = 0; - cbSdkGetTime(INST, &start_time); - while (!g_bShutdown && ((runtime == 0) || (elapsed_time < runtime))) - { - cbSdkGetTime(INST, &elapsed_time); - elapsed_time -= start_time; - - // cbSdkInitTrialData to determine how many samples are available - cbSdkInitTrialData( - INST, - 0, - trialEvent.get(), - trialCont.get(), - trialComm.get(), - nullptr - ); - - // allocate memory for flat event arrays - if (trialEvent && trialEvent->num_events) - { - // Allocate flat arrays for all events - event_ts.resize(trialEvent->num_events); - event_channels.resize(trialEvent->num_events); - event_units.resize(trialEvent->num_events); - - // Assign pointers to trialEvent structure - trialEvent->timestamps = event_ts.data(); - trialEvent->channels = event_channels.data(); - trialEvent->units = event_units.data(); - trialEvent->waveforms = nullptr; // Not using waveforms in this example - } - - if (trialCont && (trialCont->count > 0)) - { - // Allocate memory for continuous data. - // Data layout is [num_samples][count] - contiguous array of num_samples * count elements - const uint32_t n_samples = trialCont->num_samples; - const uint32_t n_channels = trialCont->count; - - // Allocate contiguous buffer for all samples and channels - cont_samples.assign(n_samples * n_channels, 0); - trialCont->samples = cont_samples.data(); - - // Allocate timestamps array - cont_timestamps.assign(n_samples, 0); - trialCont->timestamps = cont_timestamps.data(); - } - - // TODO: Allocate memory for comment data - - // cbSdkGetTrialData to fetch the data - cbSdkGetTrialData( - INST, - 1, - trialEvent.get(), - trialCont.get(), - trialComm.get(), - nullptr - ); - - // Do something with the data. - if (trialEvent && trialEvent->num_events) - { - // Print first few events as example - const size_t n_to_print = std::min(static_cast(10), static_cast(trialEvent->num_events)); - for (size_t ev_ix = 0; ev_ix < n_to_print; ev_ix++) - { - printf("Event %zu: ch=%u unit=%u ts=%" PRIu64 "\n", - ev_ix, - event_channels[ev_ix], - event_units[ev_ix], - static_cast(event_ts[ev_ix])); - } - if (trialEvent->num_events > n_to_print) - printf("... and %u more events\n", trialEvent->num_events - static_cast(n_to_print)); - } - if (trialCont && trialCont->count) - { - const uint32_t n_samples = trialCont->num_samples; - const uint32_t n_channels = trialCont->count; - if (n_samples > 0) - { - for (size_t chan_ix = 0; chan_ix < n_channels; chan_ix++) - { - // Extract min/max for this channel from the contiguous [sample][channel] array - // Data for channel chan_ix at sample i is at: cont_samples[i * n_channels + chan_ix] - int16_t min_val = cont_samples[chan_ix]; // First sample for this channel - int16_t max_val = cont_samples[chan_ix]; - for (size_t sample_ix = 0; sample_ix < n_samples; sample_ix++) - { - const int16_t val = cont_samples[sample_ix * n_channels + chan_ix]; - if (val < min_val) min_val = val; - if (val > max_val) max_val = val; - } - std::cout << "chan = " << trialCont->chan[chan_ix] - << ", nsamps = " << n_samples - << ", min = " << min_val - << ", max = " << max_val - << " (t " << cont_timestamps[0] << " - " << cont_timestamps[n_samples - 1] << ")" - << '\n'; - } - } - } - } - - printf("Shutting down...\n"); - res = close(); - if (res < 0) - printf("close failed (%d)!\n", res); - else - printf("close succeeded\n"); - - // No need to clear trialCont, trialEvent and trialComm because they are smart pointers and will dealloc. - - return 0; -} diff --git a/include/cerelink/cbhwlib.h b/include/cerelink/cbhwlib.h deleted file mode 100644 index 51f71d05..00000000 --- a/include/cerelink/cbhwlib.h +++ /dev/null @@ -1,1006 +0,0 @@ -#ifndef CBHWLIB_H_INCLUDED -#define CBHWLIB_H_INCLUDED - -#include "cbproto.h" - -#if defined(WIN32) -#ifndef _CRT_SECURE_NO_DEPRECATE -#define _CRT_SECURE_NO_DEPRECATE -#endif -// It has to be in this order for right version of sockets -#ifdef NO_AFX -#include -#include -#endif // NO_AFX -#endif // defined(WIN32) -#if defined(WIN32) -#include "pstdint.h" -#else -#include "stdint.h" -#include // For NULL on some compilers -#endif - -#pragma pack(push, 1) - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Systemwide Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -// Open multiple instances of library as stand-alone or under Central application -cbRESULT cbOpen(bool bStandAlone = false, uint32_t nInstance = 0); -// Initializes the Neuromatic library (and establishes a link to the Central Control Application if bStandAlone is FALSE). -// This function must be called before any other functions are called from this library. -// Returns OK, NOCENTRALAPP, LIBINITERROR, MEMORYUNVAIL, HARDWAREOFFLINE, INSTOUTDATED or LIBOUTDATED - -cbRESULT cbClose(bool bStandAlone = false, uint32_t nInstance = 0); -// Close the library (must match how library is openned) - -cbRESULT cbCheckApp(const char * lpName); -// Check if an application is running using its mutex - -cbRESULT cbAcquireSystemLock(const char * lpName, HANDLE & hLock); -cbRESULT cbReleaseSystemLock(const char * lpName, HANDLE & hLock); -// Acquire or release application system lock - -uint32_t GetInstrumentLocalChan(uint32_t nChan, uint32_t nInstance = 0); -// Get the instrument local channel number - -#define cbINSTINFO_READY 0x0001 // Instrument is connected -#define cbINSTINFO_LOCAL 0x0002 // Instrument runs on the localhost -#define cbINSTINFO_NPLAY 0x0004 // Instrument is nPlay -#define cbINSTINFO_CEREPLEX 0x0008 // Instrument is Cereplex -#define cbINSTINFO_EMULATOR 0x0010 // Instrument is Emulator -#define cbINSTINFO_NSP1 0x0020 // Instrument is NSP1 -#define cbINSTINFO_WNSP 0x0040 // Instrument is WNSP -#define cbINSTINFO_GEMINI_NSP 0x0080 // Instrument is Gemini NSP -#define cbINSTINFO_GEMINI_HUB 0x0100 // Instrument is Gemini Hub -cbRESULT cbGetInstInfo(uint8_t nInstrument, uint32_t *instInfo, uint32_t nInstance = 0); -// Purpose: get instrument information. - -cbRESULT cbGetLatency(uint32_t *nLatency, uint32_t nInstance = 0); -// Purpose: get instrument latency. - -// Returns instrument information -// Returns cbRESULT_OK if successful, cbRESULT_NOLIBRARY if library was never initialized. -cbRESULT cbGetSystemClockFreq(uint32_t *freq, uint32_t nInstance = 0); -// Retrieves the system timestamp/sample clock frequency (in Hz) from the Central App cache. -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized - -cbRESULT cbGetSystemClockTime(PROCTIME *time, uint32_t nInstance = 0); -// Retrieves the last 32-bit timestamp from the Central App cache. -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized - - -// Shuts down the programming library and frees any resources linked in cbOpen() -// Returns cbRESULT_OK if successful, cbRESULT_NOLIBRARY if library was never initialized. -// Updates the read pointers in the memory area so that -// all the un-read packets are ignored. In other words, it -// initializes all the pointers so that the begging of read time is NOW. -cbRESULT cbMakePacketReadingBeginNow(uint32_t nInstance = 0); -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Data checking and processing functions -// -// To get data from the shared memory buffers used in the Central App, the user can: -// 1) periodically poll for new data using a multimedia or windows timer -// 2) create a thread that uses a Win32 Event synchronization object to que the data polling -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -enum cbLevelOfConcern -{ - LOC_LOW, // Time for sippen lemonaide - LOC_MEDIUM, // Step up to the plate - LOC_HIGH, // Put yer glass down - LOC_CRITICAL, // Get yer but in gear - LOC_COUNT // How many level of concerns are there -}; - -cbRESULT cbCheckforData(cbLevelOfConcern & nLevelOfConcern, uint32_t *pktstogo = nullptr, uint32_t nInstance = 0); -// The pktstogo and timetogo are optional fields (NULL if not used) that returns the number of new -// packets and timestamps that need to be read to catch up to the buffer. -// -// Returns: cbRESULT_OK if there is new data in the buffer -// cbRESULT_NONEWDATA if there is no new data available -// cbRESULT_DATALOST if the Central App incoming data buffer has wrapped the read buffer - -cbRESULT cbWaitforData(uint32_t nInstance = 0); -// Executes a WaitForSingleObject command to wait for the Central App event signal -// -// Returns: cbRESULT_OK if there is new data in the buffer -// cbRESULT_NONEWDATA if the function timed out after 250ms -// cbRESULT_DATALOST if the Central App incoming data buffer has wrapped the read buffer -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// NEV file definitions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -cbPKT_GENERIC *cbGetNextPacketPtr(uint32_t nInstance = 0); -// Returns pointer to next packet in the shared memory space. If no packet available, returns NULL - -// Cerebus Library function to send packets via the Central Application Queue -cbRESULT cbSendPacket(void * pPacket, uint32_t nInstance = 0); -cbRESULT cbSendPacketToInstrument(void * pPacket, uint32_t nInstance = 0, uint32_t nInstrument = cbNSP1 - 1); -cbRESULT cbSendLoopbackPacket(void * pPacket, uint32_t nInstance = 0); - -cbRESULT cbGetVideoSource(char *name, float *fps, uint32_t id, uint32_t nInstance = 0); -cbRESULT cbSetVideoSource(const char *name, float fps, uint32_t id, uint32_t nInstance = 0); -// Get/Set the video source parameters. -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetTrackObj(char *name, uint16_t *type, uint16_t *pointCount, uint32_t id, uint32_t nInstance = 0); -cbRESULT cbSetTrackObj(const char *name, uint16_t type, uint16_t pointCount, uint32_t id, uint32_t nInstance = 0); -// Get/Set the trackable object parameters. -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetChanCaps(uint32_t chan, uint32_t *chancaps, uint32_t nInstance = 0); -// Retreives the channel capabilities from the Central App Cache. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Digital Input Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -cbRESULT cbGetDinpCaps(uint32_t chan, uint32_t *dinpcaps, uint32_t nInstance = 0); -// Retreives the channel's digital input capabilities from the Central App Cache. -// Port Capabilities are reported as compbined cbDINPOPT_* flags. Zero = no DINP capabilities. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetDinpOptions(uint32_t chan, uint32_t *options, uint32_t *eopchar, uint32_t nInstance = 0); -cbRESULT cbSetDinpOptions(uint32_t chan, uint32_t options, uint32_t eopchar, uint32_t nInstance = 0); -// Get/Set the Digital Input Port options for the specified channel. -// -// Port options are expressed as a combined set of cbDINP_* option flags, for example: -// a) cbDINP_SERIAL + cbDINP_BAUDxx = capture single 8-bit RS232 serial values. -// b) cbDINP_SERIAL + cbDINP_BAUDxx + cbDINP_PKTCHAR = capture serial packets that are terminated -// with an end of packet character (only the lower 8 bits are used). -// c) cbDINP_1BIT + cbDINP_ANYBIT = capture the changes of a single digital input line. -// d) cbDINP_xxBIT + cbDINP_ANYBIT = capture the xx-bit input word when any bit changes. -// e) cbDINP_xxBIT + cbDINP_WRDSTRB = capture the xx-bit input based on a word-strobe line. -// f) cbDINP_xxBIT + cbDINP_WRDSTRB + cbDINP_PKTCHAR = capture packets composed of xx-bit words -// in which the packet is terminated with the specified end-of-packet character. -// g) cbDINP_xxBIT + cbDINP_WRDSTRB + cbDINP_PKTLINE = capture packets composed of xx-bit words -// in which the last character of a packet is accompanyied with an end-of-pkt logic signal. -// h) cbDINP_xxBIT + cbDINP_REDGE = capture the xx-bit input word when any bit goes from low to hi. -// i) cbDINP_xxBIT + dbDINP_FEDGE = capture the xx-bit input word when any bit goes from hi to low. -// -// NOTE: If the end-of-packet character value (eopchar) is not used in the options, it is ignored. -// -// Add cbDINP_PREVIEW to the option set to get preview updates (cbPKT_PREVDINP) at each word, -// not only when complete packets are sent. -// -// The Get function returns values from the Central Control App cache. The Set function validates -// that the specified options are available and then queues a cbPKT_SETDINPOPT packet. The system -// acknowledges this change with a cbPKT_ACKDINPOPT packet. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDFUNCTION a requested option is not available on that channel. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Digital Output Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -cbRESULT cbGetDoutCaps(uint32_t chan, uint32_t *doutcaps, uint32_t nInstance = 0); -// Retreives the channel's digital output capabilities from the Central Control App Cache. -// Port Capabilities are reported as compbined cbDOUTOPT_* flags. Zero = no DINP capabilities. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetDoutOptions(uint32_t chan, uint32_t *options, uint32_t *monchan, int32_t *doutval, - uint8_t *triggertype = nullptr, uint16_t *trigchan = nullptr, uint16_t *trigval = nullptr, uint32_t nInstance = 0); -cbRESULT cbSetDoutOptions(uint32_t chan, uint32_t options, uint32_t monchan, int32_t doutval, - uint8_t triggertype = cbDOUT_TRIGGER_NONE, uint16_t trigchan = 0, uint16_t trigval = 0, uint32_t nInstance = 0); -// Get/Set the Digital Output Port options for the specified channel. -// -// The only changable DOUT options in this version of the interface libraries are baud rates for -// serial output ports. These are set with the cbDOUTOPT_BAUDxx options. -// -// The Get function returns values from the Central Control App cache. The Set function validates -// that the specified options are available and then queues a cbPKT_SETDOUTOPT packet. The system -// acknowledges this change with a cbPKT_REPDOUTOPT packet. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOINTERNALCHAN if there is no internal channel for mapping the in->out chan -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Analog Input Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -cbRESULT cbGetAinpCaps(uint32_t chan, uint32_t *ainpcaps, cbSCALING *physcalin, cbFILTDESC *phyfiltin, uint32_t nInstance = 0); -// Retreives the channel's analog input capabilities from the Central Control App Cache. -// Capabilities are reported as combined cbAINP_* flags. Zero = no AINP capabilities. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetAinpOpts(uint32_t chan, uint32_t *ainpopts, uint32_t *LNCrate, uint32_t *refElecChan, uint32_t nInstance = 0); -cbRESULT cbSetAinpOpts(uint32_t chan, uint32_t ainpopts, uint32_t LNCrate, uint32_t refElecChan, uint32_t nInstance = 0); -// Get and Set the user-assigned amplitude reject values. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. -// -// The LNC configuration is composed of an adaptation rate and a mode variable. The rate sets the -// first order decay of the filter according to: -// -// newLNCvalue = (LNCrate/65536)*(oldLNCvalue) + ((65536-LNCrate)/65536)*LNCsample -// -// The relationships between the adaptation time constant in sec, line frequency in Hz and -// the LNCrate variable are given below: -// -// time_constant = 1 / ln[ (LNCrate/65536)^(-line_freq) ] -// -// LNCrate = 65536 * e^[-1/(time_constant*line_freq)] -// -// The LNCmode sets whether the channel LNC block is disabled, running, or on hold. -// -// To set multiple channels on hold or run, pass channel=0. In this case, the LNCrate is ignored -// and the run or hold value passed to the LNCmode variable is applied to all LNC enabled channels. - - -cbRESULT cbGetAinpScaling(uint32_t chan, cbSCALING *scaling, uint32_t nInstance = 0); -cbRESULT cbSetAinpScaling(uint32_t chan, const cbSCALING *scaling, uint32_t nInstance = 0); -// Get/Set the user-specified scaling for the channel. The digmin and digmax values of the user -// specified scaling must be within the digmin and digmax values for the physical channel mapping. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetAinpDisplay(uint32_t chan, int32_t *smpdispmin, int32_t *smpdispmax, int32_t *spkdispmax, int32_t *lncdispmax, uint32_t nInstance = 0); -cbRESULT cbSetAinpDisplay(uint32_t chan, int32_t smpdispmin, int32_t smpdispmax, int32_t spkdispmax, int32_t lncdispmax, uint32_t nInstance = 0); -// Get and Set the display ranges used by User applications. smpdispmin/max set the digital value -// range that should be displayed for the sampled analog stream. Spike streams are assumed to be -// symmetric about zero so that spikes should be plotted from -spkdispmax to +spkdispmax. Passing -// zero as a scale instructs the Central app to send the cached value. Fields with NULL pointers -// are ignored by the library. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbSetAinpPreview(uint32_t chan, uint32_t prevopts, uint32_t nInstance = 0); -// Requests preview packets for a specific channel. -// Setting the AINPPREV_LNC option gets a single LNC update waveform. -// Setting the AINPPREV_STREAMS enables compressed preview information. -// -// A channel ID of zero requests the specified preview packets from all active ainp channels. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -////////////////////////////////////////////////////////////// -// AINP Continuous Stream Functions - -cbRESULT cbGetAinpSampling(uint32_t chan, uint32_t *filter, uint32_t *group, uint32_t nInstance = 0); -cbRESULT cbSetAinpSampling(uint32_t chan, uint32_t filter, uint32_t group, uint32_t nInstance = 0); -// Get/Set the periodic sample group for the channel. Continuous sampling is performed in -// groups with each Neural Signal Processor. There are up to 4 groups for each processor. -// A group number of zero signifies that the channel is not part of a continuous sample group. -// filter = 1 to cbNFILTS, 0 is reserved for the null filter case -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_INVALIDFUNCTION if the group number is not valid. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -////////////////////////////////////////////////////////////// -// AINP Spike Stream Functions - -cbRESULT cbGetAinpSpikeCaps(uint32_t chan, uint32_t *flags, uint32_t nInstance = 0); -cbRESULT cbGetAinpSpikeOptions(uint32_t chan, uint32_t *flags, uint32_t *filter, uint32_t nInstance = 0); -cbRESULT cbSetAinpSpikeOptions(uint32_t chan, uint32_t flags, uint32_t filter, uint32_t nInstance = 0); -// Get/Set spike capabilities and options. The EXTRACT flag must be set for a channel to perform -// spike extraction and processing. The HOOPS and TEMPLATE flags are exclusive, only one can be -// used at a time. -// the THRSHOVR flag will turn auto thresholding off for that channel -// filter = 1 to cbNFILTS, 0 is reserved for the null filter case -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_INVALIDFUNCTION if invalid flag combinations are passed. -// cbRESULT_NOLIBRARY if the library was not properly initialized. -// - - -cbRESULT cbGetAinpSpikeThreshold(uint32_t chan, int32_t *level, uint32_t nInstance = 0); -cbRESULT cbSetAinpSpikeThreshold(uint32_t chan, int32_t level, uint32_t nInstance = 0); -// Get/Set the spike detection threshold and threshold detection mode. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_INVALIDFUNCTION if invalid flag combinations are passed. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetAinpSpikeHoops(uint32_t chan, cbHOOP *hoops, uint32_t nInstance = 0); -cbRESULT cbSetAinpSpikeHoops(uint32_t chan, const cbHOOP *hoops, uint32_t nInstance = 0); -// Get/Set the spike hoop set. The hoops parameter points to an array of hoops declared as -// cbHOOP hoops[cbMAXUNITS][cbMAXHOOPS]. -// -// Empty hoop definitions have zeros for the cbHOOP structure members. Hoop definitions can be -// cleared by passing a NULL cbHOOP pointer to the Set function or by calling the Set function -// with an all-zero cbHOOP structure. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_INVALIDFUNCTION if an invalid unit or hoop number is passed. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Analog Output Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -cbRESULT cbGetAoutCaps(uint32_t chan, uint32_t *aoutcaps, cbSCALING *physcalout, cbFILTDESC *phyfiltout, uint32_t nInstance = 0); -// Get/Set the spike template capabilities and options. The nunits and nhoops values detail the -// number of units that the channel supports. -// -// Empty template definitions have zeros for the cbSPIKETEMPLATE structure members. Spike -// Template definitions can be cleared by passing a NULL cbSPIKETEMPLATE pointer to the Set -// function or by calling the Set function with an all-zero cbSPIKETEMPLATE structure. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_INVALIDFUNCTION if an invalid unit number is passed. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetAoutScaling(uint32_t chan, cbSCALING *scaling, uint32_t nInstance = 0); -cbRESULT cbSetAoutScaling(uint32_t chan, const cbSCALING *scaling, uint32_t nInstance = 0); -// Get/Set the user-specified scaling for the channel. The digmin and digmax values of the user -// specified scaling must be within the digmin and digmax values for the physical channel mapping. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetAoutOptions(uint32_t chan, uint32_t *options, uint32_t *monchan, int32_t *value, uint32_t nInstance = 0); -cbRESULT cbSetAoutOptions(uint32_t chan, uint32_t options, uint32_t monchan, int32_t value, uint32_t nInstance = 0); -// Get/Set the Monitored channel for an Analog Output Port. Setting zero for the monitored channel -// stops the monitoring and frees any instrument monitor resources. The factor ranges -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_NOINTERNALCHAN if there is no internal channel for mapping the in->out chan -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -// Request that the sorting model be updated -cbRESULT cbGetSortingModel(uint32_t nInstance = 0); -cbRESULT cbGetFeatureSpaceDomain(uint32_t nInstance = 0); - - -// Getting and setting the noise boundary -cbRESULT cbSSGetNoiseBoundary(uint32_t chanIdx, float afCentroid[3], float afMajor[3], float afMinor_1[3], float afMinor_2[3], uint32_t nInstance = 0); -cbRESULT cbSSSetNoiseBoundary(uint32_t chanIdx, float afCentroid[3], float afMajor[3], float afMinor_1[3], float afMinor_2[3], uint32_t nInstance = 0); - -cbRESULT cbSSGetNoiseBoundaryByTheta(uint32_t chanIdx, float afCentroid[3], float afAxisLen[3], float afTheta[3], uint32_t nInstance = 0); -cbRESULT cbSSSetNoiseBoundaryByTheta(uint32_t chanIdx, const float afCentroid[3], const float afAxisLen[3], const float afTheta[3], uint32_t nInstance = 0); - -// Getting and settings statistics -cbRESULT cbSSGetStatistics(uint32_t * pnUpdateSpikes, uint32_t * pnAutoalg, uint32_t * nMode, - float * pfMinClusterPairSpreadFactor, - float * pfMaxSubclusterSpreadFactor, - float * pfMinClusterHistCorrMajMeasure, - float * pfMaxClusterPairHistCorrMajMeasure, - float * pfClusterHistValleyPercentage, - float * pfClusterHistClosePeakPercentage, - float * pfClusterHistMinPeakPercentage, - uint32_t * pnWaveBasisSize, - uint32_t * pnWaveSampleSize, - uint32_t nInstance = 0); - -cbRESULT cbSSSetStatistics(uint32_t nUpdateSpikes, uint32_t nAutoalg, uint32_t nMode, - float fMinClusterPairSpreadFactor, - float fMaxSubclusterSpreadFactor, - float fMinClusterHistCorrMajMeasure, - float fMaxClusterPairHistCorrMajMeasure, - float fClusterHistValleyPercentage, - float fClusterHistClosePeakPercentage, - float fClusterHistMinPeakPercentage, - uint32_t nWaveBasisSize, - uint32_t nWaveSampleSize, - uint32_t nInstance = 0); - - -// Spike sorting artifact rejecting -cbRESULT cbSSGetArtifactReject(uint32_t * pnMaxChans, uint32_t * pnRefractorySamples, uint32_t nInstance = 0); -cbRESULT cbSSSetArtifactReject(uint32_t nMaxChans, uint32_t nRefractorySamples, uint32_t nInstance = 0); - -// Spike detection parameters -cbRESULT cbSSGetDetect(float * pfThreshold, float * pfScaling, uint32_t nInstance = 0); -cbRESULT cbSSSetDetect(float fThreshold, float fScaling, uint32_t nInstance = 0); - -// Getting and setting spike sorting status parameters -cbRESULT cbSSGetStatus(cbAdaptControl * pcntlUnitStats, cbAdaptControl * pcntlNumUnits, uint32_t nInstance = 0); -cbRESULT cbSSSetStatus(cbAdaptControl cntlUnitStats, cbAdaptControl cntlNumUnits, uint32_t nInstance = 0); - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Data Packet Structures (chid<0x8000) -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -cbRESULT cbGetNplay(char *fname, float *speed, uint32_t *flags, PROCTIME *ftime, PROCTIME *stime, PROCTIME *etime, PROCTIME * filever, uint32_t nInstance = 0); -cbRESULT cbSetNplay(const char *fname, float speed, uint32_t mode, PROCTIME val, PROCTIME stime, PROCTIME etime, uint32_t nInstance = 0); -// Get/Set the nPlay parameters. -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetSpikeLength(uint32_t *length, uint32_t *pretrig, uint32_t * pSysfreq, uint32_t nInstance = 0); -cbRESULT cbSetSpikeLength(uint32_t length, uint32_t pretrig, uint32_t nInstance = 0); -// Get/Set the system-wide spike length. Lengths should be specified in multiples of 2 and -// within the range of 16 to 128 samples long. -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_INVALIDFUNCTION if invalid flag combinations are passed. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetSystemRunLevel(uint32_t *runlevel, uint32_t *runflags, uint32_t *resetque, uint32_t nInstance = 0); -cbRESULT cbSetSystemRunLevel(uint32_t runlevel, uint32_t runflags, uint32_t resetque, uint8_t nInstrument = 0, uint32_t nInstance = 0); -// Get Set the System Condition -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized - -cbRESULT cbSetComment(uint8_t charset, uint32_t rgba, PROCTIME time, const char* comment, uint32_t nInstance = 0); -// Set one comment event. -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetProcInfo(uint32_t proc, cbPROCINFO *procinfo, uint32_t nInstance = 0); -// Retreives information for the Signal Processor module located at procid -// The function requires an allocated but uninitialized cbPROCINFO structure. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDADDRESS if no hardware at the specified Proc and Bank address -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetChanCount(uint32_t *count, uint32_t nInstance = 0); -// Retreives the total number of channels in the system -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetBankInfo(uint32_t proc, uint32_t bank, cbBANKINFO *bankinfo, uint32_t nInstance = 0); -// Retreives information for the Signal bank located at bankaddr on Proc procaddr. -// The function requires an allocated but uninitialized cbBANKINFO structure. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDADDRESS if no hardware at the specified Proc and Bank address -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetFilterDesc(uint32_t proc, uint32_t filt, cbFILTDESC *filtdesc, uint32_t nInstance = 0); -// Retreives the user filter definitions from a specific processor -// filter = 1 to cbNFILTS, 0 is reserved for the null filter case -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDADDRESS if no hardware at the specified Proc and Bank address -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -// Tell me about the current adaptive filter settings -cbRESULT cbGetAdaptFilter(uint32_t proc, // which NSP processor? - uint32_t * pnMode, // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - float * pdLearningRate, // speed at which adaptation happens. Very small. e.g. 5e-12 - uint32_t * pnRefChan1, // The first reference channel (1 based). - uint32_t * pnRefChan2, // The second reference channel (1 based). - uint32_t nInstance = 0); - - -// Update the adaptive filter settings -cbRESULT cbSetAdaptFilter(uint32_t proc, // which NSP processor? - const uint32_t * pnMode, // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - const float * pdLearningRate, // speed at which adaptation happens. Very small. e.g. 5e-12 - const uint32_t * pnRefChan1, // The first reference channel (1 based). - const uint32_t * pnRefChan2, // The second reference channel (1 based). - uint32_t nInstance = 0); - -// Useful for creating cbPKT_ADAPTFILTINFO packets -struct PktAdaptFiltInfo : public cbPKT_ADAPTFILTINFO -{ - PktAdaptFiltInfo(const uint32_t nMode, const float dLearningRate, const uint32_t nRefChan1, const uint32_t nRefChan2) : cbPKT_ADAPTFILTINFO() - { - this->cbpkt_header.chid = 0x8000; - this->cbpkt_header.type = cbPKTTYPE_ADAPTFILTSET; - this->cbpkt_header.dlen = cbPKTDLEN_ADAPTFILTINFO; - - this->nMode = nMode; - this->dLearningRate = dLearningRate; - this->nRefChan1 = nRefChan1; - this->nRefChan2 = nRefChan2; - }; -}; - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Reference Electrode filtering -// Tell me about the current reference electrode filter settings -cbRESULT cbGetRefElecFilter(uint32_t proc, // which NSP processor? - uint32_t * pnMode, // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - uint32_t * pnRefChan, // The reference channel (1 based). - uint32_t nInstance = 0); - - -// Update the reference electrode filter settings -cbRESULT cbSetRefElecFilter(uint32_t proc, // which NSP processor? - const uint32_t * pnMode, // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - const uint32_t * pnRefChan, // The reference channel (1 based). - uint32_t nInstance = 0); - -// NTrode Information Packets -cbRESULT cbGetNTrodeInfo( uint32_t ntrode, char *label, cbMANUALUNITMAPPING ellipses[][cbMAXUNITS], uint16_t * nSite, uint16_t * chans, uint16_t * fs, uint32_t nInstance = 0); -cbRESULT cbSetNTrodeInfo( uint32_t ntrode, const char *label, cbMANUALUNITMAPPING ellipses[][cbMAXUNITS], uint16_t fs, uint32_t nInstance = 0); - -// Sample Group (GROUP) Information Packets -cbRESULT cbGetSampleGroupInfo(uint32_t proc, uint32_t group, char *label, uint32_t *period, uint32_t *length, uint32_t nInstance = 0); -cbRESULT cbGetSampleGroupList(uint32_t proc, uint32_t group, uint32_t *length, uint16_t *list, uint32_t nInstance = 0); -cbRESULT cbSetSampleGroupOptions(uint32_t proc, uint32_t group, uint32_t period, char *label, uint32_t nInstance = 0); -// Retreives the Sample Group information in a processor and their definitions -// Labels are 16-characters maximum. -// -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_INVALIDADDRESS if no hardware at the specified Proc and Bank address -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetChanInfo(uint32_t chan, cbPKT_CHANINFO *pChanInfo, uint32_t nInstance = 0); -// Get the full channel config. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. -cbRESULT cbGetChanAmplitudeReject(uint32_t chan, cbAMPLITUDEREJECT *AmplitudeReject, uint32_t nInstance = 0); -cbRESULT cbSetChanAmplitudeReject(uint32_t chan, cbAMPLITUDEREJECT AmplitudeReject, uint32_t nInstance = 0); -// Get and Set the user-assigned amplitude reject values. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetChanAutoThreshold(uint32_t chan, uint32_t *bEnabled, uint32_t nInstance = 0); -cbRESULT cbSetChanAutoThreshold(uint32_t chan, uint32_t bEnabled, uint32_t nInstance = 0); -// Get and Set the user-assigned auto threshold option -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetChanUnitMapping( uint32_t chan, cbMANUALUNITMAPPING *unitmapping, uint32_t nInstance = 0); -cbRESULT cbSetChanUnitMapping( uint32_t chan, const cbMANUALUNITMAPPING *unitmapping, uint32_t nInstance = 0); -// Get and Set the user-assigned unit override for the channel. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetChanLoc(uint32_t chan, uint32_t *proc, uint32_t *bank, char *banklabel, uint32_t *term, uint32_t nInstance = 0); -// Gives the physical processor number, bank label, and terminal number of the specified channel -// by reading the configuration data in the Central App Cache. Bank Labels are the name of the -// bank that is written on the instrument, and they are null-terminated, up to 16 char long. -// -// Returns: cbRESULT_OK if all is ok -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -// flags for user flags...no effect on the cerebus -//#define cbUSER_DISABLED 0x00000001 // Channel should be electrically disabled -//#define cbUSER_EXPERIMENT 0x00000100 // Channel used for experiment environment information -//#define cbUSER_NEURAL 0x00000200 // Channel connected to neural electrode or signal - -cbRESULT cbGetChanLabel(uint32_t chan, char *label, uint32_t *userflags, int32_t *position, uint32_t nInstance = 0); -cbRESULT cbSetChanLabel(uint32_t chan, const char *label, uint32_t userflags, const int32_t *position, uint32_t nInstance = 0); -// Get and Set the user-assigned label for the channel. Channel Names may be up to 16 chars long -// and should be null terminated if shorter. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetChanNTrodeGroup(uint32_t chan, uint32_t *NTrodeGroup, uint32_t nInstance = 0); -cbRESULT cbSetChanNTrodeGroup(uint32_t chan, uint32_t NTrodeGroup, uint32_t nInstance = 0); -// Get and Set the user-assigned label for the N-Trode. N-Trode Names may be up to 16 chars long -// and should be null terminated if shorter. -// -// Returns: cbRESULT_OK if data successfully retreived or packet successfully queued to be sent. -// cbRESULT_INVALIDCHANNEL if the specified channel is not mapped or does not exist. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -///////////////////////////////////////////////////////////////////////////////// -// These are part of the "reflected" mechanism. They go out as type 0xE? and come -// Back in as type 0x6? - -#define cbPKTTYPE_MASKED_REFLECTED 0xE0 -#define cbPKTTYPE_COMPARE_MASK_REFLECTED 0xF0 -#define cbPKTTYPE_REFLECTED_CONVERSION_MASK 0x7F - - -cbRESULT cbGetChannelSelection(cbPKT_UNIT_SELECTION* pPktUnitSel, uint32_t nProc, uint32_t nInstance = 0); -// Retreives the channel unit selection status -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -cbRESULT cbGetFileInfo(cbPKT_FILECFG * filecfg, uint32_t nInstance = 0); -// Retreives the file recordign status -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -//----------------------------------------------------- -///// Packets to tell me about the spike sorting model -/// Sets the noise boundary parameters -// The information obtained by these functions is implicit within the structure data, -// but they are provided for convenience -void GetAxisLengths(const cbPKT_SS_NOISE_BOUNDARY *pPkt, float afAxisLen[3]); -void GetRotationAngles(const cbPKT_SS_NOISE_BOUNDARY *pPkt, float afTheta[3]); -void InitPktSSNoiseBoundary(cbPKT_SS_NOISE_BOUNDARY* pPkt, uint32_t chan, float cen1, float cen2, float cen3, float maj1, float maj2, float maj3, - float min11, float min12, float min13, float min21, float min22, float min23); - -/// Send this packet to the NSP to tell it to reset all spike sorting to default values -cbRESULT cbSetSSReset(uint32_t nInstance = 0); -// Restart spike sorting (applies to histogram peak count). -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - -// This packet contains the status of the automatic spike sorting. -// -/// Send this packet to the NSP to tell it to re calculate all PCA Basis Vectors and Values -cbRESULT cbSetSSRecalc(uint8_t proc, uint32_t chan, uint32_t mode, uint32_t nInstance = 0); -// Recalc spike sorting (applies to PCA based algorithms). -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetLncParameters(uint32_t nProc, uint32_t* nLncFreq, uint32_t* nLncRefChan, uint32_t* nLncGMode, uint32_t nInstance = 0); -cbRESULT cbSetLncParameters(uint32_t nProc, uint32_t nLncFreq, uint32_t nLncRefChan, uint32_t nLncGMode, uint32_t nInstance = 0); -// Get/Set the system-wide LNC parameters. -// -// Returns: cbRESULT_OK if data successfully retrieved. -// cbRESULT_NOLIBRARY if the library was not properly initialized. - - -cbRESULT cbGetAoutWaveform(uint32_t channel, uint8_t trigNum, uint16_t * mode, uint32_t * repeats, uint16_t * trig, - uint16_t * trigChan, uint16_t * trigValue, cbWaveformData * wave, uint32_t nInstance = 0); -// Returns anallog output waveform information -// Returns cbRESULT_OK if successful, cbRESULT_NOLIBRARY if library was never initialized. - -/// @author Hyrum L. Sessions -/// @date 1 Aug 2019 -/// @brief Get the waveform number for a specific aout channel -/// -/// since channels are not contiguous, we can't just subtract the number of analog channels to -/// get the number. -/// -/// @param [in] channel - 1-based channel number -/// @param [out] wavenum - -/// @param [in] nInstance - Instance number of the library -cbRESULT cbGetAoutWaveformNumber(uint32_t channel, uint32_t* wavenum, uint32_t nInstance = 0); - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Shared Memory Definitions used by Central App and Cerebus library functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -typedef struct { - COLORREF winrsvd[48]; - COLORREF dispback; - COLORREF dispgridmaj; - COLORREF dispgridmin; - COLORREF disptext; - COLORREF dispwave; - COLORREF dispwavewarn; - COLORREF dispwaveclip; - COLORREF dispthresh; - COLORREF dispmultunit; - COLORREF dispunit[16]; // 0 = unclassified - COLORREF dispnoise; - COLORREF dispchansel[3]; - COLORREF disptemp[5]; - COLORREF disprsvd[14]; -} cbCOLORTABLE; - -cbRESULT cbGetColorTable(cbCOLORTABLE **colortable, uint32_t nInstance = 0); - - -typedef struct { - float fRMSAutoThresholdDistance; // multiplier to use for autothresholding when using - // RMS to guess noise - uint32_t reserved[31]; -} cbOPTIONTABLE; - - -// Get/Set the multiplier to use for autothresholdine when using RMS to guess noise -// This will adjust fAutoThresholdDistance above, but use the API instead -float cbGetRMSAutoThresholdDistance(uint32_t nInstance = 0); -void cbSetRMSAutoThresholdDistance(float fRMSAutoThresholdDistance, uint32_t nInstance = 0); - -////////////////////////////////////////////////////////////////////////////////////////////////// - - -#define cbPKT_SPKCACHEPKTCNT 400 -#define cbPKT_SPKCACHELINECNT cbNUM_ANALOG_CHANS - -typedef struct { - uint32_t chid; // ID of the Channel - uint32_t pktcnt; // # of packets which can be saved - uint32_t pktsize; // Size of an individual packet - uint32_t head; // Where (0 based index) in the circular buffer to place the NEXT packet. - uint32_t valid; // How many packets have come in since the last configuration - cbPKT_SPK spkpkt[cbPKT_SPKCACHEPKTCNT]; // Circular buffer of the cached spikes -} cbSPKCACHE; - -typedef struct { - uint32_t flags; - uint32_t chidmax; - uint32_t linesize; - uint32_t spkcount; - cbSPKCACHE cache[cbPKT_SPKCACHELINECNT]; -} cbSPKBUFF; - -cbRESULT cbGetSpkCache(uint32_t chid, cbSPKCACHE **cache, uint32_t nInstance = 0); - -#ifdef WIN32 -enum WM_USER_GLOBAL -{ - WM_USER_WAITEVENT = WM_USER, // mmtimer says it is OK to continue - WM_USER_CRITICAL_DATA_CATCHUP, // We have reached a critical data point and we have skipped -}; -#endif - - -#define cbRECBUFFLEN (cbNUM_FE_CHANS * 32768 * 4) -typedef struct { - uint32_t received; - PROCTIME lasttime; - uint32_t headwrap; - uint32_t headindex; - uint32_t buffer[cbRECBUFFLEN]; -} cbRECBUFF; - -#ifdef _MSC_VER -// The following structure is used to hold Cerebus packets queued for transmission to the NSP. -// The length of the structure is set during initialization of the buffer in the Central App. -// The pragmas allow a zero-length data field entry in the structure for referencing the data. -#pragma warning(push) -#pragma warning(disable:4200) -#endif - -typedef struct { - uint32_t transmitted; // How many packets have we sent out? - - uint32_t headindex; // 1st empty position - // (moves on filling) - - uint32_t tailindex; // 1 past last emptied position (empty when head = tail) - // Moves on emptying - - uint32_t last_valid_index;// index number of greatest valid starting index for a head (or tail) - uint32_t bufferlen; // number of indexes in buffer (units of uint32_t) <------+ - uint32_t buffer[0]; // big buffer of data...there are actually "bufferlen"--+ indices -} cbXMTBUFF; - -#define cbXMT_GLOBAL_BUFFLEN ((cbCER_UDP_SIZE_MAX / 4) * 5000 + 2) // room for 500 packets -#define cbXMT_LOCAL_BUFFLEN ((cbCER_UDP_SIZE_MAX / 4) * 2000 + 2) // room for 200 packets - - -#ifdef _MSC_VER -#pragma warning(pop) -#endif - -#define WM_USER_SET_THOLD_SIGMA (WM_USER + 100) -#define WM_USER_SET_THOLD_TIME (WM_USER + 101) - -typedef struct { - // ***** THESE MUST BE 1ST IN THE STRUCTURE WITH MODELSET LAST OF THESE *** - // ***** SEE WriteCCFNoPrompt() *** - cbPKT_FS_BASIS asBasis[cbMAXCHANS]; // All the PCA basis values - cbPKT_SS_MODELSET asSortModel[cbMAXCHANS][cbMAXUNITS + 2]; // All the model (rules) for spike sorting - - //////// These are spike sorting options - cbPKT_SS_DETECT pktDetect; // parameters dealing with actual detection - cbPKT_SS_ARTIF_REJECT pktArtifReject; // artifact rejection - cbPKT_SS_NOISE_BOUNDARY pktNoiseBoundary[cbMAXCHANS]; // where o'where are the noise boundaries - cbPKT_SS_STATISTICS pktStatistics; // information about statistics - cbPKT_SS_STATUS pktStatus; // Spike sorting status - -} cbSPIKE_SORTING; - -#define PCSTAT_TYPE_CERVELLO 0x00000001 // Cervello type system -#define PCSTAT_DISABLE_RAW 0x00000002 // Disable recording of raw data - -enum NSP_STATUS { NSP_INIT, NSP_NOIPADDR, NSP_NOREPLY, NSP_FOUND, NSP_INVALID }; - -class cbPcStatus -{ -public: - cbPKT_UNIT_SELECTION isSelection[cbMAXPROCS]{}; - -private: - int32_t m_iBlockRecording; - uint32_t m_nPCStatusFlags; - uint32_t m_nNumFEChans; // number of each type of channels received from the instrument - uint32_t m_nNumAnainChans; - uint32_t m_nNumAnalogChans; - uint32_t m_nNumAoutChans; - uint32_t m_nNumAudioChans; - uint32_t m_nNumAnalogoutChans; - uint32_t m_nNumDiginChans; - uint32_t m_nNumSerialChans; - uint32_t m_nNumDigoutChans; - uint32_t m_nNumTotalChans; -#ifndef CBPROTO_311 - NSP_STATUS m_nNspStatus[cbMAXPROCS]{}; // true if the nsp has received a sysinfo from each NSP - uint32_t m_nNumNTrodesPerInstrument[cbMAXPROCS]{}; - uint32_t m_nGeminiSystem; // Used as boolean true if connected to a gemini system -#endif - -public: - cbPcStatus() : - m_iBlockRecording(0), - m_nPCStatusFlags(0), - m_nNumFEChans(0), - m_nNumAnainChans(0), - m_nNumAnalogChans(0), - m_nNumAoutChans(0), - m_nNumAudioChans(0), - m_nNumAnalogoutChans(0), - m_nNumDiginChans(0), - m_nNumSerialChans(0), - m_nNumDigoutChans(0), - m_nNumTotalChans(0) -#ifndef CBPROTO_311 - , m_nGeminiSystem(0) -#endif - { - for (uint32_t nProc = 0; nProc < cbMAXPROCS; ++nProc) - { - isSelection[nProc].lastchan = 1; -#ifndef CBPROTO_311 - m_nNspStatus[nProc] = NSP_INIT; - m_nNumNTrodesPerInstrument[nProc] = cbMAXNTRODES; -#endif - } - } - [[nodiscard]] bool IsRecordingBlocked() const { return m_iBlockRecording != 0; } - [[nodiscard]] uint32_t cbGetPCStatusFlags() const { return m_nPCStatusFlags; } - [[nodiscard]] uint32_t cbGetNumFEChans() const { return m_nNumFEChans; } - [[nodiscard]] uint32_t cbGetNumAnainChans() const { return m_nNumAnainChans; } - [[nodiscard]] uint32_t cbGetNumAnalogChans() const { return m_nNumAnalogChans; } - [[nodiscard]] uint32_t cbGetNumAoutChans() const { return m_nNumAoutChans; } - [[nodiscard]] uint32_t cbGetNumAudioChans() const { return m_nNumAudioChans; } - [[nodiscard]] uint32_t cbGetNumAnalogoutChans() const { return m_nNumAnalogoutChans; } - [[nodiscard]] uint32_t cbGetNumDiginChans() const { return m_nNumDiginChans; } - [[nodiscard]] uint32_t cbGetNumSerialChans() const { return m_nNumSerialChans; } - [[nodiscard]] uint32_t cbGetNumDigoutChans() const { return m_nNumDigoutChans; } - [[nodiscard]] uint32_t cbGetNumTotalChans() const { return m_nNumTotalChans; } - -#ifndef CBPROTO_311 - [[nodiscard]] NSP_STATUS cbGetNspStatus(const uint32_t nProc) const { return m_nNspStatus[nProc]; } - [[nodiscard]] uint32_t cbGetNumNTrodesPerInstrument(const uint32_t nProc) const { return m_nNumNTrodesPerInstrument[nProc - 1]; } - void cbSetNspStatus(const uint32_t nInstrument, const NSP_STATUS nStatus) { m_nNspStatus[nInstrument] = nStatus; } - void cbSetNumNTrodesPerInstrument(const uint32_t nInstrument, const uint32_t nNumNTrodesPerInstrument) { m_nNumNTrodesPerInstrument[nInstrument - 1] = nNumNTrodesPerInstrument; } -#else - // m_NspStatus not avail in 3.11 so set to always found to pass logic checks. - NSP_STATUS cbGetNspStatus(uint32_t nProc) { return NSP_FOUND; } -#endif - void SetBlockRecording(const bool bBlockRecording) { m_iBlockRecording += bBlockRecording ? 1 : -1; } - void cbSetPCStatusFlags(const uint32_t nPCStatusFlags) { m_nPCStatusFlags = nPCStatusFlags; } - void cbSetNumFEChans(const uint32_t nNumFEChans) { m_nNumFEChans = nNumFEChans; } - void cbSetNumAnainChans(const uint32_t nNumAnainChans) { m_nNumAnainChans = nNumAnainChans; } - void cbSetNumAnalogChans(const uint32_t nNumAnalogChans) { m_nNumAnalogChans = nNumAnalogChans; } - void cbSetNumAoutChans(const uint32_t nNumAoutChans) { m_nNumAoutChans = nNumAoutChans; } - void cbSetNumAudioChans(const uint32_t nNumAudioChans) { m_nNumAudioChans = nNumAudioChans; } - void cbSetNumAnalogoutChans(const uint32_t nNumAnalogoutChans) { m_nNumAnalogoutChans = nNumAnalogoutChans; } - void cbSetNumDiginChans(const uint32_t nNumDiginChans) { m_nNumDiginChans = nNumDiginChans; } - void cbSetNumSerialChans(const uint32_t nNumSerialChans) { m_nNumSerialChans = nNumSerialChans; } - void cbSetNumDigoutChans(const uint32_t nNumDigoutChans) { m_nNumDigoutChans = nNumDigoutChans; } - void cbSetNumTotalChans(const uint32_t nNumTotalChans) { m_nNumTotalChans = nNumTotalChans; } -}; - -typedef struct { - uint32_t version; - uint32_t sysflags; - cbOPTIONTABLE optiontable; // Should be 32 32-bit values - cbCOLORTABLE colortable; // Should be 96 32-bit values - cbPKT_SYSINFO sysinfo; - cbPKT_PROCINFO procinfo[cbMAXPROCS]; - cbPKT_BANKINFO bankinfo[cbMAXPROCS][cbMAXBANKS]; - cbPKT_GROUPINFO groupinfo[cbMAXPROCS][cbMAXGROUPS]; // sample group ID (1-4=proc1, 5-8=proc2, etc) - cbPKT_FILTINFO filtinfo[cbMAXPROCS][cbMAXFILTS]; - cbPKT_ADAPTFILTINFO adaptinfo[cbMAXPROCS]; // Settings about adapting - cbPKT_REFELECFILTINFO refelecinfo[cbMAXPROCS]; // Settings about reference electrode filtering - cbPKT_CHANINFO chaninfo[cbMAXCHANS]; - cbSPIKE_SORTING isSortingOptions; // parameters dealing with spike sorting - cbPKT_NTRODEINFO isNTrodeInfo[cbMAXNTRODES]; // allow for the max number of ntrodes (if all are stereo-trodes) - cbPKT_AOUT_WAVEFORM isWaveform[AOUT_NUM_GAIN_CHANS][cbMAX_AOUT_TRIGGER]; // Waveform parameters - cbPKT_LNC isLnc[cbMAXPROCS]; //LNC parameters - cbPKT_NPLAY isNPlay; // nPlay Info - cbVIDEOSOURCE isVideoSource[cbMAXVIDEOSOURCE]; // Video source - cbTRACKOBJ isTrackObj[cbMAXTRACKOBJ]; // Trackable objects - cbPKT_FILECFG fileinfo; // File recording status - // This must be at the bottom of this structure because it is variable size 32-bit or 64-bit - // depending on the compile settings e.g. 64-bit cbmex communicating with 32-bit Central - HANDLE hwndCentral; // Handle to the Window in Central - -} cbCFGBUFF; - - - -// External Global Variables - -typedef struct cbSharedMemHandle { - HANDLE hnd = nullptr; - char name[64] = {0}; - int fd = -1; - uint32_t size = 0; -} cbSharedMemHandle; - -extern cbSharedMemHandle cb_xmt_global_buffer_hnd[cbMAXOPEN]; // Transmit queues to send out of this PC -extern cbXMTBUFF* cb_xmt_global_buffer_ptr[cbMAXOPEN]; - -extern cbSharedMemHandle cb_xmt_local_buffer_hnd[cbMAXOPEN]; // Transmit queues only for local (this PC) use -extern cbXMTBUFF* cb_xmt_local_buffer_ptr[cbMAXOPEN]; - -extern cbSharedMemHandle cb_rec_buffer_hnd[cbMAXOPEN]; -extern cbRECBUFF* cb_rec_buffer_ptr[cbMAXOPEN]; -extern cbSharedMemHandle cb_cfg_buffer_hnd[cbMAXOPEN]; -extern cbCFGBUFF* cb_cfg_buffer_ptr[cbMAXOPEN]; -extern cbSharedMemHandle cb_pc_status_buffer_hnd[cbMAXOPEN]; -extern cbPcStatus* cb_pc_status_buffer_ptr[cbMAXOPEN]; // parameters dealing with local pc status -extern cbSharedMemHandle cb_spk_buffer_hnd[cbMAXOPEN]; -extern cbSPKBUFF* cb_spk_buffer_ptr[cbMAXOPEN]; -extern HANDLE cb_sig_event_hnd[cbMAXOPEN]; - -extern uint32_t cb_library_initialized[cbMAXOPEN]; -extern uint32_t cb_library_index[cbMAXOPEN]; - -#pragma pack(pop) - -#endif // end of include guard \ No newline at end of file diff --git a/include/cerelink/cbproto.h b/include/cerelink/cbproto.h deleted file mode 100755 index 84737001..00000000 --- a/include/cerelink/cbproto.h +++ /dev/null @@ -1,2130 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////////////////////////// -/// -/// @file cbProto.h -/// @attention (c) Copyright 2002 - 2008 Cyberkinetics, Inc. All rights reserved. -/// @attention (c) Copyright 2008 - 2023 Blackrock Microsystems LLC. All rights reserved. -/// -/// @author Kirk Korver -/// @date 2002 -/// -/// @brief Definition of the Neuromatic library protocols. -/// -/// This code library defines a standardized control and data access interface for microelectrode -/// neurophysiology equipment. The interface allows several applications to simultaneously access -/// the control and data stream for the equipment through a central control application. This -/// central application governs the flow of information to the user interface applications and -/// performs hardware specific data processing for the instruments. This is diagrammed as follows: -/// -/// Instruments <---> Central Control App <--+--> Cerebus Library <---> User Application -/// +--> Cerebus Library <---> User Application -/// +--> Cerebus Library <---> User Application -/// -/// The Central Control Application can also exchange window/application configuration data so that -/// the Central Application can save and restore instrument and application window settings. -/// -/// All hardware configuration, hardware acknowledgement, and data information are passed on the -/// system in packet form. Cerebus user applications interact with the hardware in the system by -/// sending and receiving configuration and data packets through the Central Control Application. -/// In order to aid efficiency, the Central Control App caches information regarding hardware -/// configuration so that multiple applications do not need to request hardware configuration -/// packets from the system. The Neuromatic Library provides high-level functions for retrieving -/// data from this cache and high-level functions for transmitting configuration packets to the -/// hardware. Neuromatic applications must provide a callback function for receiving data and -/// configuration acknowledgement packets. -/// -/// The data stream from the hardware is composed of "neural data" to be saved in experiment files -/// and "preview data" that provides information such as compressed real-time channel data for -/// scrolling displays and Line Noise Cancellation waveforms to update the user. -/// -/// Central Startup Procedure: -/// On start, central sends cbPKT_SYSINFO with the runlevel set to cbRUNLEVEL_RUNNING -/// The NSP responds with its current runlevel (cbRUNLEVEL_STARTUP, STANDBY, or RUNNING) -/// At 0.5 seconds Central checks the returned runlevel and sets a flag if it is STARTUP for other -/// processing such as autoloading a CCF file. Then it sends a cbRUNLEVEL_HARDRESET -/// At 1.0 seconds Central sends a generic packet with the type set to cbPKTTYPE_REQCONFIGALL -/// The NSP responds with a boatload of configuration packets -/// At 2.0 seconds, Central sets runlevel to cbRUNLEVEL_RUNNING -/// The NSP will then start sending data packets and respond to configuration packets.= -/// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -// Standard include guards -#ifndef CBPROTO_H_INCLUDED -#define CBPROTO_H_INCLUDED - -// Only standard headers might be included here -#if defined(WIN32) -// It has to be in this order for right version of sockets -#ifdef NO_AFX -#include -#include -#endif -#endif // WIN32 - -#ifdef __KERNEL__ -#include -#else -#include -#endif - -#pragma pack(push, 1) - -#ifdef CBPROTO_311 -#define cbVERSION_MAJOR 3 -#define cbVERSION_MINOR 11 -#else -#define cbVERSION_MAJOR 4 -#define cbVERSION_MINOR 1 -#endif - -// Version history: -// - 03 Feb 2023 hls - Separate protocol structures into new file cbProto.h -// 4.1 - 14 Mar 2022 hls - Update CHANINFO to be a multiple of 32-bits. Added triginst. -// 25 May 2022 hls - change type in cbPKT_HEADER to 16-bit to allow for future expansion & add counter to -// cbPKT_SYSPROTOCOLMONITOR in case some of those packets get missed. -// 4.0 - 12 Jul 2017 ahr - changing time values form 32 uint to 64 uint. creating header typedef to be included in structs. -// 3.11 - 17 Jan 2017 hls - Changed list in cbPKT_GROUPINFO from uint32_t to uint16_t to allow for 256-channels -// 3.10 - 23 Oct 2014 hls - Added reboot capability for extension loader -// 26 Jul 2014 js - Added analog output to extension -// 18 Mar 2014 sa - Add triggered digital output -// 3 Mar 2013 eaz - Adjust MTU -// 3.9 - 23 Jan 2012 eaz - Expanded Analogout Waveform packet -// 29 Apr 2012 eaz - Added cross-platform glue -// 06 Nov 2012 eaz - Added multiple library instance handling -// 3.8 - 10 Oct 2011 hls - added map info packet -// 15 Apr 2011 tkr - added poll packet -// 13 Apr 2011 tkr - added initialize auto impedance packet -// 04 Apr 2011 tkr - added impedance data packet -// 30 Mar 2011 tkr - added patient info for file recording -// 27 Aug 2010 rpa - Added NTrode ellipse support for NTrode sorting -// 3.7 - 04 May 2010 eaz - Added comment, video synchronization and NeuroMotive configuration packets -// 25 May 2010 eaz - Added tracking, logging and patient trigger configuration packets -// 03 Jul 2010 eaz - Added NeuroMotive status and file config reporting -// 03 Jul 2010 eaz - Added nPlay stepping -// 04 Jul 2010 eaz - Added cbOpen under custom stand-alone application -// 3.6 - 10 Nov 2008 jrs - Added extra space for 3D boundaries & Packet for -// recalculating PCA Basis Vectors and Values -// 10 Apr 2009 eaz - Added PCA basis calculation info -// 12 May 2009 eaz - Added LNC parameters and type (nominal frequency, lock channel and method) -// 20 May 2009 eaz - Added LNC waveform display scaling -// 20 May 2009 hls - Added software reference electrode per channel -// 21 May 2009 eaz - Added Auto/Manual threshold packet -// 14 Jun 2009 eaz - Added Waveform Collection Matrix info -// 23 Jun 2009 eaz - Added Video Synch packet -// 25 Jun 2009 eaz - Added stand-alone application capability to library -// 20 Oct 2009 eaz - Expanded the file config packet to control and monitor recording -// 02 Nov 2009 eaz - Expanded nPlay packet to control and monitor nPlay -// 3.5 - 1 Aug 2007 hls - Added env min/max for threshold preview -// 3.4 - 12 Jun 2007 mko - Change Spike Sort Override Circles to Ellipses -// 31 May 2007 hls - Added cbPKTTYPE_REFELECFILTSET -// 19 Jun 2007 hls - Added cbPKT_SS_RESET_MODEL -// 3.3 - 27 Mar 2007 mko - Include angle of rotation with Noise Boundary variables -// 3.2 - 7 Dec 2006 mko - Make spike width variable - to max length of 128 samples -// 20 Dec 2006 hls - move wave to end of spike packet -// 3.1 - 25 Sep 2006 hls - Added unit mapping functionality -// 17 Sep 2006 djs - Changed from noise line to noise ellipse and renamed -// variables with NOISE_LINE to NOISE_BOUNDARY for -// better generalization context -// 15 Aug 2006 djs - Changed exponential measure to histogram correlation -// measure and added histogram peak count algorithm -// 21 Jul 2006 djs/hls - Added exponential measure autoalg -// djs/hls - Changed exponential measure to histogram correlation -// measure and added histogram peak count algorithm -// mko - Added protocol checking to procinfo system -// 3.0 - 15 May 2006 hls - Merged the Manual & Auto Sort protocols -// 2.5 - 18 Nov 2005 hls - Added cbPKT_SS_STATUS -// 2.4 - 10 Nov 2005 kfk - Added cbPKT_SET_DOUT -// 2.3 - 02 Nov 2005 kfk - Added cbPKT_SS_RESET -// 2.2 - 27 Oct 2005 kfk - Updated cbPKT_SS_STATISTICS to include params to affect -// spike sorting rates -// -// 2.1 - 22 Jun 2005 kfk - added all packets associated with spike sorting options -// cbPKTDLEN_SS_DETECT -// cbPKT_SS_ARTIF_REJECT -// cbPKT_SS_NOISE_LINE -// cbPKT_SS_STATISTICS -// -// 2.0 - 11 Apr 2005 kfk - Redefined the Spike packet to include classification data -// -// 1.8 - 27 Mar 2006 ab - Added cbPKT_SS_STATUS -// 1.7 - 7 Feb 2006 hls - Added anagain to cbSCALING structure -// to support different external gain -// 1.6 - 25 Feb 2005 kfk - Added cbPKTTYPE_ADAPTFILTSET and -// cbGetAdaptFilter() and cbSGetAdaptFilter() -// 1.5 - 30 Dec 2003 kfk - Added cbPKTTYPE_REPCONFIGALL and cbPKTTYPE_PREVREP -// redefined cbPKTTYPE_REQCONFIGALL -// 1.4 - 15 Dec 2003 kfk - Added "last_valid_index" to the send buffer -// Added cbDOUT_TRACK -// - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Fixed storage size definitions for declared variables -// (includes conditional testing so that there is no clash with win32 headers) -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#define MAX_UINT16 0xFFFF - -#ifndef WIN32 -typedef const char * LPCSTR; -typedef void * HANDLE; -typedef uint32_t COLORREF; -typedef unsigned int UINT; -#define RGB(r,g,b) ((uint8_t)r + ((uint8_t)g << 8) + ((uint8_t)b << 16)) -#define MAKELONG(a, b) ((a & 0xffff) | ((b & 0xffff) << 16)) -#define MAX_PATH 1024 -#endif - -#ifdef CBPROTO_311 -typedef uint32_t PROCTIME; -#else -typedef uint64_t PROCTIME; -#endif -typedef int16_t A2D_DATA; -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Default Cerebus networking connection parameters -// -// All connections should be defined here -// -/////////////////////////////////////////////////////////////////////////////////////////////////// -#define cbNET_UDP_ADDR_INST "192.168.137.1" // Cerebus default address -#define cbNET_UDP_ADDR_CNT "192.168.137.128" // Gemini NSP default control address -#define cbNET_UDP_ADDR_BCAST "192.168.137.255" // NSP default broadcast address -#define cbNET_UDP_PORT_BCAST 51002 // Neuroflow Data Port -#define cbNET_UDP_PORT_CNT 51001 // Neuroflow Control Port - -// maximum udp datagram size used to transport cerebus packets, taken from MTU size -#define cbCER_UDP_SIZE_MAX 58080 // Note that multiple packets may reside in one udp datagram as aggregate - -#define cbNET_TCP_PORT_GEMINI 51005 // Neuroflow Data Port -#define cbNET_TCP_ADDR_GEMINI_HUB "192.168.137.200" // NSP default control address - -#define cbNET_UDP_ADDR_HOST "192.168.137.199" // Cerebus (central) default address -#define cbNET_UDP_ADDR_GEMINI_NSP "192.168.137.128" // NSP default control address -#define cbNET_UDP_ADDR_GEMINI_HUB "192.168.137.200" // HUB default control address -#define cbNET_UDP_ADDR_GEMINI_HUB2 "192.168.137.201" // HUB default control address -#define cbNET_UDP_ADDR_BCAST "192.168.137.255" // NSP default broadcast address -#define cbNET_UDP_PORT_GEMINI_NSP 51001 // Gemini NSP Port -#define cbNET_UDP_PORT_GEMINI_HUB 51002 // Gemini HUB Port -#define cbNET_UDP_PORT_GEMINI_HUB2 51003 // Gemini HUB Port - -#define PROTOCOL_UDP 0 -#define PROTOCOL_TCP 1 - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Maximum entity ranges for static declarations using this version of the library -// -/////////////////////////////////////////////////////////////////////////////////////////////////// -#define cbNSP1 1 - -/////////////////////////////////////////////////////////// -#define cbRAWGROUP 6 // group number for raw data feed -/////////////////////////////////////////////////////////// - -// Front End Channels (128) -> (256) -// Analog Input (16) -> (32) -// Analog Output (4) -> (8) -// Audio Output (2) -> (4) -// Digital Output (4) -> (8) -// Digital Input (1) -> (2) -// Serial Input (1) -> (2) -//--------------------------------------- -// Total (actually 156) (160) -> (320) -// -#define cbMAXOPEN 4 // Maximum number of open cbhwlib's (nsp's) -#if defined(__cplusplus) && !defined(CBPROTO_311) -// Client-side defs -#define cbMAXPROCS 3 // Number of NSPs for client -#define cbNUM_FE_CHANS 512 // Front end channels for client -#else -// If we were to reuse cbProto in a (simulated device)... -#define cbMAXPROCS 1 // Number of NSPs for the embedded software -#define cbNUM_FE_CHANS 256 // Front end channels for the NSP -#endif -#define cbMAXGROUPS 8 // number of sample rate groups -#define cbMAXFILTS 32 -#define cbMAXVIDEOSOURCE 1 // maximum number of video sources -#define cbMAXTRACKOBJ 20 // maximum number of trackable objects -#define cbMAXHOOPS 4 -#define cbMAX_AOUT_TRIGGER 5 // maximum number of per-channel (analog output, or digital output) triggers - -// N-Trode definitions -#define cbMAXSITES 4 //*** maximum number of electrodes that can be included in an n-trode group -- eventually want to support octrodes -#define cbMAXSITEPLOTS ((cbMAXSITES - 1) * cbMAXSITES / 2) // combination of 2 out of n is n!/((n-2)!2!) -- the only issue is the display - -// Channel Definitions -#define cbNUM_ANAIN_CHANS (16 * cbMAXPROCS) // #Analog Input channels -#define cbNUM_ANALOG_CHANS (cbNUM_FE_CHANS + cbNUM_ANAIN_CHANS) // Total Analog Inputs - -// Fixed size for structures shared across NSP devices and Central -// This matches the NSP device's cbNUM_ANALOG_CHANS (256 FE + 16 ANAIN = 272) -// Must be used for variable-length arrays in packet structures to ensure consistent shared memory layout -#define cbNUM_ANALOG_CHANS_STRUCT 272 -#define cbNUM_ANAOUT_CHANS (4 * cbMAXPROCS) // #Analog Output channels -#define cbNUM_AUDOUT_CHANS (2 * cbMAXPROCS) // #Audio Output channels -#define cbNUM_ANALOGOUT_CHANS (cbNUM_ANAOUT_CHANS + cbNUM_AUDOUT_CHANS) // Total Analog Output -#define cbNUM_DIGIN_CHANS (1 * cbMAXPROCS) // #Digital Input channels -#define cbNUM_SERIAL_CHANS (1 * cbMAXPROCS) // #Serial Input channels -#define cbNUM_DIGOUT_CHANS (4 * cbMAXPROCS) // #Digital Output channels - -// Total of all channels = 156 -#define cbMAXCHANS (cbNUM_ANALOG_CHANS + cbNUM_ANALOGOUT_CHANS + cbNUM_DIGIN_CHANS + cbNUM_SERIAL_CHANS + cbNUM_DIGOUT_CHANS) - -#define cbFIRST_FE_CHAN 0 // 0 First Front end channel - -// Bank definitions - NOTE: If any of the channel types have more than cbCHAN_PER_BANK channels, the banks must be increased accordingly -#define cbCHAN_PER_BANK 32 // number of 32 channel banks == 1024 -#define cbNUM_FE_BANKS (cbNUM_FE_CHANS / cbCHAN_PER_BANK) // number of Front end banks -#define cbNUM_ANAIN_BANKS 1 // number of Analog Input banks -#define cbNUM_ANAOUT_BANKS 1 // number of Analog Output banks -#define cbNUM_AUDOUT_BANKS 1 // number of Audio Output banks -#define cbNUM_DIGIN_BANKS 1 // number of Digital Input banks -#define cbNUM_SERIAL_BANKS 1 // number of Serial Input banks -#define cbNUM_DIGOUT_BANKS 1 // number of Digital Output banks - -// Custom digital filters -#define cbFIRST_DIGITAL_FILTER 13 // (0-based) filter number, must be less than cbMAXFILTS -#define cbNUM_DIGITAL_FILTERS 4 - -// This is the number of aout chans with gain. Conveniently, the -// 4 Analog Outputs and the 2 Audio Outputs are right next to each other -// in the channel numbering sequence. -#define AOUT_NUM_GAIN_CHANS (cbNUM_ANAOUT_CHANS + cbNUM_AUDOUT_CHANS) - -// Total number of banks -#define cbMAXBANKS (cbNUM_FE_BANKS + cbNUM_ANAIN_BANKS + cbNUM_ANAOUT_BANKS + cbNUM_AUDOUT_BANKS + cbNUM_DIGIN_BANKS + cbNUM_SERIAL_BANKS + cbNUM_DIGOUT_BANKS) - -#define cbMAXUNITS 5 // hard coded to 5 in some places -#define cbMAXNTRODES (cbNUM_FE_CHANS / 2) // minimum is stereotrode so max n-trodes is max front-end chans / 2 - -#define SCALE_LNC_COUNT 17 -#define SCALE_CONTINUOUS_COUNT 17 -#define SCALE_SPIKE_COUNT 23 - -// Special unit classification values -enum UnitClassification -{ - UC_UNIT_UNCLASSIFIED = 0, // This unit is not classified - UC_UNIT_ANY = 254, // Any unit including noise - UC_UNIT_NOISE = 255, // This unit is really noise -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Some of the string length constants -#define cbLEN_STR_UNIT 8 -#define cbLEN_STR_LABEL 16 -#define cbLEN_STR_FILT_LABEL 16 -#define cbLEN_STR_IDENT 64 -#define cbLEN_STR_COMMENT 256 -/////////////////////////////////////////////////////////////////////////////////////////////////// - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Library Result definitions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -typedef unsigned int cbRESULT; - -#define cbRESULT_OK 0 // Function executed normally -#define cbRESULT_NOLIBRARY 1 // The library was not properly initialized -#define cbRESULT_NOCENTRALAPP 2 // Unable to access the central application -#define cbRESULT_LIBINITERROR 3 // Error attempting to initialize library error -#define cbRESULT_MEMORYUNAVAIL 4 // Not enough memory available to complete the operation -#define cbRESULT_INVALIDADDRESS 5 // Invalid Processor or Bank address -#define cbRESULT_INVALIDCHANNEL 6 // Invalid channel ID passed to function -#define cbRESULT_INVALIDFUNCTION 7 // Channel exists, but requested function is not available -#define cbRESULT_NOINTERNALCHAN 8 // No internal channels available to connect hardware stream -#define cbRESULT_HARDWAREOFFLINE 9 // Hardware is offline or unavailable -#define cbRESULT_DATASTREAMING 10 // Hardware is streaming data and cannot be configured -#define cbRESULT_NONEWDATA 11 // There is no new data to be read in -#define cbRESULT_DATALOST 12 // The Central App incoming data buffer has wrapped -#define cbRESULT_INVALIDNTRODE 13 // Invalid NTrode number passed to function -#define cbRESULT_BUFRECALLOCERR 14 // Receive buffer could not be allocated -#define cbRESULT_BUFGXMTALLOCERR 15 // Global transmit buffer could not be allocated -#define cbRESULT_BUFLXMTALLOCERR 16 // Local transmit buffer could not be allocated -#define cbRESULT_BUFCFGALLOCERR 17 // Configuration buffer could not be allocated -#define cbRESULT_BUFPCSTATALLOCERR 18 // PC status buffer could not be allocated -#define cbRESULT_BUFSPKALLOCERR 19 // Spike cache buffer could not be allocated -#define cbRESULT_EVSIGERR 20 // Couldn't create shared event signal -#define cbRESULT_SOCKERR 21 // Generic socket creation error -#define cbRESULT_SOCKOPTERR 22 // Socket option error (possibly permission issue) -#define cbRESULT_SOCKMEMERR 23 // Socket memory assignment error -#define cbRESULT_INSTINVALID 24 // Invalid range or instrument address -#define cbRESULT_SOCKBIND 25 // Cannot bind to any address (possibly no Instrument network) -#define cbRESULT_SYSLOCK 26 // Cannot (un)lock the system resources (possiblly resource busy) -#define cbRESULT_INSTOUTDATED 27 // The instrument runs an outdated protocol version -#define cbRESULT_LIBOUTDATED 28 // The library is outdated - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Library Initialization Functions -// -// The standard procedure for initializing and using this library is to: -// 1) Initialize the library with cbOpen(). -// 2) Obtain system and channel configuration info with cbGet* commands. -// 3) Configure the system channels with appropriate cbSet* commands. -// 4) Receive data through the callback function -// 5) Repeat steps 2/3/4 as needed until the application closes. -// 6) call cbClose() to de-allocate and free the library -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - - -uint32_t cbVersion(); -// Returns the major/minor revision of the current library in the upper/lower uint16_t fields. - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Systemwide Inquiry and Configuration Protocols -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -/// @brief Cerebus packet header data structure -/// -/// Every packet defined in this document contains this header (must be a multiple of uint32_t) -typedef struct { - PROCTIME time; //!< system clock timestamp - uint16_t chid; //!< channel identifier -#ifdef CBPROTO_311 - uint8_t type; - uint8_t dlen; //!< length of data field in 32-bit chunks -#else - uint16_t type; //!< packet type - uint16_t dlen; //!< length of data field in 32-bit chunks - uint8_t instrument; //!< instrument number to transmit this packets - uint8_t reserved; //!< reserved for future -#endif -} cbPKT_HEADER; - -/// @brief Old Cerebus packet header data structure -/// -/// This is used to read old CCF files -typedef struct { - uint32_t time; //!< system clock timestamp - uint16_t chid; //!< channel identifier - uint8_t type; //!< packet type - uint8_t dlen; //!< length of data field in 32-bit chunks -} cbPKT_HEADER_OLD; - -//changing header size to size w/ uint64. used to be 8. -#define cbPKT_MAX_SIZE 1024 // the maximum size of the packet in bytes for 128 channels - -#define cbPKT_HEADER_SIZE sizeof(cbPKT_HEADER) // define the size of the packet header in bytes -#define cbPKT_HEADER_32SIZE (cbPKT_HEADER_SIZE / 4) // define the size of the packet header in uint32_t's - -/// @brief Generic Cerebus packet data structure (1024 bytes total) -/// -/// This packet contains the header and the data is just a series of uint32_t data points. All other packets are -/// based on this structure -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t data[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE) / 4]; //!< data buffer (up to 1016 bytes) -} cbPKT_GENERIC; - -#define cbPKT_HEADER_SIZE_OLD sizeof(cbPKT_HEADER_OLD) // define the size of the packet header in bytes - -/// @brief Old Generic Cerebus packet data structure (1024 bytes total) -/// -/// This is used to read old CCF files -typedef struct { - cbPKT_HEADER_OLD cbpkt_header; - - uint32_t data[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE_OLD) / 4]; //!< data buffer (up to 1016 bytes) -} cbPKT_GENERIC_OLD; - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Configuration/Report Packet Definitions (chid = 0x8000) -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#define cbPKTCHAN_CONFIGURATION 0x8000 // Channel # to mean configuration - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Detailed Processor and Bank Inquiry Functions -// -// Instrumentation equipment is organized into three levels within this library: -// 1) Signal Processors - Signal processing and data distribution layer -// 2) Signal Banks - Groups of channels with similar properties and physical locations -// 3) Signal Channels - Individual physical channels with one or more functions -// -// Computer --+-- Signal Processor ----- Signal Bank --+-- Channel -// | | +-- Channel -// | | +-- Channel -// | | -// | +-- Signal Bank --+-- Channel -// | +-- Channel -// | +-- Channel -// | -// +-- Signal Processor ----- Signal Bank --+-- Channel -// | +-- Channel -// | +-- Channel -// | -// +-- Signal Bank --+-- Channel -// +-- Channel -// +-- Channel -// -// In this implementation, Signal Channels are numbered from 1-32767 across the entire system and -// are associated to Signal Banks and Signal Processors by the hardware. -// -// Signal Processors are numbered 1-8 and Signal Banks are numbered from 1-16 within a specific -// Signal Processor. Processor and Bank numbers are NOT required to be continuous and -// are a function of the hardware configuration. For example, an instrumentation set-up could -// include Processors at addresses 1, 2, and 7. Configuration packets given to the computer to -// describe the hardware also report the channel enumeration. -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -/// @brief Struct - Signal Processor Configuration Structure -typedef struct { - uint32_t idcode; //!< manufacturer part and rom ID code of the Signal Processor - char ident[cbLEN_STR_IDENT]; //!< ID string with the equipment name of the Signal Processor - uint32_t chanbase; //!< The lowest channel identifier claimed by this processor - uint32_t chancount; //!< number of channel identifiers claimed by this processor - uint32_t bankcount; //!< number of signal banks supported by the processor - uint32_t groupcount; //!< number of sample groups supported by the processor - uint32_t filtcount; //!< number of digital filters supported by the processor - uint32_t sortcount; //!< number of channels supported for spike sorting (reserved for future) - uint32_t unitcount; //!< number of supported units for spike sorting (reserved for future) - uint32_t hoopcount; //!< number of supported hoops for spike sorting (reserved for future) - uint32_t reserved; //!< reserved for future use, set to 0 - uint32_t version; //!< current version of libraries -} cbPROCINFO; - -/// @brief Struct - Signal Bank Configuration Structure -typedef struct { - uint32_t idcode; //!< manufacturer part and rom ID code of the module addressed to this bank - char ident[cbLEN_STR_IDENT]; //!< ID string with the equipment name of the Signal Bank hardware module - char label[cbLEN_STR_LABEL]; //!< Label on the instrument for the signal bank, eg "Analog In" - uint32_t chanbase; //!< The lowest channel identifier claimed by this bank - uint32_t chancount; //!< number of channel identifiers claimed by this bank -} cbBANKINFO; - -/// @brief Struct - NeuroMotive video source -typedef struct { - char name[cbLEN_STR_LABEL]; //!< filename of the video file - float fps; //!< nominal record fps -} cbVIDEOSOURCE; - -#define cbTRACKOBJ_TYPE_UNDEFINED 0 -#define cbTRACKOBJ_TYPE_2DMARKERS 1 -#define cbTRACKOBJ_TYPE_2DBLOB 2 -#define cbTRACKOBJ_TYPE_3DMARKERS 3 -#define cbTRACKOBJ_TYPE_2DBOUNDARY 4 -#define cbTRACKOBJ_TYPE_1DSIZE 5 - -/// @brief Struct - Track object structure for NeuroMotive -typedef struct { - char name[cbLEN_STR_LABEL]; //!< name of the object - uint16_t type; //!< trackable type (cbTRACKOBJ_TYPE_*) - uint16_t pointCount; //!< maximum number of points -} cbTRACKOBJ; - -#define cbFILTTYPE_PHYSICAL 0x0001 -#define cbFILTTYPE_DIGITAL 0x0002 -#define cbFILTTYPE_ADAPTIVE 0x0004 -#define cbFILTTYPE_NONLINEAR 0x0008 -#define cbFILTTYPE_BUTTERWORTH 0x0100 -#define cbFILTTYPE_CHEBYCHEV 0x0200 -#define cbFILTTYPE_BESSEL 0x0400 -#define cbFILTTYPE_ELLIPTICAL 0x0800 - -/// @brief Struct - Filter description structure -/// -/// Filter description used in cbPKT_CHANINFO -typedef struct { - char label[cbLEN_STR_FILT_LABEL]; - uint32_t hpfreq; //!< high-pass corner frequency in milliHertz - uint32_t hporder; //!< high-pass filter order - uint32_t hptype; //!< high-pass filter type - uint32_t lpfreq; //!< low-pass frequency in milliHertz - uint32_t lporder; //!< low-pass filter order - uint32_t lptype; //!< low-pass filter type -} cbFILTDESC; - -/// @brief Struct - Amplitude Rejection structure -typedef struct { - uint32_t bEnabled; //!< BOOL implemented as uint32_t - for structure alignment at paragraph boundary - int16_t nAmplPos; //!< any spike that has a value above nAmplPos will be rejected - int16_t nAmplNeg; //!< any spike that has a value below nAmplNeg will be rejected -} cbAMPLITUDEREJECT; - -/// @brief Struct - Manual Unit Mapping structure -/// -/// Defines an ellipsoid for sorting. Used in cbPKT_CHANINFO and cbPKT_NTRODEINFO -typedef struct { - int16_t nOverride; //!< override to unit if in ellipsoid - int16_t afOrigin[3]; //!< ellipsoid origin - int16_t afShape[3][3]; //!< ellipsoid shape - int16_t aPhi; //!< - uint32_t bValid; //!< is this unit in use at this time? - //!< BOOL implemented as uint32_t - for structure alignment at paragraph boundary -} cbMANUALUNITMAPPING; - -#define cbCHAN_EXISTS 0x00000001 // Channel id is allocated -#define cbCHAN_CONNECTED 0x00000002 // Channel is connected and mapped and ready to use -#define cbCHAN_ISOLATED 0x00000004 // Channel is electrically isolated -#define cbCHAN_AINP 0x00000100 // Channel has analog input capabilities -#define cbCHAN_AOUT 0x00000200 // Channel has analog output capabilities -#define cbCHAN_DINP 0x00000400 // Channel has digital input capabilities -#define cbCHAN_DOUT 0x00000800 // Channel has digital output capabilities -#define cbCHAN_GYRO 0x00001000 // Channel has gyroscope/accelerometer/magnetometer/temperature capabilities - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Digital Input Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#define cbDINP_SERIALMASK 0x000000FF // Bit mask used to detect RS232 Serial Baud Rates -#define cbDINP_BAUD2400 0x00000001 // RS232 Serial Port operates at 2400 (n-8-1) -#define cbDINP_BAUD9600 0x00000002 // RS232 Serial Port operates at 9600 (n-8-1) -#define cbDINP_BAUD19200 0x00000004 // RS232 Serial Port operates at 19200 (n-8-1) -#define cbDINP_BAUD38400 0x00000008 // RS232 Serial Port operates at 38400 (n-8-1) -#define cbDINP_BAUD57600 0x00000010 // RS232 Serial Port operates at 57600 (n-8-1) -#define cbDINP_BAUD115200 0x00000020 // RS232 Serial Port operates at 115200 (n-8-1) -#define cbDINP_1BIT 0x00000100 // Port has a single input bit (eg single BNC input) -#define cbDINP_8BIT 0x00000200 // Port has 8 input bits -#define cbDINP_16BIT 0x00000400 // Port has 16 input bits -#define cbDINP_32BIT 0x00000800 // Port has 32 input bits -#define cbDINP_ANYBIT 0x00001000 // Capture the port value when any bit changes. -#define cbDINP_WRDSTRB 0x00002000 // Capture the port when a word-write line is strobed -#define cbDINP_PKTCHAR 0x00004000 // Capture packets using an End of Packet Character -#define cbDINP_PKTSTRB 0x00008000 // Capture packets using an End of Packet Logic Input -#define cbDINP_MONITOR 0x00010000 // Port controls other ports or system events -#define cbDINP_REDGE 0x00020000 // Capture the port value when any bit changes lo-2-hi (rising edge) -#define cbDINP_FEDGE 0x00040000 // Capture the port value when any bit changes hi-2-lo (falling edge) -#define cbDINP_STRBANY 0x00080000 // Capture packets using 8-bit strobe/8-bit any Input -#define cbDINP_STRBRIS 0x00100000 // Capture packets using 8-bit strobe/8-bit rising edge Input -#define cbDINP_STRBFAL 0x00200000 // Capture packets using 8-bit strobe/8-bit falling edge Input -#define cbDINP_MASK (cbDINP_ANYBIT | cbDINP_WRDSTRB | cbDINP_PKTCHAR | cbDINP_PKTSTRB | cbDINP_MONITOR | cbDINP_REDGE | cbDINP_FEDGE | cbDINP_STRBANY | cbDINP_STRBRIS | cbDINP_STRBFAL) - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Digital Output Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - - -#define cbDOUT_SERIALMASK 0x000000FF // Port operates as an RS232 Serial Connection -#define cbDOUT_BAUD2400 0x00000001 // Serial Port operates at 2400 (n-8-1) -#define cbDOUT_BAUD9600 0x00000002 // Serial Port operates at 9600 (n-8-1) -#define cbDOUT_BAUD19200 0x00000004 // Serial Port operates at 19200 (n-8-1) -#define cbDOUT_BAUD38400 0x00000008 // Serial Port operates at 38400 (n-8-1) -#define cbDOUT_BAUD57600 0x00000010 // Serial Port operates at 57600 (n-8-1) -#define cbDOUT_BAUD115200 0x00000020 // Serial Port operates at 115200 (n-8-1) -#define cbDOUT_1BIT 0x00000100 // Port has a single output bit (eg single BNC output) -#define cbDOUT_8BIT 0x00000200 // Port has 8 output bits -#define cbDOUT_16BIT 0x00000400 // Port has 16 output bits -#define cbDOUT_32BIT 0x00000800 // Port has 32 output bits -#define cbDOUT_VALUE 0x00010000 // Port can be manually configured -#define cbDOUT_TRACK 0x00020000 // Port should track the most recently selected channel -#define cbDOUT_FREQUENCY 0x00040000 // Port can output a frequency -#define cbDOUT_TRIGGERED 0x00080000 // Port can be triggered -#define cbDOUT_MONITOR_UNIT0 0x01000000 // Can monitor unit 0 = UNCLASSIFIED -#define cbDOUT_MONITOR_UNIT1 0x02000000 // Can monitor unit 1 -#define cbDOUT_MONITOR_UNIT2 0x04000000 // Can monitor unit 2 -#define cbDOUT_MONITOR_UNIT3 0x08000000 // Can monitor unit 3 -#define cbDOUT_MONITOR_UNIT4 0x10000000 // Can monitor unit 4 -#define cbDOUT_MONITOR_UNIT5 0x20000000 // Can monitor unit 5 -#define cbDOUT_MONITOR_UNIT_ALL 0x3F000000 // Can monitor ALL units -#define cbDOUT_MONITOR_SHIFT_TO_FIRST_UNIT 24 // This tells us how many bit places to get to unit 1 -// Trigger types for Digital Output channels -#define cbDOUT_TRIGGER_NONE 0 // instant software trigger -#define cbDOUT_TRIGGER_DINPRISING 1 // digital input rising edge trigger -#define cbDOUT_TRIGGER_DINPFALLING 2 // digital input falling edge trigger -#define cbDOUT_TRIGGER_SPIKEUNIT 3 // spike unit -#define cbDOUT_TRIGGER_NM 4 // comment RGBA color (A being big byte) -#define cbDOUT_TRIGGER_RECORDINGSTART 5 // recording start trigger -#define cbDOUT_TRIGGER_EXTENSION 6 // extension trigger - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Analog Input Inquiry and Configuration Functions -// -// The analog input processing in this library assumes the following signal flow structure: -// -// Input with --+-- Adaptive LNC filter -- Adaptive Filter --+-- Sampling Stream Filter -- Sample Group -// physical | | -// filter +-- Raw Preview | -// +-- Adaptive Filter -- Spike Stream Filter --+-- Spike Processing -// | | -// | +-- Spike Preview -// +-- LNC Preview -// -// Adaptive Filter (above) is one or the other depending on settings, never both! -// -// This system forks the signal into 2 separate streams: a continuous stream and a spike stream. -// All simpler systems are derived from this structure and unincluded elements are bypassed or -// omitted, for example the structure of the NSAS neural channels would be: -// -// Input with --------+-- Spike Processing ( NOTE: the physical filter is tuned ) -// physical | ( for spikes and spike processing ) -// filter +-- Spike Preview -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - - -// In this system, analog values are represented by 16-bit signed integers. The cbSCALING -// structure gives the mapping between the signal's analog space and the converted digital values. -// -// (anamax) --- -// | -// | -// | \ --- (digmax) -// analog === Analog to ===\ | -// range === Digital ===/ digital -// | / range -// | | -// | --- (digmin) -// (anamin) --- -// -// The analog range extent values are reported in 32-bit integers, along with a unit description. -// Units should be given with traditional metric scales such as P, M, K, m, u(for micro), n, p, -// etc and they are limited to 8 ASCII characters for description. -// -// The anamin and anamax represent the min and max values of the analog signal. The digmin and -// digmax values are their corresponding converted digital values. If the signal is inverted in -// the scaling conversion, the digmin value will be greater than the digmax value. -// -// For example if a +/-5V signal is mapped into a +/-1024 digital value, the preferred unit -// would be "mV", anamin/max = +/- 5000, and digmin/max= +/-1024. - -/// @brief Struct - Scaling structure -/// -/// Structure used in cbPKT_CHANINFO -typedef struct { - int16_t digmin; //!< digital value that corresponds with the anamin value - int16_t digmax; //!< digital value that corresponds with the anamax value - int32_t anamin; //!< the minimum analog value present in the signal - int32_t anamax; //!< the maximum analog value present in the signal - int32_t anagain; //!< the gain applied to the default analog values to get the analog values - char anaunit[cbLEN_STR_UNIT]; //!< the unit for the analog signal (eg, "uV" or "MPa") -} cbSCALING; - - -#define cbAINP_RAWPREVIEW 0x00000001 // Generate scrolling preview data for the raw channel -#define cbAINP_LNC 0x00000002 // Line Noise Cancellation -#define cbAINP_LNCPREVIEW 0x00000004 // Retrieve the LNC correction waveform -#define cbAINP_SMPSTREAM 0x00000010 // stream the analog input stream directly to disk -#define cbAINP_SMPFILTER 0x00000020 // Digitally filter the analog input stream -#define cbAINP_RAWSTREAM 0x00000040 // Raw data stream available -#define cbAINP_SPKSTREAM 0x00000100 // Spike Stream is available -#define cbAINP_SPKFILTER 0x00000200 // Selectable Filters -#define cbAINP_SPKPREVIEW 0x00000400 // Generate scrolling preview of the spike channel -#define cbAINP_SPKPROC 0x00000800 // Channel is able to do online spike processing -#define cbAINP_OFFSET_CORRECT_CAP 0x00001000 // Offset correction mode (0-disabled 1-enabled) - -#define cbAINP_LNC_OFF 0x00000000 // Line Noise Cancellation disabled -#define cbAINP_LNC_RUN_HARD 0x00000001 // Hardware-based LNC running and adapting according to the adaptation const -#define cbAINP_LNC_RUN_SOFT 0x00000002 // Software-based LNC running and adapting according to the adaptation const -#define cbAINP_LNC_HOLD 0x00000004 // LNC running, but not adapting -#define cbAINP_LNC_MASK 0x00000007 // Mask for LNC Flags -#define cbAINP_REFELEC_LFPSPK 0x00000010 // Apply reference electrode to LFP & Spike -#define cbAINP_REFELEC_SPK 0x00000020 // Apply reference electrode to Spikes only -#define cbAINP_REFELEC_MASK 0x00000030 // Mask for Reference Electrode flags -#define cbAINP_RAWSTREAM_ENABLED 0x00000040 // Raw data stream enabled -#define cbAINP_OFFSET_CORRECT 0x00000100 // Offset correction mode (0-disabled 1-enabled) - - -// <> -#define cbAINPPREV_LNC 0x81 -#define cbAINPPREV_STREAM 0x82 -#define cbAINPPREV_ALL 0x83 - -////////////////////////////////////////////////////////////// -// AINP Spike Stream Functions - -#define cbAINPSPK_EXTRACT 0x00000001 // Time-stamp and packet to first superthreshold peak -#define cbAINPSPK_REJART 0x00000002 // Reject around clipped signals on multiple channels -#define cbAINPSPK_REJCLIP 0x00000004 // Reject clipped signals on the channel -#define cbAINPSPK_ALIGNPK 0x00000008 // -#define cbAINPSPK_REJAMPL 0x00000010 // Reject based on amplitude -#define cbAINPSPK_THRLEVEL 0x00000100 // Analog level threshold detection -#define cbAINPSPK_THRENERGY 0x00000200 // Energy threshold detection -#define cbAINPSPK_THRAUTO 0x00000400 // Auto threshold detection -#define cbAINPSPK_SPREADSORT 0x00001000 // Enable Auto spread Sorting -#define cbAINPSPK_CORRSORT 0x00002000 // Enable Auto Histogram Correlation Sorting -#define cbAINPSPK_PEAKMAJSORT 0x00004000 // Enable Auto Histogram Peak Major Sorting -#define cbAINPSPK_PEAKFISHSORT 0x00008000 // Enable Auto Histogram Peak Fisher Sorting -#define cbAINPSPK_HOOPSORT 0x00010000 // Enable Manual Hoop Sorting -#define cbAINPSPK_PCAMANSORT 0x00020000 // Enable Manual PCA Sorting -#define cbAINPSPK_PCAKMEANSORT 0x00040000 // Enable K-means PCA Sorting -#define cbAINPSPK_PCAEMSORT 0x00080000 // Enable EM-clustering PCA Sorting -#define cbAINPSPK_PCADBSORT 0x00100000 // Enable DBSCAN PCA Sorting -#define cbAINPSPK_AUTOSORT (cbAINPSPK_SPREADSORT | cbAINPSPK_CORRSORT | cbAINPSPK_PEAKMAJSORT | cbAINPSPK_PEAKFISHSORT) // old auto sorting methods -#define cbAINPSPK_NOSORT 0x00000000 // No sorting -#define cbAINPSPK_PCAAUTOSORT (cbAINPSPK_PCAKMEANSORT | cbAINPSPK_PCAEMSORT | cbAINPSPK_PCADBSORT) // All PCA sorting auto algorithms -#define cbAINPSPK_PCASORT (cbAINPSPK_PCAMANSORT | cbAINPSPK_PCAAUTOSORT) // All PCA sorting algorithms -#define cbAINPSPK_ALLSORT (cbAINPSPK_AUTOSORT | cbAINPSPK_HOOPSORT | cbAINPSPK_PCASORT) // All sorting algorithms - -/// @brief Struct - Hoop definition structure -/// -/// Defines the hoop used for sorting. There can be up to 5 hoops per unit. Used in cbPKT_CHANINFO -typedef struct { - uint16_t valid; //!< 0=undefined, 1 for valid - int16_t time; //!< time offset into spike window - int16_t min; //!< minimum value for the hoop window - int16_t max; //!< maximum value for the hoop window -} cbHOOP; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Analog Output Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - - -#define cbAOUT_AUDIO 0x00000001 // Channel is physically optimized for audio output -#define cbAOUT_SCALE 0x00000002 // Output a static value -#define cbAOUT_TRACK 0x00000004 // Output a static value -#define cbAOUT_STATIC 0x00000008 // Output a static value -#define cbAOUT_MONITORRAW 0x00000010 // Monitor an analog signal line - RAW data -#define cbAOUT_MONITORLNC 0x00000020 // Monitor an analog signal line - Line Noise Cancelation -#define cbAOUT_MONITORSMP 0x00000040 // Monitor an analog signal line - Continuous -#define cbAOUT_MONITORSPK 0x00000080 // Monitor an analog signal line - spike -#define cbAOUT_STIMULATE 0x00000100 // Stimulation waveform functions are available. -#define cbAOUT_WAVEFORM 0x00000200 // Custom Waveform -#define cbAOUT_EXTENSION 0x00000400 // Output Waveform from Extension - -// To control and keep track of how long an element of spike sorting has been adapting. -// -enum ADAPT_TYPE { ADAPT_NEVER, ADAPT_ALWAYS, ADAPT_TIMED }; - -/// @brief Struct - Adaptive Control -typedef struct { - uint32_t nMode; //!< 0-do not adapt at all, 1-always adapt, 2-adapt if timer not timed out - float fTimeOutMinutes; //!< how many minutes until time out - float fElapsedMinutes; //!< the amount of time that has elapsed -} cbAdaptControl; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Data Packet Structures (chid<0x8000) -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////// -// Declarations of firmware update packet structures and constants -// Never change anything about this packet -//////////////////////////////////////////////////////////////////// -#define cbRUNLEVEL_UPDATE 78 -#define cbPKTTYPE_UPDATESET 0xF1 -#define cbPKTTYPE_UPDATEREP 0x71 -#define cbPKTDLEN_UPDATE ((sizeof(cbPKT_UPDATE)/4)-2) - -/// @brief PKT Set:0xF1 Rep:0x71 - Update Packet -/// -/// Update the firmware of the NSP. This will copy data received into files in a temporary location and if -/// completed, on reboot will copy the files to the proper location to run. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - char filename[64]; //!< filename to be updated - uint32_t blockseq; //!< sequence of the current block - uint32_t blockend; //!< last block of the current file - uint32_t blocksiz; //!< block size of the current block - uint8_t block[512]; //!< block data -} cbPKT_UPDATE; - -/// @brief PKT Set:0xF1 Rep:0x71 - Update Packet -/// -/// Update the firmware of the NSP. This will copy data received into files in a temporary location and if -/// completed, on reboot will copy the files to the proper location to run. -/// -/// Since the NSP needs to work with old versions of the firmware, this packet retains the old header format. -typedef struct { - uint32_t time; //!< system clock timestamp - uint16_t chan; //!< channel identifier - uint8_t type; //!< packet type - uint8_t dlen; //!< length of data field in 32-bit chunks - char filename[64]; //!< filename to be updated - uint32_t blockseq; //!< sequence of the current block - uint32_t blockend; //!< last block of the current file - uint32_t blocksiz; //!< block size of the current block - uint8_t block[512]; //!< block data -} cbPKT_UPDATE_OLD; - -/// @brief Data packet - Sample Group data packet -/// -/// This packet contains each sample for the specified group. The group is specified in the type member of the -/// header. Groups are currently 1=500S/s, 2=1kS/s, 3=2kS/s, 4=10kS/s, 5=30kS/s, 6=raw (3-kS/s no filter). The -/// list of channels associated with each group is transmitted using the cbPKT_GROUPINFO when the list of channels -/// changes. cbpkt_header.chid is always zero. cbpkt_header.type is the group number -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - A2D_DATA data[cbNUM_ANALOG_CHANS]; // variable length address list -} cbPKT_GROUP; - -#define DINP_EVENT_ANYBIT 0x00000001 -#define DINP_EVENT_STROBE 0x00000002 - -/// @brief Data packet - Digital input data value. -/// -/// This packet is sent when a digital input value has met the criteria set in Hardware Configuration. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header -#ifdef CBPROTO_311 - uint32_t data[254]; //!< data buffer (up to 1016 bytes) -#else - uint32_t valueRead; //!< data read from the digital input port - uint32_t bitsChanged; //!< bits that have changed from the last packet sent - uint32_t eventType; //!< type of event, eg DINP_EVENT_ANYBIT, DINP_EVENT_STROBE -#endif -} cbPKT_DINP; - - -/// Note: cbMAX_PNTS must be an even number -#define cbMAX_PNTS 128 // spike width in samples; should be long enough for widest possible spike waveform - -#define cbPKTDLEN_SPK ((sizeof(cbPKT_SPK)/4) - cbPKT_HEADER_32SIZE) -#define cbPKTDLEN_SPKSHORT (cbPKTDLEN_SPK - ((sizeof(int16_t)*cbMAX_PNTS)/4)) - -/// @brief Data packet - Spike waveform data -/// -/// Detected spikes are sent through this packet. The spike waveform may or may not be sent depending -/// on the dlen contained in the header. The waveform can be anywhere from 30 samples to 128 samples -/// based on user configuration. The default spike length is 48 samples. cbpkt_header.chid is the -/// channel number of the spike. cbpkt_header.type is the sorted unit number (0=unsorted, 255=noise). -typedef struct { - cbPKT_HEADER cbpkt_header; //!< in the header for this packet, the type is used as the unit number - - float fPattern[3]; //!< values of the pattern space (Normal uses only 2, PCA uses third) - int16_t nPeak; //!< highest datapoint of the waveform - int16_t nValley; //!< lowest datapoint of the waveform - - int16_t wave[cbMAX_PNTS]; //!< datapoints of each sample of the waveform. Room for all possible points collected - //!< wave must be the last item in the structure because it can be variable length to a max of cbMAX_PNTS -} cbPKT_SPK; - -/// @brief Gyro Data packet - Gyro input data value. -/// -/// This packet is sent when gyro data has changed. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint8_t gyroscope[4]; //!< X, Y, Z values read from the gyroscope, last byte set to zero - uint8_t accelerometer[4]; //!< X, Y, Z values read from the accelerometer, last byte set to zero - uint8_t magnetometer[4]; //!< X, Y, Z values read from the magnetometer, last byte set to zero - uint16_t temperature; //!< temperature data - uint16_t reserved; //!< set to zero -} cbPKT_GYRO; - -/// System Heartbeat Packet (sent every 10ms) -#define cbPKTTYPE_SYSHEARTBEAT 0x00 -#define cbPKTDLEN_SYSHEARTBEAT ((sizeof(cbPKT_SYSHEARTBEAT)/4) - cbPKT_HEADER_32SIZE) -#define HEARTBEAT_MS 10 - -/// @brief PKT Set:N/A Rep:0x00 - System Heartbeat packet -/// -/// This is sent every 10ms -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - -} cbPKT_SYSHEARTBEAT; - -// Audio commands "val" -#define cbAUDIO_CMD_NONE 0 // PC->NPLAY query audio status -// nPlay file version (first byte NSx version, second byte NEV version) -#define cbNPLAY_FILE_NS21 1 // NSX 2.1 file -#define cbNPLAY_FILE_NS22 2 // NSX 2.2 file -#define cbNPLAY_FILE_NS30 3 // NSX 3.0 file -#define cbNPLAY_FILE_NEV21 (1 << 8) // Nev 2.1 file -#define cbNPLAY_FILE_NEV22 (2 << 8) // Nev 2.2 file -#define cbNPLAY_FILE_NEV23 (3 << 8) // Nev 2.3 file -#define cbNPLAY_FILE_NEV30 (4 << 8) // Nev 3.0 file -// nPlay commands and status changes (cbPKT_NPLAY.mode) -#define cbNPLAY_FNAME_LEN (cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE - 40) //!< length of the file name (with terminating null) Note: if changing info in the packet, change the constant -#define cbNPLAY_MODE_NONE 0 //!< no command (parameters) -#define cbNPLAY_MODE_PAUSE 1 //!< PC->NPLAY pause if "val" is non-zero, un-pause otherwise -#define cbNPLAY_MODE_SEEK 2 //!< PC->NPLAY seek to time "val" -#define cbNPLAY_MODE_CONFIG 3 //!< PC<->NPLAY request full config -#define cbNPLAY_MODE_OPEN 4 //!< PC->NPLAY open new file in "val" for playback -#define cbNPLAY_MODE_PATH 5 //!< PC->NPLAY use the directory path in fname -#define cbNPLAY_MODE_CONFIGMAIN 6 //!< PC<->NPLAY request main config packet -#define cbNPLAY_MODE_STEP 7 //!< PC<->NPLAY run "val" procTime steps and pause, then send cbNPLAY_FLAG_STEPPED -#define cbNPLAY_MODE_SINGLE 8 //!< PC->NPLAY single mode if "val" is non-zero, wrap otherwise -#define cbNPLAY_MODE_RESET 9 //!< PC->NPLAY reset nPlay -#define cbNPLAY_MODE_NEVRESORT 10 //!< PC->NPLAY resort NEV if "val" is non-zero, do not if otherwise -#define cbNPLAY_MODE_AUDIO_CMD 11 //!< PC->NPLAY perform audio command in "val" (cbAUDIO_CMD_*), with option "opt" -#define cbNPLAY_FLAG_NONE 0x00 //!< no flag -#define cbNPLAY_FLAG_CONF 0x01 //!< NPLAY->PC config packet ("val" is "fname" file index) -#define cbNPLAY_FLAG_MAIN (0x02 | cbNPLAY_FLAG_CONF) //!< NPLAY->PC main config packet ("val" is file version) -#define cbNPLAY_FLAG_DONE 0x02 //!< NPLAY->PC step command done - -// nPlay configuration packet(sent on restart together with config packet) -#define cbPKTTYPE_NPLAYREP 0x5C /* NPLAY->PC response */ -#define cbPKTTYPE_NPLAYSET 0xDC /* PC->NPLAY request */ -#define cbPKTDLEN_NPLAY ((sizeof(cbPKT_NPLAY)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xDC Rep:0x5C - nPlay configuration packet(sent on restart together with config packet) -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - union { - PROCTIME ftime; //!< the total time of the file. - PROCTIME opt; //!< optional value - }; - PROCTIME stime; //!< start time - PROCTIME etime; //!< stime < end time < ftime - PROCTIME val; //!< Used for current time to traverse, file index, file version, ... - uint16_t mode; //!< cbNPLAY_MODE_* command to nPlay - uint16_t flags; //!< cbNPLAY_FLAG_* status of nPlay - float speed; //!< positive means fast-forward, negative means rewind, 0 means go as fast as you can. - char fname[cbNPLAY_FNAME_LEN]; //!< This is a String with the file name. -} cbPKT_NPLAY; - -#define cbTRIGGER_MODE_UNDEFINED 0 -#define cbTRIGGER_MODE_BUTTONPRESS 1 // Patient button press event -#define cbTRIGGER_MODE_EVENTRESET 2 // event reset - -// Custom trigger event packet -#define cbPKTTYPE_TRIGGERREP 0x5E /* NPLAY->PC response */ -#define cbPKTTYPE_TRIGGERSET 0xDE /* PC->NPLAY request */ -#define cbPKTDLEN_TRIGGER ((sizeof(cbPKT_TRIGGER)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xDE Rep:0x5E - Trigger Packet used for Cervello system -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t mode; //!< cbTRIGGER_MODE_* -} cbPKT_TRIGGER; - -// Video tracking event packet -#define cbMAX_TRACKCOORDS (128) // Maximum nimber of coordinates (must be an even number) -#define cbPKTTYPE_VIDEOTRACKREP 0x5F /* NPLAY->PC response */ -#define cbPKTTYPE_VIDEOTRACKSET 0xDF /* PC->NPLAY request */ -#define cbPKTDLEN_VIDEOTRACK ((sizeof(cbPKT_VIDEOTRACK)/4) - cbPKT_HEADER_32SIZE) -#define cbPKTDLEN_VIDEOTRACKSHORT (cbPKTDLEN_VIDEOTRACK - ((sizeof(uint16_t)*cbMAX_TRACKCOORDS)/4)) - -/// @brief PKT Set:0xDF Rep:0x5F - NeuroMotive video tracking -/// -/// Tracks objects within NeuroMotive -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint16_t parentID; //!< parent ID - uint16_t nodeID; //!< node ID (cross-referenced in the TrackObj header) - uint16_t nodeCount; //!< Children count - uint16_t pointCount; //!< number of points at this node - //!< this must be the last item in the structure because it can be variable length to a max of cbMAX_TRACKCOORDS - union { - uint16_t coords[cbMAX_TRACKCOORDS]; - uint32_t sizes[cbMAX_TRACKCOORDS / 2]; - }; -} cbPKT_VIDEOTRACK; - - -#define cbLOG_MODE_NONE 0 // Normal log -#define cbLOG_MODE_CRITICAL 1 // Critical log -#define cbLOG_MODE_RPC 2 // PC->NSP: Remote Procedure Call (RPC) -#define cbLOG_MODE_PLUGINFO 3 // NSP->PC: Plugin information -#define cbLOG_MODE_RPC_RES 4 // NSP->PC: Remote Procedure Call Results -#define cbLOG_MODE_PLUGINERR 5 // NSP->PC: Plugin error information -#define cbLOG_MODE_RPC_END 6 // NSP->PC: Last RPC packet -#define cbLOG_MODE_RPC_KILL 7 // PC->NSP: terminate last RPC -#define cbLOG_MODE_RPC_INPUT 8 // PC->NSP: RPC command input -#define cbLOG_MODE_UPLOAD_RES 9 // NSP->PC: Upload result -#define cbLOG_MODE_ENDPLUGIN 10 // PC->NSP: Signal the plugin to end -#define cbLOG_MODE_NSP_REBOOT 11 // PC->NSP: Reboot the NSP - -// Reconfiguration log event -#define cbMAX_LOG 128 // Maximum log description -#define cbPKTTYPE_LOGREP 0x63 /* NPLAY->PC response */ -#define cbPKTTYPE_LOGSET 0xE3 /* PC->NPLAY request */ -#define cbPKTDLEN_LOG ((sizeof(cbPKT_LOG)/4) - cbPKT_HEADER_32SIZE) -#define cbPKTDLEN_LOGSHORT (cbPKTDLEN_LOG - ((sizeof(char)*cbMAX_LOG)/4)) // All but description - -/// @brief PKT Set:0xE3 Rep:0x63 - Log packet -/// -/// Similar to the comment packet but used for internal NSP events and extension communicationl. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint16_t mode; //!< log mode (cbLOG_MODE_*) - char name[cbLEN_STR_LABEL]; //!< Logger source name (Computer name, Plugin name, ...) - char desc[cbMAX_LOG]; //!< description of the change (will fill the rest of the packet) -} cbPKT_LOG; - -// Protocol Monitoring packet (sent periodically about every second) -#define cbPKTTYPE_SYSPROTOCOLMONITOR 0x01 -#define cbPKTDLEN_SYSPROTOCOLMONITOR ((sizeof(cbPKT_SYSPROTOCOLMONITOR)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:N/A Rep:0x01 - System protocol monitor -/// -/// Packets are sent via UDP. This packet is sent by the NSP every 10ms telling Central how many packets have been sent -/// since the last packet was sent. Central compares it with the number of packets it has received since the last packet -/// was received. If there is a difference, Central displays an error messages that packets have been lost. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t sentpkts; //!< Packets sent since last cbPKT_SYSPROTOCOLMONITOR (or 0 if timestamp=0); - //!< the cbPKT_SYSPROTOCOLMONITOR packets are counted as well so this must - //!< be equal to at least 1 -#ifndef CBPROTO_311 - uint32_t counter; //!< Counter of number cbPKT_SYSPROTOCOLMONITOR packets sent since beginning of NSP time -#endif -} cbPKT_SYSPROTOCOLMONITOR; - - -#define cbPKTTYPE_REQCONFIGALL 0x88 // request for ALL configuration information -#define cbPKTTYPE_REPCONFIGALL 0x08 // response that NSP got your request - - -// System Condition Report Packet -#define cbPKTTYPE_SYSREP 0x10 -#define cbPKTTYPE_SYSREPSPKLEN 0x11 -#define cbPKTTYPE_SYSREPRUNLEV 0x12 -#define cbPKTTYPE_SYSSET 0x90 -#define cbPKTTYPE_SYSSETSPKLEN 0x91 -#define cbPKTTYPE_SYSSETRUNLEV 0x92 -#define cbPKTDLEN_SYSINFO ((sizeof(cbPKT_SYSINFO)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0x88 Rep:0x08 - System info -/// -/// Contains system information including the runlevel -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t sysfreq; //!< System sampling clock frequency in Hz - uint32_t spikelen; //!< The length of the spike events - uint32_t spikepre; //!< Spike pre-trigger samples - uint32_t resetque; //!< The channel for the reset to que on - uint32_t runlevel; //!< System runlevel - uint32_t runflags; //!< Lock recording after reset -// uint32_t timeres; //!< Time resolution in integers per second - possible enhancement since we are now using PTP time -} cbPKT_SYSINFO; - -#define cbPKTDLEN_OLDSYSINFO ((sizeof(cbPKT_OLDSYSINFO)/4) - 2) -typedef struct { - uint32_t time; // system clock timestamp - uint16_t chid; // 0x8000 - uint8_t type; // PKTTYPE_SYS* - uint8_t dlen; // cbPKT_OLDSYSINFODLEN - - uint32_t sysfreq; // System clock frequency in Hz - uint32_t spikelen; // The length of the spike events - uint32_t spikepre; // Spike pre-trigger samples - uint32_t resetque; // The channel for the reset to que on - uint32_t runlevel; // System runlevel - uint32_t runflags; -} cbPKT_OLDSYSINFO; - -#define cbRUNLEVEL_STARTUP 10 -#define cbRUNLEVEL_HARDRESET 20 -#define cbRUNLEVEL_STANDBY 30 -#define cbRUNLEVEL_RESET 40 -#define cbRUNLEVEL_RUNNING 50 -#define cbRUNLEVEL_STRESSED 60 -#define cbRUNLEVEL_ERROR 70 -#define cbRUNLEVEL_SHUTDOWN 80 - -#define cbRUNFLAGS_NONE 0 -#define cbRUNFLAGS_LOCK 1 // Lock recording after reset - -#define cbPKTTYPE_VIDEOSYNCHREP 0x29 /* NSP->PC response */ -#define cbPKTTYPE_VIDEOSYNCHSET 0xA9 /* PC->NSP request */ -#define cbPKTDLEN_VIDEOSYNCH ((sizeof(cbPKT_VIDEOSYNCH)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xA9 Rep:0x29 - Video/external synchronization packet. -/// -/// This packet comes from NeuroMotive through network or RS232 -/// then is transmitted to the Central after spike or LFP packets to stamp them. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint16_t split; //!< file split number of the video file - uint32_t frame; //!< frame number in last video - uint32_t etime; //!< capture elapsed time (in milliseconds) - uint16_t id; //!< video source id -} cbPKT_VIDEOSYNCH; - -// Comment annotation packet. -#define cbMAX_COMMENT 128 // cbMAX_COMMENT must be a multiple of four -#define cbPKTTYPE_COMMENTREP 0x31 /* NSP->PC response */ -#define cbPKTTYPE_COMMENTSET 0xB1 /* PC->NSP request */ -#define cbPKTDLEN_COMMENT ((sizeof(cbPKT_COMMENT)/4) - cbPKT_HEADER_32SIZE) -#define cbPKTDLEN_COMMENTSHORT (cbPKTDLEN_COMMENT - ((sizeof(uint8_t)*cbMAX_COMMENT)/4)) - -/// @brief PKT Set:0xB1 Rep:0x31 - Comment annotation packet. -/// -/// This packet injects a comment into the data stream which gets recorded in the file and displayed on Raster. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header -#ifdef CBPROTO_311 - struct { - uint8_t charset; //!< Character set (0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI) - uint8_t flags; //!< Can be any of cbCOMMENT_FLAG_* - uint8_t reserved[2]; //!< Reserved (must be 0) - } info; - uint32_t data; //!< Depends on .info.flags -#else - struct { - uint8_t charset; //!< Character set (0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI) - uint8_t reserved[3]; //!< Reserved (must be 0) - } info; - PROCTIME timeStarted; //!< Start time of when the user started typing the comment - uint32_t rgba; //!< rgba to color the comment -#endif - char comment[cbMAX_COMMENT]; //!< Comment -} cbPKT_COMMENT; - -// NeuroMotive status -#define cbNM_STATUS_IDLE 0 // NeuroMotive is idle -#define cbNM_STATUS_EXIT 1 // NeuroMotive is exiting -#define cbNM_STATUS_REC 2 // NeuroMotive is recording -#define cbNM_STATUS_PLAY 3 // NeuroMotive is playing video file -#define cbNM_STATUS_CAP 4 // NeuroMotive is capturing from camera -#define cbNM_STATUS_STOP 5 // NeuroMotive is stopping -#define cbNM_STATUS_PAUSED 6 // NeuroMotive is paused -#define cbNM_STATUS_COUNT 7 // This is the count of status options - -// NeuroMotive commands and status changes (cbPKT_NM.mode) -#define cbNM_MODE_NONE 0 -#define cbNM_MODE_CONFIG 1 // Ask NeuroMotive for configuration -#define cbNM_MODE_SETVIDEOSOURCE 2 // Configure video source -#define cbNM_MODE_SETTRACKABLE 3 // Configure trackable -#define cbNM_MODE_STATUS 4 // NeuroMotive status reporting (cbNM_STATUS_*) -#define cbNM_MODE_TSCOUNT 5 // Timestamp count (value is the period with 0 to disable this mode) -#define cbNM_MODE_SYNCHCLOCK 6 // Start (or stop) synchronization clock (fps*1000 specified by value, zero fps to stop capture) -#define cbNM_MODE_ASYNCHCLOCK 7 // Asynchronous clock -#define cbNM_FLAG_NONE 0 - -#define cbPKTTYPE_NMREP 0x32 /* NSP->PC response */ -#define cbPKTTYPE_NMSET 0xB2 /* PC->NSP request */ -#define cbPKTDLEN_NM ((sizeof(cbPKT_NM)/4) - cbPKT_HEADER_32SIZE) -/// @brief PKT Set:0xB2 Rep:0x32 - NeuroMotive packet structure -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t mode; //!< cbNM_MODE_* command to NeuroMotive or Central - uint32_t flags; //!< cbNM_FLAG_* status of NeuroMotive - uint32_t value; //!< value assigned to this mode - union { - uint32_t opt[cbLEN_STR_LABEL / 4]; //!< Additional options for this mode - char name[cbLEN_STR_LABEL]; //!< name associated with this mode - }; -} cbPKT_NM; - - -// Report Processor Information (duplicates the cbPROCINFO structure) -#define cbPKTTYPE_PROCREP 0x21 -#define cbPKTDLEN_PROCINFO ((sizeof(cbPKT_PROCINFO)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:N/A Rep:0x21 - Info about the processor -/// -/// Includes information about the counts of various features of the processor -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t proc; //!< index of the bank - uint32_t idcode; //!< manufacturer part and rom ID code of the Signal Processor - char ident[cbLEN_STR_IDENT]; //!< ID string with the equipment name of the Signal Processor - uint32_t chanbase; //!< The lowest channel number of channel id range claimed by this processor - uint32_t chancount; //!< number of channel identifiers claimed by this processor - uint32_t bankcount; //!< number of signal banks supported by the processor - uint32_t groupcount; //!< number of sample groups supported by the processor - uint32_t filtcount; //!< number of digital filters supported by the processor - uint32_t sortcount; //!< number of channels supported for spike sorting (reserved for future) - uint32_t unitcount; //!< number of supported units for spike sorting (reserved for future) - uint32_t hoopcount; //!< number of supported units for spike sorting (reserved for future) - uint32_t reserved; //!< reserved for future use, set to 0 - uint32_t version; //!< current version of libraries -} cbPKT_PROCINFO; - -// Report Bank Information (duplicates the cbBANKINFO structure) -#define cbPKTTYPE_BANKREP 0x22 -#define cbPKTDLEN_BANKINFO ((sizeof(cbPKT_BANKINFO)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:N/A Rep:0x22 - Information about the banks in the processor -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t proc; //!< the address of the processor on which the bank resides - uint32_t bank; //!< the address of the bank reported by the packet - uint32_t idcode; //!< manufacturer part and rom ID code of the module addressed to this bank - char ident[cbLEN_STR_IDENT]; //!< ID string with the equipment name of the Signal Bank hardware module - char label[cbLEN_STR_LABEL]; //!< Label on the instrument for the signal bank, eg "Analog In" - uint32_t chanbase; //!< The lowest channel number of channel id range claimed by this bank - uint32_t chancount; //!< number of channel identifiers claimed by this bank -} cbPKT_BANKINFO; - -// Filter (FILT) Information Packets -#define cbPKTTYPE_FILTREP 0x23 -#define cbPKTTYPE_FILTSET 0xA3 -#define cbPKTDLEN_FILTINFO ((sizeof(cbPKT_FILTINFO)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xA3 Rep:0x23 - Filter Information Packet -/// -/// Describes the filters contained in the NSP including the filter coefficients -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t proc; //!< - uint32_t filt; //!< - char label[cbLEN_STR_FILT_LABEL]; // name of the filter - uint32_t hpfreq; //!< high-pass corner frequency in milliHertz - uint32_t hporder; //!< high-pass filter order - uint32_t hptype; //!< high-pass filter type - uint32_t lpfreq; //!< low-pass frequency in milliHertz - uint32_t lporder; //!< low-pass filter order - uint32_t lptype; //!< low-pass filter type - //!< These are for sending the NSP filter info, otherwise the NSP has this stuff in NSPDefaults.c - double gain; //!< filter gain - double sos1a1; //!< filter coefficient - double sos1a2; //!< filter coefficient - double sos1b1; //!< filter coefficient - double sos1b2; //!< filter coefficient - double sos2a1; //!< filter coefficient - double sos2a2; //!< filter coefficient - double sos2b1; //!< filter coefficient - double sos2b2; //!< filter coefficient -} cbPKT_FILTINFO; - -// Factory Default settings request packet -#define cbPKTTYPE_CHANRESETREP 0x24 /* NSP->PC response...ignore all values */ -#define cbPKTTYPE_CHANRESET 0xA4 /* PC->NSP request */ -#define cbPKTDLEN_CHANRESET ((sizeof(cbPKT_CHANRESET) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xA4 Rep:0x24 - Channel reset packet -/// -/// This resets various aspects of a channel. For each member, 0 doesn't change the value, any non-zero value resets -/// the property to factory defaults -/// This is currently not used in the system. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t chan; //!< actual channel id of the channel being configured - - // For all the values that follow - // 0 = NOT change value; nonzero = reset to factory defaults - - uint8_t label; //!< Channel label - uint8_t userflags; //!< User flags for the channel state - uint8_t position; //!< reserved for future position information - uint8_t scalin; //!< user-defined scaling information - uint8_t scalout; //!< user-defined scaling information - uint8_t doutopts; //!< digital output options (composed of cbDOUT_* flags) - uint8_t dinpopts; //!< digital input options (composed of cbDINP_* flags) - uint8_t aoutopts; //!< analog output options - uint8_t eopchar; //!< the end of packet character - uint8_t monsource; //!< address of channel to monitor - uint8_t outvalue; //!< output value - uint8_t ainpopts; //!< analog input options (composed of cbAINP_* flags) - uint8_t lncrate; //!< line noise cancellation filter adaptation rate - uint8_t smpfilter; //!< continuous-time pathway filter id - uint8_t smpgroup; //!< continuous-time pathway sample group - uint8_t smpdispmin; //!< continuous-time pathway display factor - uint8_t smpdispmax; //!< continuous-time pathway display factor - uint8_t spkfilter; //!< spike pathway filter id - uint8_t spkdispmax; //!< spike pathway display factor - uint8_t lncdispmax; //!< Line Noise pathway display factor - uint8_t spkopts; //!< spike processing options - uint8_t spkthrlevel; //!< spike threshold level - uint8_t spkthrlimit; //!< - uint8_t spkgroup; //!< NTrodeGroup this electrode belongs to - 0 is single unit, non-0 indicates a multi-trode grouping - uint8_t spkhoops; //!< spike hoop sorting set -} cbPKT_CHANRESET; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Adaptive filtering -#define cbPKTTYPE_ADAPTFILTREP 0x25 /* NSP->PC response...*/ -#define cbPKTTYPE_ADAPTFILTSET 0xA5 /* PC->NSP request */ -#define cbPKTDLEN_ADAPTFILTINFO ((sizeof(cbPKT_ADAPTFILTINFO) / 4) - cbPKT_HEADER_32SIZE) -// These are the adaptive filter settings -#define ADAPT_FILT_DISABLED 0 -#define ADAPT_FILT_ALL 1 -#define ADAPT_FILT_SPIKES 2 - -/// @brief PKT Set:0xA5 Rep:0x25 - Adaptive filtering -/// -/// This sets the parameters for the adaptive filtering. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t chan; //!< Ignored - - uint32_t nMode; //!< 0=disabled, 1=filter continuous & spikes, 2=filter spikes - float dLearningRate; //!< speed at which adaptation happens. Very small. e.g. 5e-12 - uint32_t nRefChan1; //!< The first reference channel (1 based). - uint32_t nRefChan2; //!< The second reference channel (1 based). - -} cbPKT_ADAPTFILTINFO; // The packet....look below vvvvvvvv - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Reference Electrode filtering -#define cbPKTTYPE_REFELECFILTREP 0x26 /* NSP->PC response...*/ -#define cbPKTTYPE_REFELECFILTSET 0xA6 /* PC->NSP request */ -#define cbPKTDLEN_REFELECFILTINFO ((sizeof(cbPKT_REFELECFILTINFO) / 4) - cbPKT_HEADER_32SIZE) -// These are the reference electrode filter settings -#define REFELEC_FILT_DISABLED 0 -#define REFELEC_FILT_ALL 1 -#define REFELEC_FILT_SPIKES 2 - -/// @brief PKT Set:0xA6 Rep:0x26 - Reference Electrode Information. -/// -/// This configures a channel to be referenced by another channel. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t chan; //!< Ignored - - uint32_t nMode; //!< 0=disabled, 1=filter continuous & spikes, 2=filter spikes - uint32_t nRefChan; //!< The reference channel (1 based). -} cbPKT_REFELECFILTINFO; // The packet - -// N-Trode Information Packets -enum cbNTRODEINFO_FS_MODE { cbNTRODEINFO_FS_PEAK, cbNTRODEINFO_FS_VALLEY, cbNTRODEINFO_FS_AMPLITUDE, cbNTRODEINFO_FS_COUNT }; -#define cbPKTTYPE_REPNTRODEINFO 0x27 /* NSP->PC response...*/ -#define cbPKTTYPE_SETNTRODEINFO 0xA7 /* PC->NSP request */ -#define cbPKTDLEN_NTRODEINFO ((sizeof(cbPKT_NTRODEINFO) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xA7 Rep:0x27 - N-Trode information packets -/// -/// Sets information about an N-Trode. The user change the name, number of sites, sites (channels), -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t ntrode; //!< ntrode with which we are working (1-based) - char label[cbLEN_STR_LABEL]; //!< Label of the Ntrode (null terminated if < 16 characters) - cbMANUALUNITMAPPING ellipses[cbMAXSITEPLOTS][cbMAXUNITS]; //!< unit mapping - uint16_t nSite; //!< number channels in this NTrode ( 0 <= nSite <= cbMAXSITES) - uint16_t fs; //!< NTrode feature space cbNTRODEINFO_FS_* - uint16_t nChan[cbMAXSITES]; //!< group of channels in this NTrode -} cbPKT_NTRODEINFO; // ntrode information packet - -// Sample Group (GROUP) Information Packets -#define cbPKTTYPE_GROUPREP 0x30 //!< (lower 7bits=ppppggg) -#define cbPKTTYPE_GROUPSET 0xB0 -#define cbPKTDLEN_GROUPINFO ((sizeof(cbPKT_GROUPINFO)/4) - cbPKT_HEADER_32SIZE) -#define cbPKTDLEN_GROUPINFOSHORT (8) // basic length without list - -/// @brief PKT Set:0xB0 Rep:0x30 - Sample Group (GROUP) Information Packets -/// -/// Contains information including the name and list of channels for each sample group. The cbPKT_GROUP packet transmits -/// the data for each group based on the list contained here. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t proc; //!< processor number - uint32_t group; //!< group number - char label[cbLEN_STR_LABEL]; //!< sampling group label - uint32_t period; //!< sampling period for the group - uint32_t length; //!< number of channels in the list - uint16_t list[cbNUM_ANALOG_CHANS_STRUCT]; //!< variable length list. Uses NSP device size for shared memory compatibility -} cbPKT_GROUPINFO; - -// Analog Input (AINP) Information Packets -#define cbPKTTYPE_CHANREP 0x40 -#define cbPKTTYPE_CHANREPLABEL 0x41 -#define cbPKTTYPE_CHANREPSCALE 0x42 -#define cbPKTTYPE_CHANREPDOUT 0x43 -#define cbPKTTYPE_CHANREPDINP 0x44 -#define cbPKTTYPE_CHANREPAOUT 0x45 -#define cbPKTTYPE_CHANREPDISP 0x46 -#define cbPKTTYPE_CHANREPAINP 0x47 -#define cbPKTTYPE_CHANREPSMP 0x48 -#define cbPKTTYPE_CHANREPSPK 0x49 -#define cbPKTTYPE_CHANREPSPKTHR 0x4A -#define cbPKTTYPE_CHANREPSPKHPS 0x4B -#define cbPKTTYPE_CHANREPUNITOVERRIDES 0x4C -#define cbPKTTYPE_CHANREPNTRODEGROUP 0x4D -#define cbPKTTYPE_CHANREPREJECTAMPLITUDE 0x4E -#define cbPKTTYPE_CHANREPAUTOTHRESHOLD 0x4F -#define cbPKTTYPE_CHANSET 0xC0 -#define cbPKTTYPE_CHANSETLABEL 0xC1 -#define cbPKTTYPE_CHANSETSCALE 0xC2 -#define cbPKTTYPE_CHANSETDOUT 0xC3 -#define cbPKTTYPE_CHANSETDINP 0xC4 -#define cbPKTTYPE_CHANSETAOUT 0xC5 -#define cbPKTTYPE_CHANSETDISP 0xC6 -#define cbPKTTYPE_CHANSETAINP 0xC7 -#define cbPKTTYPE_CHANSETSMP 0xC8 -#define cbPKTTYPE_CHANSETSPK 0xC9 -#define cbPKTTYPE_CHANSETSPKTHR 0xCA -#define cbPKTTYPE_CHANSETSPKHPS 0xCB -#define cbPKTTYPE_CHANSETUNITOVERRIDES 0xCC -#define cbPKTTYPE_CHANSETNTRODEGROUP 0xCD -#define cbPKTTYPE_CHANSETREJECTAMPLITUDE 0xCE -#define cbPKTTYPE_CHANSETAUTOTHRESHOLD 0xCF -#define cbPKTDLEN_CHANINFO ((sizeof(cbPKT_CHANINFO)/4) - cbPKT_HEADER_32SIZE) -#define cbPKTDLEN_CHANINFOSHORT (cbPKTDLEN_CHANINFO - ((sizeof(cbHOOP)*cbMAXUNITS*cbMAXHOOPS)/4)) - -/// @brief PKT Set:0xCx Rep:0x4x - Channel Information -/// -/// This contains the details for each channel within the system. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t chan; //!< actual channel id of the channel being configured - uint32_t proc; //!< the address of the processor on which the channel resides - uint32_t bank; //!< the address of the bank on which the channel resides - uint32_t term; //!< the terminal number of the channel within it's bank - uint32_t chancaps; //!< general channel capablities (given by cbCHAN_* flags) - uint32_t doutcaps; //!< digital output capablities (composed of cbDOUT_* flags) - uint32_t dinpcaps; //!< digital input capablities (composed of cbDINP_* flags) - uint32_t aoutcaps; //!< analog output capablities (composed of cbAOUT_* flags) - uint32_t ainpcaps; //!< analog input capablities (composed of cbAINP_* flags) - uint32_t spkcaps; //!< spike processing capabilities - cbSCALING physcalin; //!< physical channel scaling information - cbFILTDESC phyfiltin; //!< physical channel filter definition - cbSCALING physcalout; //!< physical channel scaling information - cbFILTDESC phyfiltout; //!< physical channel filter definition - char label[cbLEN_STR_LABEL]; //!< Label of the channel (null terminated if <16 characters) - uint32_t userflags; //!< User flags for the channel state - int32_t position[4]; //!< reserved for future position information - cbSCALING scalin; //!< user-defined scaling information for AINP - cbSCALING scalout; //!< user-defined scaling information for AOUT - uint32_t doutopts; //!< digital output options (composed of cbDOUT_* flags) - uint32_t dinpopts; //!< digital input options (composed of cbDINP_* flags) - uint32_t aoutopts; //!< analog output options - uint32_t eopchar; //!< digital input capablities (given by cbDINP_* flags) - union { - struct { // separate system channel to instrument specific channel number -#ifdef CBPROTO_311 - uint32_t monsource; // address of channel to monitor. Backport: (.monchan << 16) + .moninst -#else - uint16_t moninst; //!< instrument of channel to monitor. Not part of monsource in proto 3.11. - uint16_t monchan; //!< channel to monitor. Can be retrieved from (monsource >> 16) & 0xFFFF -#endif - int32_t outvalue; //!< output value - }; - struct { // used for digout timed output - uint16_t lowsamples; //!< number of samples to set low for timed output - uint16_t highsamples; //!< number of samples to set high for timed output - int32_t offset; //!< number of samples to offset the transitions for timed output - }; - }; - uint8_t trigtype; //!< trigger type (see cbDOUT_TRIGGER_*) -#ifndef CBPROTO_311 - uint8_t reserved[2]; //!< 2 bytes reserved - uint8_t triginst; //!< instrument of the trigger channel -#endif - uint16_t trigchan; //!< trigger channel - uint16_t trigval; //!< trigger value - uint32_t ainpopts; //!< analog input options (composed of cbAINP* flags) - uint32_t lncrate; //!< line noise cancellation filter adaptation rate - uint32_t smpfilter; //!< continuous-time pathway filter id - uint32_t smpgroup; //!< continuous-time pathway sample group - int32_t smpdispmin; //!< continuous-time pathway display factor - int32_t smpdispmax; //!< continuous-time pathway display factor - uint32_t spkfilter; //!< spike pathway filter id - int32_t spkdispmax; //!< spike pathway display factor - int32_t lncdispmax; //!< Line Noise pathway display factor - uint32_t spkopts; //!< spike processing options - int32_t spkthrlevel; //!< spike threshold level - int32_t spkthrlimit; //!< - uint32_t spkgroup; //!< NTrodeGroup this electrode belongs to - 0 is single unit, non-0 indicates a multi-trode grouping - int16_t amplrejpos; //!< Amplitude rejection positive value - int16_t amplrejneg; //!< Amplitude rejection negative value - uint32_t refelecchan; //!< Software reference electrode channel - cbMANUALUNITMAPPING unitmapping[cbMAXUNITS]; //!< manual unit mapping - cbHOOP spkhoops[cbMAXUNITS][cbMAXHOOPS]; //!< spike hoop sorting set -} cbPKT_CHANINFO; - -///////////////////////////////////////////////////////////////////////////////// -// These are part of the "reflected" mechanism. They go out as type 0xE? and come -// Back in as type 0x6? - -#define cbPKTTYPE_MASKED_REFLECTED 0xE0 -#define cbPKTTYPE_COMPARE_MASK_REFLECTED 0xF0 -#define cbPKTTYPE_REFLECTED_CONVERSION_MASK 0x7F - - -// These are the masks for use with abyUnitSelections -enum -{ - UNIT_UNCLASS_MASK = 0x01, // mask to use to say unclassified units are selected - UNIT_1_MASK = 0x02, // mask to use to say unit 1 is selected - UNIT_2_MASK = 0x04, // mask to use to say unit 2 is selected - UNIT_3_MASK = 0x08, // mask to use to say unit 3 is selected - UNIT_4_MASK = 0x10, // mask to use to say unit 4 is selected - UNIT_5_MASK = 0x20, // mask to use to say unit 5 is selected - CONTINUOUS_MASK = 0x40, // mask to use to say the continuous signal is selected - - UNIT_ALL_MASK = UNIT_UNCLASS_MASK | - UNIT_1_MASK | // This means the channel is completely selected - UNIT_2_MASK | - UNIT_3_MASK | - UNIT_4_MASK | - UNIT_5_MASK | - CONTINUOUS_MASK | - 0xFF80, // this is here to select all digital inupt bits in raster when expanded -}; - - -// Unit selection packet -#define cbPKTTYPE_REPUNITSELECTION 0x62 -#define cbPKTTYPE_SETUNITSELECTION 0xE2 -#define cbPKTDLEN_UNITSELECTION ((sizeof(cbPKT_UNIT_SELECTION) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief Unit Selection -/// -/// Packet which says that these channels are now selected -typedef struct -{ - cbPKT_HEADER cbpkt_header; //!< packet header - - int32_t lastchan; //!< Which channel was clicked last. - uint16_t abyUnitSelections[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE - sizeof(int32_t))]; //!< one for each channel, channels are 0 based here, shows units selected -} cbPKT_UNIT_SELECTION; - -// file config options -#define cbFILECFG_OPT_NONE 0x00000000 // Launch File dialog, set file info, start or stop recording -#define cbFILECFG_OPT_KEEPALIVE 0x00000001 // Keep-alive message -#define cbFILECFG_OPT_REC 0x00000002 // Recording is in progress -#define cbFILECFG_OPT_STOP 0x00000003 // Recording stopped -#define cbFILECFG_OPT_NMREC 0x00000004 // NeuroMotive recording status -#define cbFILECFG_OPT_CLOSE 0x00000005 // Close file application -#define cbFILECFG_OPT_SYNCH 0x00000006 // Recording datetime -#define cbFILECFG_OPT_OPEN 0x00000007 // Launch File dialog, do not set or do anything -#define cbFILECFG_OPT_TIMEOUT 0x00000008 // Keep alive not received so it timed out -#define cbFILECFG_OPT_PAUSE 0x00000009 // Recording paused - -// file save configuration packet -#define cbPKTTYPE_REPFILECFG 0x61 -#define cbPKTTYPE_SETFILECFG 0xE1 -#define cbPKTDLEN_FILECFG ((sizeof(cbPKT_FILECFG)/4) - cbPKT_HEADER_32SIZE) -#define cbPKTDLEN_FILECFGSHORT (cbPKTDLEN_FILECFG - ((sizeof(char)*3*cbLEN_STR_COMMENT)/4)) // used for keep-alive messages - -/// @brief PKT Set:0xE1 Rep:0x61 - File configuration packet -/// -/// File recording can be started or stopped externally using this packet. It also contains a timeout mechanism to notify -/// if file isn't still recording. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t options; //!< cbFILECFG_OPT_* - uint32_t duration; - uint32_t recording; //!< If cbFILECFG_OPT_NONE this option starts/stops recording remotely - uint32_t extctrl; //!< If cbFILECFG_OPT_REC this is split number (0 for non-TOC) - //!< If cbFILECFG_OPT_STOP this is error code (0 means no error) - - char username[cbLEN_STR_COMMENT]; //!< name of computer issuing the packet - union { - char filename[cbLEN_STR_COMMENT]; //!< filename to record to - char datetime[cbLEN_STR_COMMENT]; //!< - }; - char comment[cbLEN_STR_COMMENT]; //!< comment to include in the file -} cbPKT_FILECFG; - -// Patient information for recording file -#define cbMAX_PATIENTSTRING 128 // - -#define cbPKTTYPE_REPPATIENTINFO 0x64 -#define cbPKTTYPE_SETPATIENTINFO 0xE4 -#define cbPKTDLEN_PATIENTINFO ((sizeof(cbPKT_PATIENTINFO)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xE4 Rep:0x64 - Patient information packet. -/// -/// This can be used to externally set the patient information of a file. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - char ID[cbMAX_PATIENTSTRING]; //!< Patient identification - char firstname[cbMAX_PATIENTSTRING]; //!< Patient first name - char lastname[cbMAX_PATIENTSTRING]; //!< Patient last name - uint32_t DOBMonth; //!< Patient birth month - uint32_t DOBDay; //!< Patient birth day - uint32_t DOBYear; //!< Patient birth year -} cbPKT_PATIENTINFO; - -// Calculated Impedance data packet -#define cbPKTTYPE_REPIMPEDANCE 0x65 -#define cbPKTTYPE_SETIMPEDANCE 0xE5 -#define cbPKTDLEN_IMPEDANCE ((sizeof(cbPKT_IMPEDANCE)/4) - cbPKT_HEADER_32SIZE) - -/// *Deprecated* Send impedance data -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - float data[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE) / sizeof(float)]; //!< variable length address list -} cbPKT_IMPEDANCE; - -// Poll packet command -#define cbPOLL_MODE_NONE 0 // no command (parameters) -#define cbPOLL_MODE_APPSTATUS 1 // Poll or response to poll about the status of an application -// Poll packet status flags -#define cbPOLL_FLAG_NONE 0 // no flag (parameters) -#define cbPOLL_FLAG_RESPONSE 1 // Response to the query -// Extra information -#define cbPOLL_EXT_NONE 0 // No extra information -#define cbPOLL_EXT_EXISTS 1 // App exists -#define cbPOLL_EXT_RUNNING 2 // App is running -// poll applications to determine different states -// Central will return if an application exists (and is running) -#define cbPKTTYPE_REPPOLL 0x67 -#define cbPKTTYPE_SETPOLL 0xE7 -#define cbPKTDLEN_POLL ((sizeof(cbPKT_POLL)/4) - cbPKT_HEADER_32SIZE) - -/// *Deprecated* Poll for packet mechanism -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t mode; //!< any of cbPOLL_MODE_* commands - uint32_t flags; //!< any of the cbPOLL_FLAG_* status - uint32_t extra; //!< Extra parameters depending on flags and mode - char appname[32]; //!< name of program to apply command specified by mode - char username[256]; //!< return your computername - uint32_t res[32]; //!< reserved for the future -} cbPKT_POLL; - - -// initiate impedance check -#define cbPKTTYPE_REPINITIMPEDANCE 0x66 -#define cbPKTTYPE_SETINITIMPEDANCE 0xE6 -#define cbPKTDLEN_INITIMPEDANCE ((sizeof(cbPKT_INITIMPEDANCE)/4) - cbPKT_HEADER_32SIZE) - -/// *Deprecated* Initiate impedance calculations -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t initiate; //!< on set call -> 1 to start autoimpedance - //!< on response -> 1 initiated -} cbPKT_INITIMPEDANCE; - -// file save configuration packet -#define cbPKTTYPE_REPMAPFILE 0x68 -#define cbPKTTYPE_SETMAPFILE 0xE8 -#define cbPKTDLEN_MAPFILE ((sizeof(cbPKT_MAPFILE)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xE8 Rep:0x68 - Map file -/// -/// Sets the mapfile for applications that use a mapfile so they all display similarly. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - char filename[512]; //!< filename of the mapfile to use -} cbPKT_MAPFILE; - - -//----------------------------------------------------- -///// Packets to tell me about the spike sorting model - -#define cbPKTTYPE_SS_MODELALLREP 0x50 /* NSP->PC response */ -#define cbPKTTYPE_SS_MODELALLSET 0xD0 /* PC->NSP request */ -#define cbPKTDLEN_SS_MODELALLSET ((sizeof(cbPKT_SS_MODELALLSET) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD0 Rep:0x50 - Get the spike sorting model for all channels (Histogram Peak Count) -/// -/// This packet says, "Give me all the model". In response, you will get a series of cbPKTTYPE_MODELREP -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - -} cbPKT_SS_MODELALLSET; - -// -#define cbPKTTYPE_SS_MODELREP 0x51 /* NSP->PC response */ -#define cbPKTTYPE_SS_MODELSET 0xD1 /* PC->NSP request */ -#define cbPKTDLEN_SS_MODELSET ((sizeof(cbPKT_SS_MODELSET) / 4) - cbPKT_HEADER_32SIZE) -#define MAX_REPEL_POINTS 3 - -/// @brief PKT Set:0xD1 Rep:0x51 - Get the spike sorting model for a single channel (Histogram Peak Count) -/// -/// The system replys with the model of a specific channel. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t chan; //!< actual channel id of the channel being configured (0 based) - uint32_t unit_number; //!< unit label (0 based, 0 is noise cluster) - uint32_t valid; //!< 1 = valid unit, 0 = not a unit, in other words just deleted when NSP -> PC - uint32_t inverted; //!< 0 = not inverted, 1 = inverted - - // Block statistics (change from block to block) - int32_t num_samples; //!< non-zero value means that the block stats are valid - float mu_x[2]; - float Sigma_x[2][2]; - float determinant_Sigma_x; - ///// Only needed if we are using a Bayesian classification model - float Sigma_x_inv[2][2]; - float log_determinant_Sigma_x; - ///// - float subcluster_spread_factor_numerator; - float subcluster_spread_factor_denominator; - float mu_e; - float sigma_e_squared; -} cbPKT_SS_MODELSET; - - -// This packet contains the options for the automatic spike sorting. -// -#define cbPKTTYPE_SS_DETECTREP 0x52 /* NSP->PC response */ -#define cbPKTTYPE_SS_DETECTSET 0xD2 /* PC->NSP request */ -#define cbPKTDLEN_SS_DETECT ((sizeof(cbPKT_SS_DETECT) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD2 Rep:0x52 - Auto threshold parameters -/// -/// Set the auto threshold parameters -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - float fThreshold; //!< current detection threshold - float fMultiplier; //!< multiplier -} cbPKT_SS_DETECT; - -// Options for artifact rejecting -// -#define cbPKTTYPE_SS_ARTIF_REJECTREP 0x53 /* NSP->PC response */ -#define cbPKTTYPE_SS_ARTIF_REJECTSET 0xD3 /* PC->NSP request */ -#define cbPKTDLEN_SS_ARTIF_REJECT ((sizeof(cbPKT_SS_ARTIF_REJECT) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD3 Rep:0x53 - Artifact reject -/// -/// Sets the artifact rejection parameters. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t nMaxSimulChans; //!< how many channels can fire exactly at the same time??? - uint32_t nRefractoryCount; //!< for how many samples (30 kHz) is a neuron refractory, so can't re-trigger -} cbPKT_SS_ARTIF_REJECT; - -// Options for noise boundary -// -#define cbPKTTYPE_SS_NOISE_BOUNDARYREP 0x54 /* NSP->PC response */ -#define cbPKTTYPE_SS_NOISE_BOUNDARYSET 0xD4 /* PC->NSP request */ -#define cbPKTDLEN_SS_NOISE_BOUNDARY ((sizeof(cbPKT_SS_NOISE_BOUNDARY) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD4 Rep:0x54 - Noise boundary -/// -/// Sets the noise boundary parameters -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t chan; //!< which channel we belong to - float afc[3]; //!< the center of the ellipsoid - float afS[3][3]; //!< an array of the axes for the ellipsoid -} cbPKT_SS_NOISE_BOUNDARY; - -// All sorting algorithms for which we can change settings -// (in cbPKT_SS_STATISTICS.nAutoalg) -#define cbAUTOALG_NONE 0 // No sorting -#define cbAUTOALG_SPREAD 1 // Auto spread -#define cbAUTOALG_HIST_CORR_MAJ 2 // Auto Hist Correlation -#define cbAUTOALG_HIST_PEAK_COUNT_MAJ 3 // Auto Hist Peak Maj -#define cbAUTOALG_HIST_PEAK_COUNT_FISH 4 // Auto Hist Peak Fish -#define cbAUTOALG_PCA 5 // Manual PCA -#define cbAUTOALG_HOOPS 6 // Manual Hoops -#define cbAUTOALG_PCA_KMEANS 7 // K-means PCA -#define cbAUTOALG_PCA_EM 8 // EM-clustering PCA -#define cbAUTOALG_PCA_DBSCAN 9 // DBSCAN PCA -// The commands to change sorting parameters and state -// (in cbPKT_SS_STATISTICS.nMode) -#define cbAUTOALG_MODE_SETTING 0 // Change the settings and leave sorting the same (PC->NSP request) -#define cbAUTOALG_MODE_APPLY 1 // Change settings and apply this sorting to all channels (PC->NSP request) -// This packet contains the settings for the spike sorting algorithms -// -#define cbPKTTYPE_SS_STATISTICSREP 0x55 /* NSP->PC response */ -#define cbPKTTYPE_SS_STATISTICSSET 0xD5 /* PC->NSP request */ -#define cbPKTDLEN_SS_STATISTICS ((sizeof(cbPKT_SS_STATISTICS) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD5 Rep:0x55 - Spike sourting statistics (Histogram peak count) -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t nUpdateSpikes; //!< update rate in spike counts - - uint32_t nAutoalg; //!< sorting algorithm (0=none 1=spread, 2=hist_corr_maj, 3=hist_peak_count_maj, 4=hist_peak_count_maj_fisher, 5=pca, 6=hoops) - uint32_t nMode; //!< cbAUTOALG_MODE_SETTING, - - float fMinClusterPairSpreadFactor; //!< larger number = more apt to combine 2 clusters into 1 - float fMaxSubclusterSpreadFactor; //!< larger number = less apt to split because of 2 clusers - - float fMinClusterHistCorrMajMeasure; //!< larger number = more apt to split 1 cluster into 2 - float fMaxClusterPairHistCorrMajMeasure; //!< larger number = less apt to combine 2 clusters into 1 - - float fClusterHistValleyPercentage; //!< larger number = less apt to split nearby clusters - float fClusterHistClosePeakPercentage; //!< larger number = less apt to split nearby clusters - float fClusterHistMinPeakPercentage; //!< larger number = less apt to split separated clusters - - uint32_t nWaveBasisSize; //!< number of wave to collect to calculate the basis, - //!< must be greater than spike length - uint32_t nWaveSampleSize; //!< number of samples sorted with the same basis before re-calculating the basis - //!< 0=manual re-calculation - //!< nWaveBasisSize * nWaveSampleSize is the number of waves/spikes to run against - //!< the same PCA basis before next -} cbPKT_SS_STATISTICS; - -// Send this packet to the NSP to tell it to reset all spike sorting to default values -#define cbPKTTYPE_SS_RESETREP 0x56 /* NSP->PC response */ -#define cbPKTTYPE_SS_RESETSET 0xD6 /* PC->NSP request */ -#define cbPKTDLEN_SS_RESET ((sizeof(cbPKT_SS_RESET) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD6 Rep:0x56 - Spike sorting reset -/// -/// Send this packet to the NSP to tell it to reset all spike sorting to default values -typedef struct -{ - cbPKT_HEADER cbpkt_header; //!< packet header -} cbPKT_SS_RESET; - -// This packet contains the status of the automatic spike sorting. -// -#define cbPKTTYPE_SS_STATUSREP 0x57 /* NSP->PC response */ -#define cbPKTTYPE_SS_STATUSSET 0xD7 /* PC->NSP request */ -#define cbPKTDLEN_SS_STATUS ((sizeof(cbPKT_SS_STATUS) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD7 Rep:0x57 - Spike sorting status (Histogram peak count) -/// -/// This packet contains the status of the automatic spike sorting. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - cbAdaptControl cntlUnitStats; //!< - cbAdaptControl cntlNumUnits; //!< -} cbPKT_SS_STATUS; - -// Send this packet to the NSP to tell it to reset all spike sorting models -#define cbPKTTYPE_SS_RESET_MODEL_REP 0x58 /* NSP->PC response */ -#define cbPKTTYPE_SS_RESET_MODEL_SET 0xD8 /* PC->NSP request */ -#define cbPKTDLEN_SS_RESET_MODEL ((sizeof(cbPKT_SS_RESET_MODEL) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD8 Rep:0x58 - Spike sorting reset model -/// -/// Send this packet to the NSP to tell it to reset all spike sorting models -typedef struct -{ - cbPKT_HEADER cbpkt_header; //!< packet header -} cbPKT_SS_RESET_MODEL; - -// Feature space commands and status changes (cbPKT_SS_RECALC.mode) -#define cbPCA_RECALC_START 0 // PC ->NSP start recalculation -#define cbPCA_RECALC_STOPPED 1 // NSP->PC finished recalculation -#define cbPCA_COLLECTION_STARTED 2 // NSP->PC waveform collection started -#define cbBASIS_CHANGE 3 // Change the basis of feature space -#define cbUNDO_BASIS_CHANGE 4 -#define cbREDO_BASIS_CHANGE 5 -#define cbINVALIDATE_BASIS 6 - -// Send this packet to the NSP to tell it to re calculate all PCA Basis Vectors and Values -#define cbPKTTYPE_SS_RECALCREP 0x59 /* NSP->PC response */ -#define cbPKTTYPE_SS_RECALCSET 0xD9 /* PC->NSP request */ -#define cbPKTDLEN_SS_RECALC ((sizeof(cbPKT_SS_RECALC) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xD9 Rep:0x59 - Spike Sorting recalculate PCA -/// -/// Send this packet to the NSP to tell it to re calculate all PCA Basis Vectors and Values -typedef struct -{ - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t chan; //!< 1 based channel we want to recalc (0 = All channels) - uint32_t mode; //!< cbPCA_RECALC_START -> Start PCa basis, cbPCA_RECALC_STOPPED-> PCA basis stopped, cbPCA_COLLECTION_STARTED -> PCA waveform collection started -} cbPKT_SS_RECALC; - -// This packet holds the calculated basis of the feature space from NSP to Central -// Or it has the previous basis retrieved and transmitted by central to NSP -#define cbPKTTYPE_FS_BASISREP 0x5B /* NSP->PC response */ -#define cbPKTTYPE_FS_BASISSET 0xDB /* PC->NSP request */ -#define cbPKTDLEN_FS_BASIS ((sizeof(cbPKT_FS_BASIS) / 4) - cbPKT_HEADER_32SIZE) -#define cbPKTDLEN_FS_BASISSHORT (cbPKTDLEN_FS_BASIS - ((sizeof(float)* cbMAX_PNTS * 3)/4)) - -/// @brief PKT Set:0xDB Rep:0x5B - Feature Space Basis -/// -/// This packet holds the calculated basis of the feature space from NSP to Central -/// Or it has the previous basis retrieved and transmitted by central to NSP -typedef struct -{ - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t chan; //!< 1-based channel number - uint32_t mode; //!< cbBASIS_CHANGE, cbUNDO_BASIS_CHANGE, cbREDO_BASIS_CHANGE, cbINVALIDATE_BASIS ... - uint32_t fs; //!< Feature space: cbAUTOALG_PCA - /// basis must be the last item in the structure because it can be variable length to a max of cbMAX_PNTS - float basis[cbMAX_PNTS][3]; //!< Room for all possible points collected -} cbPKT_FS_BASIS; - -// This packet holds the Line Noise Cancellation parameters -#define cbPKTTYPE_LNCREP 0x28 /* NSP->PC response */ -#define cbPKTTYPE_LNCSET 0xA8 /* PC->NSP request */ -#define cbPKTDLEN_LNC ((sizeof(cbPKT_LNC) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xA8 Rep:0x28 - Line Noise Cancellation -/// -/// This packet holds the Line Noise Cancellation parameters -typedef struct -{ - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t lncFreq; //!< Nominal line noise frequency to be canceled (in Hz) - uint32_t lncRefChan; //!< Reference channel for lnc synch (1-based) - uint32_t lncGlobalMode; //!< reserved -} cbPKT_LNC; - -// Send this packet to force the digital output to this value -#define cbPKTTYPE_SET_DOUTREP 0x5D /* NSP->PC response */ -#define cbPKTTYPE_SET_DOUTSET 0xDD /* PC->NSP request */ -#define cbPKTDLEN_SET_DOUT ((sizeof(cbPKT_SET_DOUT) / 4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xDD Rep:0x5D - Set Digital Output -/// -/// Allows setting the digital output value if not assigned set to monitor a channel or timed waveform or triggered -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint16_t chan; //!< which digital output channel (1 based, will equal chan from GetDoutCaps) - uint16_t value; //!< Which value to set? zero = 0; non-zero = 1 (output is 1 bit) -} cbPKT_SET_DOUT; - -#define cbMAX_WAVEFORM_PHASES ((cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE - 24) / 4) // Maximum number of phases in a waveform - -/// @brief Struct - Analog output waveform -/// -/// Contains the parameters to define a waveform for Analog Output channels -typedef struct -{ - int16_t offset; //!< DC offset - union { - struct { - uint16_t sineFrequency; //!< sine wave Hz - int16_t sineAmplitude; //!< sine amplitude - }; - struct { - uint16_t seq; //!< Wave sequence number (for file playback) - uint16_t seqTotal; //!< total number of sequences - uint16_t phases; //!< Number of valid phases in this wave (maximum is cbMAX_WAVEFORM_PHASES) - uint16_t duration[cbMAX_WAVEFORM_PHASES]; //!< array of durations for each phase - int16_t amplitude[cbMAX_WAVEFORM_PHASES]; //!< array of amplitude for each phase - }; - }; -} cbWaveformData; - -// signal generator waveform type -#define cbWAVEFORM_MODE_NONE 0 // waveform is disabled -#define cbWAVEFORM_MODE_PARAMETERS 1 // waveform is a repeated sequence -#define cbWAVEFORM_MODE_SINE 2 // waveform is a sinusoids -// signal generator waveform trigger type -#define cbWAVEFORM_TRIGGER_NONE 0 // instant software trigger -#define cbWAVEFORM_TRIGGER_DINPREG 1 // digital input rising edge trigger -#define cbWAVEFORM_TRIGGER_DINPFEG 2 // digital input falling edge trigger -#define cbWAVEFORM_TRIGGER_SPIKEUNIT 3 // spike unit -#define cbWAVEFORM_TRIGGER_COMMENTCOLOR 4 // comment RGBA color (A being big byte) -#define cbWAVEFORM_TRIGGER_RECORDINGSTART 5 // recording start trigger -#define cbWAVEFORM_TRIGGER_EXTENSION 6 // extension trigger -// AOUT signal generator waveform data -#define cbPKTTYPE_WAVEFORMREP 0x33 /* NSP->PC response */ -#define cbPKTTYPE_WAVEFORMSET 0xB3 /* PC->NSP request */ -#define cbPKTDLEN_WAVEFORM ((sizeof(cbPKT_AOUT_WAVEFORM)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xB3 Rep:0x33 - AOUT waveform -/// -/// This sets a user defined waveform for one or multiple Analog & Audio Output channels. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint16_t chan; //!< which analog output/audio output channel (1-based, will equal chan from GetDoutCaps) - - /// Each file may contain multiple sequences. - /// Each sequence consists of phases - /// Each phase is defined by amplitude and duration - - /// Waveform parameter information - uint16_t mode; //!< Can be any of cbWAVEFORM_MODE_* - uint32_t repeats; //!< Number of repeats (0 means forever) -#ifdef CBPROTO_311 - uint16_t trig; //!< Can be any of cbWAVEFORM_TRIGGER_* -#else - uint8_t trig; //!< Can be any of cbWAVEFORM_TRIGGER_* - uint8_t trigInst; //!< Instrument the trigChan belongs -#endif - uint16_t trigChan; //!< Depends on trig: - /// for cbWAVEFORM_TRIGGER_DINP* 1-based trigChan (1-16) is digin1, (17-32) is digin2, ... - /// for cbWAVEFORM_TRIGGER_SPIKEUNIT 1-based trigChan (1-156) is channel number - /// for cbWAVEFORM_TRIGGER_COMMENTCOLOR trigChan is A->B in A->B->G->R - uint16_t trigValue; //!< Trigger value (spike unit, G-R comment color, ...) - uint8_t trigNum; //!< trigger number (0-based) (can be up to cbMAX_AOUT_TRIGGER-1) - uint8_t active; //!< status of trigger - cbWaveformData wave; //!< Actual waveform data -} cbPKT_AOUT_WAVEFORM; - -// Stimulation data -#define cbPKTTYPE_STIMULATIONREP 0x34 /* NSP->PC response */ -#define cbPKTTYPE_STIMULATIONSET 0xB4 /* PC->NSP request */ -#define cbPKTDLEN_STIMULATION ((sizeof(cbPKT_STIMULATION)/4) - cbPKT_HEADER_32SIZE) - -/// @brief PKT Set:0xB4 Rep:0x34 - Stimulation command -/// -/// This sets a user defined stimulation for stim/record headstages -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint8_t commandBytes[40]; //!< series of bytes to control stimulation -} cbPKT_STIMULATION; - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Preview Data Packet Definitions (chid = 0x8000 + channel) -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - - -// preview information requests -#define cbPKTTYPE_PREVSETLNC 0x81 -#define cbPKTTYPE_PREVSETSTREAM 0x82 -#define cbPKTTYPE_PREVSET 0x83 - -#define cbPKTTYPE_PREVREP 0x03 // Acknowledged response from the packet above - - -// line noise cancellation (LNC) waveform preview packet -#define cbPKTTYPE_PREVREPLNC 0x01 -#define cbPKTDLEN_PREVREPLNC ((sizeof(cbPKT_LNCPREV)/4) - cbPKT_HEADER_32SIZE) - -/// @brief Preview packet - Line Noise preview -/// -/// Sends a preview of the line noise waveform. -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - uint32_t freq; //!< Estimated line noise frequency * 1000 (zero means not valid) - int16_t wave[300]; //!< lnc cancellation waveform (downsampled by 2) -} cbPKT_LNCPREV; - -// These values are taken by nSampleRows if PCA -#define cbPCA_START_COLLECTION 0 // start collecting samples -#define cbPCA_START_BASIS 1 // start basis calculation -#define cbPCA_MANUAL_LAST_SAMPLE 2 // the manual-only PCA, samples at zero, calculates PCA basis at 1 and stops at 2 -// the first time a basis is calculated it can be used, even for the waveforms collected for the next basis -#define cbSTREAMPREV_NONE 0x00000000 -#define cbSTREAMPREV_PCABASIS_NONEMPTY 0x00000001 -// Streams preview packet -#define cbPKTTYPE_PREVREPSTREAM 0x02 -#define cbPKTDLEN_PREVREPSTREAM ((sizeof(cbPKT_STREAMPREV)/4) - cbPKT_HEADER_32SIZE) - -/// @brief Preview packet -/// -/// sends preview of various data points. This is sent every 10ms -typedef struct { - cbPKT_HEADER cbpkt_header; //!< packet header - - int16_t rawmin; //!< minimum raw channel value over last preview period - int16_t rawmax; //!< maximum raw channel value over last preview period - int16_t smpmin; //!< minimum sample channel value over last preview period - int16_t smpmax; //!< maximum sample channel value over last preview period - int16_t spkmin; //!< minimum spike channel value over last preview period - int16_t spkmax; //!< maximum spike channel value over last preview period - uint32_t spkmos; //!< mean of squares - uint32_t eventflag; //!< flag to detail the units that happend in the last sample period - int16_t envmin; //!< minimum envelope channel value over the last preview period - int16_t envmax; //!< maximum envelope channel value over the last preview period - int32_t spkthrlevel; //!< preview of spike threshold level - uint32_t nWaveNum; //!< this tracks the number of waveforms collected in the WCM for each channel - uint32_t nSampleRows; //!< tracks number of sample vectors of waves - uint32_t nFlags; //!< cbSTREAMPREV_* -} cbPKT_STREAMPREV; - - - -#pragma pack(pop) - -#endif // end of include guard \ No newline at end of file diff --git a/include/cerelink/cbsdk.h b/include/cerelink/cbsdk.h deleted file mode 100644 index 0bf399d3..00000000 --- a/include/cerelink/cbsdk.h +++ /dev/null @@ -1,1024 +0,0 @@ -/* =STS=> cbsdk.h[4901].aa20 submit SMID:22 */ -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2010 - 2021 Blackrock Microsystems, LLC -// -// $Workfile: cbsdk.h $ -// $Archive: /Cerebus/Human/WindowsApps/cbmex/cbsdk.h $ -// $Revision: 1 $ -// $Date: 2/17/11 3:15p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// -// -// Notes: -// Only functions are exported, no data, and no classes -// No exception is thrown, every function returns an error code that should be consulted by caller -// Each instance will run one light thread that is responsible for networking, buffering and callbacks -// Additional threads may be created by Qt based on platforms -// Note for developers: -// Data structure in cbsdk should be decoupled from cbhlib as much as possible because cbhlib may change at any version -// for some functions it is easier to just rely on the data structure as defined in cbhwlib (e.g. callbacks and CCF), -// but this may change in future to give cbsdk a more stable and independent API -// -/** -* \file cbsdk.h -* \brief Cerebus SDK - This header file is distributed as part of the SDK. -*/ - -#ifndef CBSDK_H_INCLUDED -#define CBSDK_H_INCLUDED - -#include "cbhwlib.h" -#include "CCFUtils.h" - -#ifdef STATIC_CBSDK_LINK -#undef CBSDK_EXPORTS -#endif - -#ifdef WIN32 -// Windows shared library -#ifdef CBSDK_EXPORTS - #define CBSDKAPI __declspec(dllexport) -#elif ! defined(STATIC_CBSDK_LINK) - #define CBSDKAPI __declspec(dllimport) -#else - #define CBSDKAPI -#endif -#else -// Non Windows shared library -#ifdef CBSDK_EXPORTS - #define CBSDKAPI __attribute__ ((visibility ("default"))) -#else - #define CBSDKAPI -#endif -#endif - -/** -* \brief Library version information. -*/ -typedef struct cbSdkVersion -{ - // Library version - uint32_t major; - uint32_t minor; - uint32_t release; - uint32_t beta; - // Protocol version - uint32_t majorp; - uint32_t minorp; - // NSP version - uint32_t nspmajor; - uint32_t nspminor; - uint32_t nsprelease; - uint32_t nspbeta; - // NSP protocol version - uint32_t nspmajorp; - uint32_t nspminorp; -} cbSdkVersion; - -/// cbSdk return values -typedef enum cbSdkResult -{ - CBSDKRESULT_WARNCONVERT = 3, ///< If file conversion is needed - CBSDKRESULT_WARNCLOSED = 2, ///< Library is already closed - CBSDKRESULT_WARNOPEN = 1, ///< Library is already opened - CBSDKRESULT_SUCCESS = 0, ///< Successful operation - CBSDKRESULT_NOTIMPLEMENTED = -1, ///< Not implemented - CBSDKRESULT_UNKNOWN = -2, ///< Unknown error - CBSDKRESULT_INVALIDPARAM = -3, ///< Invalid parameter - CBSDKRESULT_CLOSED = -4, ///< Interface is closed cannot do this operation - CBSDKRESULT_OPEN = -5, ///< Interface is open cannot do this operation - CBSDKRESULT_NULLPTR = -6, ///< Null pointer - CBSDKRESULT_ERROPENCENTRAL = -7, ///< Unable to open Central interface - CBSDKRESULT_ERROPENUDP = -8, ///< Unable to open UDP interface (might happen if default) - CBSDKRESULT_ERROPENUDPPORT = -9, ///< Unable to open UDP port - CBSDKRESULT_ERRMEMORYTRIAL = -10, ///< Unable to allocate RAM for trial cache data - CBSDKRESULT_ERROPENUDPTHREAD = -11, ///< Unable to open UDP timer thread - CBSDKRESULT_ERROPENCENTRALTHREAD = -12, ///< Unable to open Central communication thread - CBSDKRESULT_INVALIDCHANNEL = -13, ///< Invalid channel number - CBSDKRESULT_INVALIDCOMMENT = -14, ///< Comment too long or invalid - CBSDKRESULT_INVALIDFILENAME = -15, ///< Filename too long or invalid - CBSDKRESULT_INVALIDCALLBACKTYPE = -16, ///< Invalid callback type - CBSDKRESULT_CALLBACKREGFAILED = -17, ///< Callback register/unregister failed - CBSDKRESULT_ERRCONFIG = -18, ///< Trying to run an unconfigured method - CBSDKRESULT_INVALIDTRACKABLE = -19, ///< Invalid trackable id, or trackable not present - CBSDKRESULT_INVALIDVIDEOSRC = -20, ///< Invalid video source id, or video source not present - CBSDKRESULT_ERROPENFILE = -21, ///< Cannot open file - CBSDKRESULT_ERRFORMATFILE = -22, ///< Wrong file format - CBSDKRESULT_OPTERRUDP = -23, ///< Socket option error (possibly permission issue) - CBSDKRESULT_MEMERRUDP = -24, ///< Socket memory assignment error - CBSDKRESULT_INVALIDINST = -25, ///< Invalid range or instrument address - CBSDKRESULT_ERRMEMORY = -26, ///< library memory allocation error - CBSDKRESULT_ERRINIT = -27, ///< Library initialization error - CBSDKRESULT_TIMEOUT = -28, ///< Conection timeout error - CBSDKRESULT_BUSY = -29, ///< Resource is busy - CBSDKRESULT_ERROFFLINE = -30, ///< Instrument is offline - CBSDKRESULT_INSTOUTDATED = -31, ///< The instrument runs an outdated protocol version - CBSDKRESULT_LIBOUTDATED = -32, ///< The library is outdated -} cbSdkResult; - -/// cbSdk Connection Type (Central, UDP, other) -typedef enum cbSdkConnectionType -{ - CBSDKCONNECTION_DEFAULT = 0, ///< Try Central then UDP - CBSDKCONNECTION_CENTRAL, ///< Use Central - CBSDKCONNECTION_UDP, ///< Use UDP - CBSDKCONNECTION_GEMININSP, ///< Connect to Gemini NSP - CBSDKCONNECTION_GEMINIHUB, ///< Connect to Gemini Hub - CBSDKCONNECTION_GEMINIHUB2, ///< Connect to a second Gemini Hub - CBSDKCONNECTION_GEMINIHUB3, ///< Connect to a third Gemini Hub - CBSDKCONNECTION_CLOSED, ///< Closed - CBSDKCONNECTION_COUNT ///< Allways the last value (Unknown) -} cbSdkConnectionType; - -/// Instrument Type -typedef enum cbSdkInstrumentType -{ - CBSDKINSTRUMENT_NSP = 0, ///< NSP - CBSDKINSTRUMENT_NPLAY, ///< Local nPlay - CBSDKINSTRUMENT_LOCALNSP, ///< Local NSP - CBSDKINSTRUMENT_REMOTENPLAY, ///< Remote nPlay - CBSDKINSTRUMENT_GEMININSP, ///< Gemini NSP - CBSDKINSTRUMENT_GEMINIHUB, ///< Gemini Hub - CBSDKINSTRUMENT_COUNT ///< Allways the last value (Invalid) -} cbSdkInstrumentType; - -/// CCF operation progress and status -typedef struct cbSdkCCFEvent -{ - cbStateCCF state; ///< CCF state - cbSdkResult result; ///< Last result - const char* szFileName; ///< CCF filename under operation - uint8_t progress; ///< Progress (in percent) -} cbSdkCCFEvent; - -/// Reason for packet loss -typedef enum cbSdkPktLostEventType -{ - CBSDKPKTLOSTEVENT_UNKNOWN = 0, ///< Unknown packet lost - CBSDKPKTLOSTEVENT_LINKFAILURE, ///< Link failure - CBSDKPKTLOSTEVENT_PC2NSP, ///< PC to NSP connection lost - CBSDKPKTLOSTEVENT_NET, ///< Network error -} cbSdkPktLostEventType; - -/// Packet lost event -typedef struct cbSdkPktLostEvent -{ - cbSdkPktLostEventType type; ///< packet lost event type -} cbSdkPktLostEvent; - -/// Instrument information -typedef struct cbSdkInstInfo -{ - uint32_t instInfo; ///< bitfield of cbINSTINFO_* (0 means closed) -} cbSdkInstInfo; - -/// Packet type information -typedef enum cbSdkPktType -{ - cbSdkPkt_PACKETLOST = 0, ///< will be received only by the first registered callback - ///< data points to cbSdkPktLostEvent - cbSdkPkt_INSTINFO, ///< data points to cbSdkInstInfo - cbSdkPkt_SPIKE, ///< data points to cbPKT_SPK - cbSdkPkt_DIGITAL, ///< data points to cbPKT_DINP - cbSdkPkt_SERIAL, ///< data points to cbPKT_DINP - cbSdkPkt_CONTINUOUS, ///< data points to cbPKT_GROUP - cbSdkPkt_TRACKING, ///< data points to cbPKT_VIDEOTRACK - cbSdkPkt_COMMENT, ///< data points to cbPKT_COMMENT - cbSdkPkt_GROUPINFO, ///< data points to cbPKT_GROUPINFO - cbSdkPkt_CHANINFO, ///< data points to cbPKT_CHANINFO - cbSdkPkt_FILECFG, ///< data points to cbPKT_FILECFG - cbSdkPkt_POLL, ///< data points to cbPKT_POLL - cbSdkPkt_SYNCH, ///< data points to cbPKT_VIDEOSYNCH - cbSdkPkt_NM, ///< data points to cbPKT_NM - cbSdkPkt_CCF, ///< data points to cbSdkCCFEvent - cbSdkPkt_IMPEDANCE, ///< data points to cbPKT_IMPEDANCE - cbSdkPkt_SYSHEARTBEAT, ///< data points to cbPKT_SYSHEARTBEAT - cbSdkPkt_LOG, ///< data points to cbPKT_LOG - cbSdkPkt_COUNT ///< Always the last value -} cbSdkPktType; - -/// Type of events to monitor -typedef enum cbSdkCallbackType -{ - CBSDKCALLBACK_ALL = 0, ///< Monitor all events - CBSDKCALLBACK_INSTINFO, ///< Monitor instrument connection information - CBSDKCALLBACK_SPIKE, ///< Monitor spike events - CBSDKCALLBACK_DIGITAL, ///< Monitor digital input events - CBSDKCALLBACK_SERIAL, ///< Monitor serial input events - CBSDKCALLBACK_CONTINUOUS, ///< Monitor continuous events - CBSDKCALLBACK_TRACKING, ///< Monitor video tracking events - CBSDKCALLBACK_COMMENT, ///< Monitor comment or custom events - CBSDKCALLBACK_GROUPINFO, ///< Monitor channel group info events - CBSDKCALLBACK_CHANINFO, ///< Monitor channel info events - CBSDKCALLBACK_FILECFG, ///< Monitor file config events - CBSDKCALLBACK_POLL, ///< respond to poll - CBSDKCALLBACK_SYNCH, ///< Monitor video synchronizarion events - CBSDKCALLBACK_NM, ///< Monitor NeuroMotive events - CBSDKCALLBACK_CCF, ///< Monitor CCF events - CBSDKCALLBACK_IMPEDANCE, ///< Monitor impedance events - CBSDKCALLBACK_SYSHEARTBEAT, ///< Monitor system heartbeats (100 times a second) - CBSDKCALLBACK_LOG, ///< Monitor system heartbeats (100 times a second) - CBSDKCALLBACK_COUNT ///< Always the last value -} cbSdkCallbackType; - -/// Trial type -typedef enum cbSdkTrialType -{ - CBSDKTRIAL_CONTINUOUS, - CBSDKTRIAL_EVENTS, - CBSDKTRIAL_COMMENTS, - CBSDKTRIAL_TRACKING, -} cbSdkTrialType; - -// Central uses them for their file name extensions (ns1, ns2, ..., ns5) -typedef enum DefaultSampleGroup { - SDK_SMPGRP_NONE = 0, - SDK_SMPGRP_RATE_500 = 1, - SDK_SMPGRP_RATE_1K = 2, - SDK_SMPGRP_RATE_2K = 3, - SDK_SMPGRP_RATE_10k = 4, - SDK_SMPGRP_RATE_30k = 5, - SDK_SMPGRP_RAW = 6 -} DefaultSampleGroup; - -/** Callback details. - * \n pEventData points to a cbPkt_* structure depending on the type - * \n pCallbackData is what is used to register the callback - */ -typedef void (* cbSdkCallback)(uint32_t nInstance, const cbSdkPktType type, const void* pEventData, void* pCallbackData); - - -/// The default number of continuous samples that will be stored per channel in the trial buffer -#define cbSdk_CONTINUOUS_DATA_SAMPLES 102400 // multiple of 4096 -/// The default number of events that will be stored per channel in the trial buffer -#define cbSdk_EVENT_DATA_SAMPLES (2 * 8192) // multiple of 4096 - -/// Maximum file size (in bytes) that is allowed to upload to NSP -#define cbSdk_MAX_UPOLOAD_SIZE (1024 * 1024 * 1024) - -/// \todo these should become functions as we may introduce different instruments -#define cbSdk_TICKS_PER_SECOND 30000.0 -/// The number of seconds corresponding to one cb clock tick -#define cbSdk_SECONDS_PER_TICK (1.0 / cbSdk_TICKS_PER_SECOND) - -/// Trial spike events -typedef struct cbSdkTrialEvent -{ - PROCTIME trial_start_time; ///< Trial start time (device timestamp when trial started) - uint32_t num_events; ///< Number of events in the arrays - PROCTIME* timestamps; ///< [num_events] - timestamps in chronological order - uint16_t* channels; ///< [num_events] - channel IDs (1-based) - uint16_t* units; ///< [num_events] - unit classification or digital data - void* waveforms; ///< [num_events][cbMAX_PNTS] - spike waveforms (optional, can be nullptr) -} cbSdkTrialEvent; - -/// Connection information -typedef struct cbSdkConnection -{ - cbSdkConnection() - { - nInPort = cbNET_UDP_PORT_BCAST; - nOutPort = cbNET_UDP_PORT_CNT; - #ifdef __APPLE__ - nRecBufSize = (6 * 1024 * 1024); // Despite setting kern.ipc.maxsockbuf=8388608, 8MB is still too much. - #else - nRecBufSize = (8 * 1024 * 1024); // 8MB default needed for best performance - #endif - szInIP = ""; - szOutIP = ""; - nRange = 0; - } - int nInPort; ///< Client port number - int nOutPort; ///< Instrument port number - int nRecBufSize; ///< Receive buffer size (0 to ignore altogether) - const char* szInIP; ///< Client IPv4 address - const char* szOutIP; ///< Instrument IPv4 address - int nRange; ///< Range of IP addresses to try to open -} cbSdkConnection; - -/// Trial continuous data -typedef struct cbSdkTrialCont -{ - PROCTIME trial_start_time; ///< Trial start time (device timestamp when trial started) - uint8_t group; ///< Sample group to retrieve (0-7, set by user before Init) - uint16_t count; ///< Number of valid channels in this group (output from Init) - uint16_t chan[cbNUM_ANALOG_CHANS]; ///< Channel numbers (1-based, output from Init) - uint16_t sample_rate; ///< Sample rate for this group (Hz, output from Init) - uint32_t num_samples; ///< Number of samples: input = max to read, output = actual read - int16_t * samples; ///< Pointer to contiguous [num_samples][count] array - PROCTIME * timestamps; ///< Pointer to array of [num_samples] timestamps -} cbSdkTrialCont; - -/// Trial comment data -typedef struct cbSdkTrialComment -{ - PROCTIME trial_start_time; ///< Trial start time (device timestamp when trial started) - uint16_t num_samples; ///< Number of comments - uint8_t * charsets; ///< Buffer to hold character sets - uint32_t * rgbas; ///< Buffer to hold rgba values (actually tbgr) - uint8_t * * comments; ///< Pointer to comments - PROCTIME * timestamps; ///< Buffer to hold time stamps -} cbSdkTrialComment; - -/// Trial video tracking data -typedef struct cbSdkTrialTracking -{ - PROCTIME trial_start_time; ///< Trial start time (device timestamp when trial started) - uint16_t count; ///< Number of valid trackable objects (up to cbMAXTRACKOBJ) - uint16_t ids[cbMAXTRACKOBJ]; ///< Node IDs (holds count elements) - uint16_t max_point_counts[cbMAXTRACKOBJ]; ///< Maximum point counts (holds count elements) - uint16_t types[cbMAXTRACKOBJ]; ///< Node types (can be cbTRACKOBJ_TYPE_* and determines coordinate counts) (holds count elements) - uint8_t names[cbMAXTRACKOBJ][cbLEN_STR_LABEL + 1]; ///< Node names (holds count elements) - uint16_t num_samples[cbMAXTRACKOBJ]; ///< Number of samples - uint16_t * point_counts[cbMAXTRACKOBJ]; ///< Buffer to hold number of valid points (up to max_point_counts) (holds count*num_samples elements) - void * * coords[cbMAXTRACKOBJ] ; ///< Buffer to hold tracking points (holds count*num_samples tarackables, each of max_point_counts points - uint32_t * synch_frame_numbers[cbMAXTRACKOBJ];///< Buffer to hold synch frame numbers (holds count*num_samples elements) - uint32_t * synch_timestamps[cbMAXTRACKOBJ]; ///< Buffer to hold synchronized tracking time stamps (in milliseconds) (holds count*num_samples elements) - PROCTIME * timestamps[cbMAXTRACKOBJ]; ///< Buffer to hold tracking time stamps (holds count*num_samples elements) -} cbSdkTrialTracking; - -/// Output waveform type -typedef enum cbSdkWaveformType -{ - cbSdkWaveform_NONE = 0, - cbSdkWaveform_PARAMETERS, ///< Parameters - cbSdkWaveform_SINE, ///< Sinusoid - cbSdkWaveform_COUNT, ///< Always the last value -} cbSdkWaveformType; - -/// Trigger type -typedef enum cbSdkWaveformTriggerType -{ - cbSdkWaveformTrigger_NONE = 0, ///< Instant software trigger - cbSdkWaveformTrigger_DINPREG, ///< Digital input rising edge trigger - cbSdkWaveformTrigger_DINPFEG, ///< Digital input falling edge trigger - cbSdkWaveformTrigger_SPIKEUNIT, ///< Spike unit - cbSdkWaveformTrigger_COMMENTCOLOR, ///< Custom colored event (e.g. NeuroMotive event) - cbSdkWaveformTrigger_SOFTRESET, ///< Soft-reset trigger (e.g. file recording start) - cbSdkWaveformTrigger_EXTENSION, ///< Extension trigger - cbSdkWaveformTrigger_COUNT, ///< Always the last value -} cbSdkWaveformTriggerType; - -/// Extended pointer-form of cbWaveformData -typedef struct cbSdkWaveformData -{ - cbSdkWaveformType type; - uint32_t repeats; - cbSdkWaveformTriggerType trig; - uint16_t trigChan; - uint16_t trigValue; - uint8_t trigNum; - int16_t offset; - union { - struct { - uint16_t sineFrequency; - int16_t sineAmplitude; - }; - struct { - uint16_t phases; - uint16_t * duration; - int16_t * amplitude; - }; - }; -} cbSdkWaveformData; - -/// Analog output monitor -typedef struct cbSdkAoutMon -{ - uint16_t chan; ///< (1-based) channel to monitor - bool bTrack; ///< If true then monitor last tracked channel - bool bSpike; ///< If spike or continuous should be monitored -} cbSdkAoutMon; - -/// CCF data -typedef struct cbSdkCCF -{ - int ccfver; ///< CCF internal version - cbCCF data; -} cbSdkCCF; - -typedef enum cbSdkSystemType -{ - cbSdkSystem_RESET = 0, - cbSdkSystem_SHUTDOWN, - cbSdkSystem_STANDBY, -} cbSdkSystemType; - -/// Extension command type -typedef enum cbSdkExtCmdType -{ - cbSdkExtCmd_RPC = 0, // RPC command - cbSdkExtCmd_UPLOAD, // Upload the file - cbSdkExtCmd_TERMINATE, // Signal last RPC command to terminate - cbSdkExtCmd_INPUT, // Input to RPC command - cbSdkExtCmd_END_PLUGIN, // Signal to end plugin - cbSdkExtCmd_NSP_REBOOT, // Restart the NSP - cbSdkExtCmd_PLUGINFO, // Get plugin info -} cbSdkExtCmdType; - -/// Extension command -typedef struct cbSdkExtCmd -{ - cbSdkExtCmdType cmd; - char szCmd[cbMAX_LOG]; -} cbSdkExtCmd; - - -extern "C" -{ -/** - * @brief Retrieve SDK library version and, if connected, instrument protocol/firmware versions. - * - * This may be called before cbSdkOpen(); in that case only the library fields are populated and - * a CBSDKRESULT_WARNCLOSED may be returned to indicate no active instrument connection. When the - * instance is open the structure is filled with both library and instrument (NSP) version info. - * @param nInstance SDK instance index - * @param version Pointer to cbSdkVersion structure to populate - * @return cbSdkResult Success or warning/error (CBSDKRESULT_NULLPTR if version is NULL) - */ -CBSDKAPI cbSdkResult cbSdkGetVersion(uint32_t nInstance, cbSdkVersion * version); - -/** - * @brief Read CCF configuration from file or NSP. - * - * If a filename is provided, reads from file (library can be closed, but a warning is returned). If nullptr, reads from NSP (library must be open). - * Optionally converts, sends, or runs in a separate thread. Progress callback should be registered beforehand if needed. - * @param nInstance SDK instance number - * @param pData Pointer to cbSdkCCF structure to receive data - * pData->data CCF data will be copied here - * pData->ccfver the internal version of the original data - * @param szFileName CCF filename to read (or nullptr to read from NSP) - * @param bConvert Allow conversion if needed - * @param bSend Send CCF after successful read - * @param bThreaded Run operation in a separate thread - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkReadCCF(uint32_t nInstance, cbSdkCCF * pData, const char * szFileName, bool bConvert, bool bSend = false, bool bThreaded = false); - -/** - * @brief Write CCF configuration to file or send to NSP. - * - * If a filename is provided, writes to file. - * If nullptr, sends CCF to NSP (library must be open). - * Optionally runs in a separate thread. - * Progress callback should be registered beforehand if needed. - * @param nInstance SDK instance number - * @param pData Pointer to cbSdkCCF structure with data to write - * pData->data CCF data to use - * pData->ccfver Last internal version used - * @param szFileName CCF filename to write (or nullptr to send to NSP) - * @param bThreaded Run operation in a separate thread - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkWriteCCF(uint32_t nInstance, cbSdkCCF * pData, const char * szFileName, bool bThreaded = false); - -/** - * @brief Open (initialize) an SDK instance connection to an instrument. - * - * Allocates internal structures, starts background network / processing threads, and attempts to - * establish communication using the requested connection type. If CBSDKCONNECTION_DEFAULT is used the - * library will first try a Central (shared memory / service) connection and fall back to UDP on failure. - * Calling cbSdkOpen() on an already open instance returns CBSDKRESULT_WARNOPEN and leaves the existing - * connection intact. Once open you may register callbacks and configure trials. - * @note No exceptions are thrown; all failures are reported via the returned cbSdkResult. - * @param nInstance Zero-based instance index (0 .. cbMAXOPEN-1) - * @param conType Desired connection strategy (DEFAULT, CENTRAL, UDP) - * @param con Connection parameters (ports, IPs, receive buffer size, IP range). A default-constructed - * cbSdkConnection supplies sensible defaults. - * @return cbSdkResult Success (CBSDKRESULT_SUCCESS or CBSDKRESULT_WARNOPEN) or error such as - * CBSDKRESULT_ERROPENCENTRAL, CBSDKRESULT_ERROPENUDP, CBSDKRESULT_TIMEOUT, CBSDKRESULT_INVALIDINST. - */ -CBSDKAPI cbSdkResult cbSdkOpen(uint32_t nInstance, - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT, - const cbSdkConnection &con = cbSdkConnection()); - -/** - * @brief Get the current connection and instrument type for a given instance. - * - * @param nInstance SDK instance number - * @param conType Pointer to receive the connection type - * @param instType Pointer to receive the instrument type - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkGetType(uint32_t nInstance, cbSdkConnectionType * conType, cbSdkInstrumentType * instType); - -/** - * @brief Close the SDK library for a given instance. - * - * Releases resources and unregisters callbacks. Safe to call multiple times. - * @param nInstance SDK instance number - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkClose(uint32_t nInstance); - -/** - * @brief Query the current instrument sample clock time. - * - * Returns the current 30 kHz (or system-configured) tick count from the instrument. The value can - * be converted to seconds using cbSdk_SECONDS_PER_TICK. Requires an open connection. - * @param nInstance SDK instance index - * @param cbtime Pointer to PROCTIME variable to receive current tick value - * @return cbSdkResult Success or error (CBSDKRESULT_CLOSED if instance not open, CBSDKRESULT_NULLPTR if cbtime null) - */ -CBSDKAPI cbSdkResult cbSdkGetTime(uint32_t nInstance, PROCTIME * cbtime); - -/** - * @brief Get direct access to the internal spike cache shared memory for a channel. - * - * Note: The spike cache is volatile and should not be used for critical operations such as recording. - * @param nInstance SDK instance number - * @param channel Channel number (1-based) - * @param cache Pointer to receive the spike cache pointer - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkGetSpkCache(uint32_t nInstance, uint16_t channel, cbSPKCACHE **cache); - -/** - * @brief Get the current trial setup configuration. - * - * Retrieves the configuration for trial data collection, including active channels and event settings. - * Refer to cbSdkSetTrialConfig() for parameter descriptions. - * @param nInstance SDK instance number - * @param pbActive Pointer to receive trial active status - * @param pBegchan Pointer to receive trial begin channel (optional) - * @param pBegmask Pointer to receive trial begin mask (optional) - * @param pBegval Pointer to receive trial begin value (optional) - * @param pEndchan Pointer to receive trial end channel (optional) - * @param pEndmask Pointer to receive trial end mask (optional) - * @param pEndval Pointer to receive trial end value (optional) - * @param puWaveforms Pointer to receive waveform count (optional) - * @param puConts Pointer to receive continuous sample count (optional) - * @param puEvents Pointer to receive event sample count (optional) - * @param puComments Pointer to receive comment sample count (optional) - * @param puTrackings Pointer to receive tracking sample count (optional) - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkGetTrialConfig(uint32_t nInstance, - uint32_t * pbActive, uint16_t * pBegchan = nullptr, uint32_t * pBegmask = nullptr, uint32_t * pBegval = nullptr, - uint16_t * pEndchan = nullptr, uint32_t * pEndmask = nullptr, uint32_t * pEndval = nullptr, - uint32_t * puWaveforms = nullptr, uint32_t * puConts = nullptr, uint32_t * puEvents = nullptr, - uint32_t * puComments = nullptr, uint32_t * puTrackings = nullptr); - -/** - * @brief Set the trial setup configuration for data collection. - * Configures the trial parameters, including channel range, event settings, and buffer sizes. - * A 'trial' is a time period during which data is collected and stored in memory buffers for later retrieval. - * It can be started and stopped based on specific channel events or manually. - * Each data type (continuous, event, comment, tracking) can be enabled or disabled independently. - * All timestamps are stored in absolute device time. - * - * @param nInstance SDK instance number - * @param bActive Enable or disable trial. - * If false, then any active trial is ended and the system will monitor for triggers if configured. - * If true and already in a trial then do nothing. - * If true and not already in a trial: Resets trial start time to now, write_index and write_start_index are reset to `0`. - * @param begchan Channel ID that is watched for the trial begin notification (1-based, 0 for all) - * @param begmask Mask ANDed with channel data to check for trial beginning - * @param begval Value the masked data is compared to identify trial beginning - * @param endchan Channel ID that is watched for the trial end notification (1-based, 0 for all) - * @param endmask Mask ANDed with channel data to check for trial end - * @param endval Value the masked data is compared to identify trial end - * @param uWaveforms Number of waveforms - * @param uConts Number of continuous samples - * @param uEvents Number of event samples - * @param uComments Number of comment samples - * @param uTrackings Number of tracking samples - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkSetTrialConfig(uint32_t nInstance, - uint32_t bActive, uint16_t begchan = 0, uint32_t begmask = 0, uint32_t begval = 0, - uint16_t endchan = 0, uint32_t endmask = 0, uint32_t endval = 0, - uint32_t uWaveforms = 0, uint32_t uConts = cbSdk_CONTINUOUS_DATA_SAMPLES, uint32_t uEvents = cbSdk_EVENT_DATA_SAMPLES, - uint32_t uComments = 0, uint32_t uTrackings = 0); - -/** - * @brief Close the given trial if configured. - * - * Closes the specified trial type and releases associated resources. - * @param nInstance SDK instance number - * @param type Trial type to close - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkUnsetTrialConfig(uint32_t nInstance, cbSdkTrialType type); - -/** - * @brief Retrieve label and metadata for a channel. - * - * The caller can pass nullptr for any optional output parameter. If label is supplied it must have - * capacity of at least cbLEN_STR_LABEL bytes. The bValid array (length 6) returns per-field validity flags: - * [0]=label, [1]=userflags, [2]=position(XYZ), etc. (See implementation for exact mapping.) - * @param nInstance SDK instance index - * @param channel 1-based channel number - * @param bValid Output array of validity flags (length >= 6) - * @param label Optional buffer to receive ASCII label (cbLEN_STR_LABEL) - * @param userflags Optional user flag value - * @param position Optional array of 4 int32_t values (implementation-defined meaning) - * @return cbSdkResult Success or error code (CBSDKRESULT_INVALIDCHANNEL on bad channel) - */ -CBSDKAPI cbSdkResult cbSdkGetChannelLabel(uint32_t nInstance, uint16_t channel, uint32_t * bValid, char * label = nullptr, uint32_t * userflags = nullptr, int32_t * position = nullptr); - -/** - * @brief Set label and metadata for a channel. - * @param nInstance SDK instance index - * @param channel 1-based channel number - * @param label Null-terminated ASCII label (truncated internally if longer than cbLEN_STR_LABEL-1) - * @param userflags Application-defined 32-bit flags stored with the channel - * @param position Optional pointer to 4-element int32_t array (e.g. XYZ + reserved) or nullptr to leave unchanged - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetChannelLabel(uint32_t nInstance, uint16_t channel, const char * label, uint32_t userflags, const int32_t * position); // Set channel label - -/* Get channel capabilities */ -// CBSDKAPI cbSdkResult cbSdkIsChanAnalogIn(uint32_t nInstance, uint16_t channel, uint32_t* bResult); -// CBSDKAPI cbSdkResult cbSdkIsChanFEAnalogIn(uint32_t nInstance, uint16_t channel, uint32_t* bResult); -// CBSDKAPI cbSdkResult cbSdkIsChanAIAnalogIn(uint32_t nInstance, uint16_t channel, uint32_t* bResult); - -/** - * @brief Determine if channel is any digital input (DINP or serial). - * @param nInstance SDK instance number - * @param channel Channel number (1-based) - * @param bResult Returns non-zero if channel is digital input - */ -CBSDKAPI cbSdkResult cbSdkIsChanAnyDigIn(uint32_t nInstance, uint16_t channel, uint32_t* bResult); - -/** - * @brief Determine if channel is a serial channel. - * @param nInstance SDK instance number - * @param channel Channel number (1-based) - * @param bResult Returns non-zero if channel is serial input - */ -CBSDKAPI cbSdkResult cbSdkIsChanSerial(uint32_t nInstance, uint16_t channel, uint32_t* bResult); -// CBSDKAPI cbSdkResult cbSdkIsChanDigin(uint32_t nInstance, uint16_t channel, uint32_t* bValid); -// CBSDKAPI cbSdkResult cbSdkIsChanDigout(uint32_t nInstance, uint16_t channel, uint32_t* bResult); -// CBSDKAPI cbSdkResult cbSdkIsChanAnalogOut(uint32_t nInstance, uint16_t channel, uint32_t* bResult); -// CBSDKAPI cbSdkResult cbSdkIsChanAudioOut(uint32_t nInstance, uint16_t channel, uint32_t* bResult); -// CBSDKAPI cbSdkResult cbSdkIsChanCont(uint32_t nInstance, uint16_t channel, uint32_t* bResult); - -/** - * @brief Retrieve buffered trial data for configured modalities (spike/events, continuous, comments, tracking). - * - * For each non-null structure pointer, fills counts and copies/points to the samples accumulated since the last - * cbSdkInitTrialData() call (or since activation if never initialized). The caller must have allocated the member - * buffers (timestamps, waveforms, etc.) sized according to the configuration determined by cbSdkInitTrialData(). - * If bActive is non-zero the internal read indices advance (consuming the data). If zero, the call is a - * non-destructive peek and data can be fetched again. - * Thread-safety: Do not change trial configuration concurrently. Internally the library synchronizes access across - * producer threads, but external synchronization is recommended for multi-threaded consumers. - * @param nInstance SDK instance index - * @param bSeek Non-zero to advance (consume) read indices; zero to peek only and leave indices unchanged - * @param trialevent Pointer to cbSdkTrialEvent (or nullptr to skip spike/event retrieval) - * in: trialevent->num_samples requested number of event samples - * out: trialevent->num_samples retrieved number of events - * out: trialevent->timestamps timestamps for events - * out: trialevent->waveforms waveform or digital data - * @param trialcont Pointer to cbSdkTrialCont (or nullptr to skip continuous retrieval) - * in: trialcont->group sample group to retrieve (0-7, must match Init call) - * in: trialcont->num_samples max number of samples to read - * in: trialcont->samples pointer to pre-allocated [num_samples][count] array - * in: trialcont->timestamps pointer to pre-allocated [num_samples] timestamp array - * out: trialcont->num_samples actual number of samples retrieved - * out: trialcont->samples continuous samples in [sample][channel] layout - * out: trialcont->timestamps timestamp for each sample - * @param trialcomment Pointer to cbSdkTrialComment (or nullptr to skip comment retrieval) - * in: trialcomment->num_samples requested number of comment samples - * out: trialcomment->num_samples retrieved number of comments samples - * out: trialcomment->timestamps timestamps for comments - * out: trialcomment->rgbas rgba for comments - * out: trialcomment->charsets character set for comments - * out: trialcomment->comments pointer to the comments - * @param trialtracking Pointer to cbSdkTrialTracking (or nullptr to skip tracking retrieval) - * in: trialtracking->num_samples requested number of tracking samples - * out: trialtracking->num_samples retrieved number of tracking samples - * out: trialtracking->synch_frame_numbers retrieved frame numbers - * out: trialtracking->synch_timestamps retrieved synchronized timesamps - * out: trialtracking->timestamps timestamps for tracking - * out: trialtracking->coords tracking coordinates - * @return cbSdkResult Success or error (CBSDKRESULT_ERRCONFIG if trial not configured, CBSDKRESULT_INVALIDPARAM on bad pointers) - */ -CBSDKAPI cbSdkResult cbSdkGetTrialData(uint32_t nInstance, - uint32_t bSeek, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking); - -/** - * @brief Initialize user-provided trial data structures and prime buffer pointers. - * - * Populates channel lists, waveform/comment pointer arrays, and establishes the snapshot starting point - * for subsequent cbSdkGetTrialData() calls. Also waits (up to wait_for_comment_msec) for at least one - * comment if comment buffering is configured and bActive is non-zero. - * Call before data retrieval (cbSdkGetTrialData) to fill sample counts. - * Note: No allocation is performed here, buffer pointers must be set to appropriate allocated buffers after a call to this function. - * @param nInstance SDK instance index - * @param bResetClock Non-zero to reset internal trial start time to 'now'. - * Internal trial start time serves 2 functions: - * 1) If we are waiting for a comment, it is the time after which we will accept a comment to trigger the end of the wait. - * 2) Its previous value is used as the zero-time reference for relative timestamps if absolute time is not configured. - * @param trialevent Event trial structure to initialize or nullptr - * @param trialcont Continuous trial structure to initialize or nullptr - * @param trialcomment Comment trial structure to initialize or nullptr - * @param trialtracking Tracking trial structure to initialize or nullptr - * @param wait_for_comment_msec Milliseconds to wait for a first comment (default 250) - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkInitTrialData(uint32_t nInstance, uint32_t bResetClock, - cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking, unsigned long wait_for_comment_msec = 250); - -/** - * @brief Control file recording on the instrument. - * - * Depending on options and bStart this can: start a new recording, stop current, or update metadata. - * Passing filename or comment as nullptr leaves that field unchanged (unless starting recording where - * a filename may be required). See cbFILECFG_OPT_* for options (e.g. append, split behavior). - * @param nInstance SDK instance index - * @param filename Recording file base name (without extension) or nullptr - * @param comment Optional comment/annotation string or nullptr - * @param bStart Non-zero to start (or continue) recording, zero to stop - * @param options Bitfield of cbFILECFG_OPT_* flags - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetFileConfig(uint32_t nInstance, const char * filename, const char * comment, uint32_t bStart, uint32_t options = cbFILECFG_OPT_NONE); - -/** - * @brief Query current file recording state. - * @param nInstance SDK instance index - * @param filename Optional output buffer receiving current file name (cbLEN_STR_FILE) or nullptr - * @param username Optional output buffer receiving username (cbLEN_STR_USER) or nullptr - * @param pbRecording Output flag set non-zero if actively recording - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkGetFileConfig(uint32_t nInstance, char * filename, char * username, bool * pbRecording); - -/** - * @brief Set patient information (demographics) on the NSP. - * @param nInstance SDK instance number - * @param ID Patient ID string - * @param firstname First name - * @param lastname Last name - * @param DOBMonth Month of birth (1-12) - * @param DOBDay Day of birth (1-31) - * @param DOBYear Year of birth (4-digit) - */ -CBSDKAPI cbSdkResult cbSdkSetPatientInfo(uint32_t nInstance, const char * ID, const char * firstname, const char * lastname, uint32_t DOBMonth, uint32_t DOBDay, uint32_t DOBYear); - -/** - * @brief Begin an impedance measurement sequence. - * Initiates impedance testing; progress delivered via impedance callback events. - * @param nInstance SDK instance number - */ -CBSDKAPI cbSdkResult cbSdkInitiateImpedance(uint32_t nInstance); - -/*! This sends an arbitrary packet without any validation. Please use with care or it might break the system */ -//CBSDKAPI cbSdkResult cbSdkSendPacket(uint32_t nInstance, void * ppckt); - -/** - * @brief Set instrument run level and optional lock/reset queue. - * @param nInstance SDK instance number - * @param runlevel Target run level (cbRUNLEVEL_*) - * @param locked Non-zero to lock run level - * @param resetque Non-zero to clear pending resets - */ -CBSDKAPI cbSdkResult cbSdkSetSystemRunLevel(uint32_t nInstance, uint32_t runlevel, uint32_t locked, uint32_t resetque); - -/** - * @brief Get instrument run level state. - * @param nInstance SDK instance number - * @param runlevel Current run level - * @param runflags Optional run flags - * @param resetque Optional reset queue state - */ -CBSDKAPI cbSdkResult cbSdkGetSystemRunLevel(uint32_t nInstance, uint32_t * runlevel, uint32_t * runflags = nullptr, uint32_t * resetque = nullptr); - -/** - * @brief Set (pulse or latch) a digital output line. - * Behavior depends on instrument firmware configuration; value usually 0/1. - * @param nInstance SDK instance index - * @param channel 1-based digital output channel - * @param value Output value (bit pattern or level) - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetDigitalOutput(uint32_t nInstance, uint16_t channel, uint16_t value); - -/** - * @brief Generate a synchronization pulse train on a sync output channel. - * @param nInstance SDK instance index - * @param channel 1-based sync channel - * @param nFreq Pulse frequency (Hz) or implementation-defined units - * @param nRepeats Number of pulses (0 may mean continuous depending on firmware) - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetSynchOutput(uint32_t nInstance, uint16_t channel, uint32_t nFreq, uint32_t nRepeats); - -/** - * @brief Issue an extension / plugin command to the NSP. - * @param nInstance SDK instance index - * @param extCmd Pointer to command structure (cmd + payload string) - * @return cbSdkResult Success or error code (CBSDKRESULT_INVALIDPARAM if extCmd null) - */ -CBSDKAPI cbSdkResult cbSdkExtDoCommand(uint32_t nInstance, const cbSdkExtCmd * extCmd); - -/** - * @brief Configure analog output channel for waveform generation or monitoring. - * - * Provide either wf (waveform definition) or mon (monitor specification). Passing both nullptr disables AO. - * @param nInstance SDK instance index - * @param channel 1-based analog output channel - * @param wf Waveform description (type, repeats, trigger, parameters) or nullptr - * @param mon Monitor specification (track spike/continuous) or nullptr - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetAnalogOutput(uint32_t nInstance, uint16_t channel, const cbSdkWaveformData * wf, const cbSdkAoutMon * mon); - -/** - * @brief Enable or disable channel activity for trial buffering and callbacks. - * @param nInstance SDK instance index - * @param channel 1-based channel number (0 applies to all) - * @param bActive Non-zero to enable, zero to disable - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetChannelMask(uint32_t nInstance, uint16_t channel, uint32_t bActive); - -/** - * @brief Inject a comment/custom event packet. - * @param nInstance SDK instance index - * @param t_bgr Color encoded as (A<<24)|(B<<16)|(G<<8)|R (A=alpha) - * @param charset Character set ID (e.g. 0=ANSI) - * @param comment UTF-8/ASCII string (nullptr sends a color-only marker) - * @return cbSdkResult Success or error code (CBSDKRESULT_INVALIDCOMMENT if too long) - */ -CBSDKAPI cbSdkResult cbSdkSetComment(uint32_t nInstance, uint32_t t_bgr, uint8_t charset, const char * comment = nullptr); - -/** - * @brief Apply a full channel configuration. - * @param nInstance SDK instance index - * @param channel 1-based channel to configure - * @param chaninfo Pointer to populated cbPKT_CHANINFO structure - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetChannelConfig(uint32_t nInstance, uint16_t channel, cbPKT_CHANINFO * chaninfo); - -/** - * @brief Retrieve current full channel configuration. - * @param nInstance SDK instance index - * @param channel 1-based channel - * @param chaninfo Output pointer to cbPKT_CHANINFO structure to fill - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkGetChannelConfig(uint32_t nInstance, uint16_t channel, cbPKT_CHANINFO * chaninfo); - -/** - * @brief Obtain filter description metadata. - * @param nInstance SDK instance index - * @param proc Processor index (1-based) - * @param filt Filter identifier - * @param filtdesc Output pointer to cbFILTDESC structure - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkGetFilterDesc(uint32_t nInstance, uint32_t proc, uint32_t filt, cbFILTDESC * filtdesc); - -/** - * @brief Retrieve channel list for a sample (continuous data) group. - * @param nInstance SDK instance index - * @param proc Processor index (1-based) - * @param group Sample group identifier - * @param length Input: capacity of list array; Output: number of channels returned - * @param list Caller-allocated array of uint16_t channel numbers (size >= *length) - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkGetSampleGroupList(uint32_t nInstance, uint32_t proc, uint32_t group, uint32_t *length, uint16_t *list); - -/** - * @brief Get sample group information. - * @param nInstance SDK instance number - * @param proc Processor index (1-based) - * @param group Sample group id - * @param label Optional buffer to receive label (cbLEN_STR_LABEL) - * @param period Sample period (ticks) - * @param length Number of channels in group - */ -CBSDKAPI cbSdkResult cbSdkGetSampleGroupInfo(uint32_t nInstance, uint32_t proc, uint32_t group, char *label, uint32_t *period, uint32_t *length); - -/** - * @brief Assign an analog input channel to a sample group and select filter. - * @param nInstance SDK instance index - * @param chan 1-based channel number - * @param filter Filter identifier (0 = none or depends on firmware) - * @param group Sample group ID (see SDK_SMPGRP_RATE_* constants) - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetAinpSampling(uint32_t nInstance, uint32_t chan, uint32_t filter, uint32_t group); - -/** - * @brief Configure per-channel spike extraction / sorting options. - * @param nInstance SDK instance index - * @param chan 1-based channel number - * @param flags Bitfield of spike option flags - * @param filter Spike filter identifier - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetAinpSpikeOptions(uint32_t nInstance, uint32_t chan, uint32_t flags, uint32_t filter); - -/** - * @brief Retrieve metadata for a tracking (video) object. - * - * Provides information about a trackable (NeuroMotive node) specified by its 1-based ID. Any of the - * optional output pointers (name, type, pointCount) may be nullptr to skip that field. The name buffer, - * if provided, must have capacity of at least cbLEN_STR_LABEL bytes. On invalid or unavailable ID the - * function returns CBSDKRESULT_INVALIDTRACKABLE. - * @param nInstance SDK instance index - * @param name Optional output buffer (size >= cbLEN_STR_LABEL) to receive the object's name/label - * @param type Optional output pointer receiving the object's type (cbTRACKOBJ_TYPE_*) - * @param pointCount Optional output pointer receiving the object's maximum point count - * @param id 1-based trackable object ID (range: 1 .. cbMAXTRACKOBJ) - * @return cbSdkResult Success or error code (CBSDKRESULT_INVALIDTRACKABLE if id is invalid or absent) - */ -CBSDKAPI cbSdkResult cbSdkGetTrackObj(uint32_t nInstance, char *name, uint16_t *type, uint16_t *pointCount, uint32_t id); - -/** - * @brief Retrieve video source properties. - * @param nInstance SDK instance index - * @param name Output buffer (cbLEN_STR_LABEL) for source name - * @param fps Output frames-per-second value - * @param id 1-based video source identifier - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkGetVideoSource(uint32_t nInstance, char *name, float *fps, uint32_t id); - -/** - * @brief Set global spike waveform extraction parameters. - * @param nInstance SDK instance index - * @param spklength Waveform length in samples - * @param spkpretrig Number of pre-trigger samples - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSetSpikeConfig(uint32_t nInstance, uint32_t spklength, uint32_t spkpretrig); - -/** - * @brief Query global spike/system configuration. - * @param nInstance SDK instance index - * @param spklength Output spike length (samples) - * @param spkpretrig Optional output pre-trigger length (samples) - * @param sysfreq Optional output system tick frequency (Hz) - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkGetSysConfig(uint32_t nInstance, uint32_t * spklength, uint32_t * spkpretrig = nullptr, uint32_t * sysfreq = nullptr); - -/** - * @brief Send a system-level command (reset/shutdown/standby). - * @param nInstance SDK instance index - * @param cmd Command enum value - * @return cbSdkResult Success or error code - */ -CBSDKAPI cbSdkResult cbSdkSystem(uint32_t nInstance, cbSdkSystemType cmd); - -/** - * @brief Register callback for specific event type. - * Only one user callback per type per instance allowed. - * @param nInstance SDK instance number - * @param callbackType Enumerated callback type to register - * @param pCallbackFn Function pointer invoked on events (nullptr unregisters and returns warning) - * @param pCallbackData User data passed back to callback - */ -CBSDKAPI cbSdkResult cbSdkRegisterCallback(uint32_t nInstance, cbSdkCallbackType callbackType, cbSdkCallback pCallbackFn, void* pCallbackData); - -/** - * @brief Unregister previously registered callback. - * @param nInstance SDK instance number - * @param callbackType Enumerated callback type - */ -CBSDKAPI cbSdkResult cbSdkUnRegisterCallback(uint32_t nInstance, cbSdkCallbackType callbackType); - -/** - * @brief Query whether a callback is currently registered. - * @param nInstance SDK instance number - * @param callbackType Enumerated callback type - */ -CBSDKAPI cbSdkResult cbSdkCallbackStatus(uint32_t nInstance, cbSdkCallbackType callbackType); -// At most one callback per each callback type per each connection - -/** - * @brief Convert human-readable voltage string to digital units for channel scaling. - * Accepts unit suffixes (V, mV, uV) and optional sign. Space-insensitive. - * @param nInstance SDK instance index - * @param channel 1-based channel number - * @param szVoltsUnitString String such as "5V", "-65mV", "250uV" - * @param digital Output converted raw digital value - * @return cbSdkResult Success or error code (CBSDKRESULT_INVALIDPARAM on parse failure) - */ -CBSDKAPI cbSdkResult cbSdkAnalogToDigital(uint32_t nInstance, uint16_t channel, const char * szVoltsUnitString, int32_t * digital); - -/** - * @brief Send a poll packet (cbPKT_POLL) to the NSP. - * Useful for synchronizing or identifying external client applications. - * @param nInstance SDK instance number - * @param ppckt Pointer to a poll packet structure to transmit (must be properly populated) - * @return cbSdkResult error code - */ -CBSDKAPI cbSdkResult cbSdkSendPoll(uint32_t nInstance, void * ppckt); - -} - -#endif /* CBSDK_H_INCLUDED */ diff --git a/include/cerelink/pstdint.h b/include/cerelink/pstdint.h deleted file mode 100644 index 4b90d7ec..00000000 --- a/include/cerelink/pstdint.h +++ /dev/null @@ -1,920 +0,0 @@ -/* A portable stdint.h - **************************************************************************** - * BSD License: - **************************************************************************** - * - * Copyright (c) 2005-2016 Paul Hsieh - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - **************************************************************************** - * - * Version 0.1.16.0 - * - * The ANSI C standard committee, for the C99 standard, specified the - * inclusion of a new standard include file called stdint.h. This is - * a very useful and long desired include file which contains several - * very precise definitions for integer scalar types that is critically - * important for making several classes of applications portable - * including cryptography, hashing, variable length integer libraries - * and so on. But for most developers its likely useful just for - * programming sanity. - * - * The problem is that some compiler vendors chose to ignore the C99 - * standard and some older compilers have no opportunity to be updated. - * Because of this situation, simply including stdint.h in your code - * makes it unportable. - * - * So that's what this file is all about. It's an attempt to build a - * single universal include file that works on as many platforms as - * possible to deliver what stdint.h is supposed to. Even compilers - * that already come with stdint.h can use this file instead without - * any loss of functionality. A few things that should be noted about - * this file: - * - * 1) It is not guaranteed to be portable and/or present an identical - * interface on all platforms. The extreme variability of the - * ANSI C standard makes this an impossibility right from the - * very get go. Its really only meant to be useful for the vast - * majority of platforms that possess the capability of - * implementing usefully and precisely defined, standard sized - * integer scalars. Systems which are not intrinsically 2s - * complement may produce invalid constants. - * - * 2) There is an unavoidable use of non-reserved symbols. - * - * 3) Other standard include files are invoked. - * - * 4) This file may come in conflict with future platforms that do - * include stdint.h. The hope is that one or the other can be - * used with no real difference. - * - * 5) In the current version, if your platform can't represent - * int32_t, int16_t and int8_t, it just dumps out with a compiler - * error. - * - * 6) 64 bit integers may or may not be defined. Test for their - * presence with the test: #ifdef INT64_MAX or #ifdef UINT64_MAX. - * Note that this is different from the C99 specification which - * requires the existence of 64 bit support in the compiler. If - * this is not defined for your platform, yet it is capable of - * dealing with 64 bits then it is because this file has not yet - * been extended to cover all of your system's capabilities. - * - * 7) (u)intptr_t may or may not be defined. Test for its presence - * with the test: #ifdef PTRDIFF_MAX. If this is not defined - * for your platform, then it is because this file has not yet - * been extended to cover all of your system's capabilities, not - * because its optional. - * - * 8) The following might not been defined even if your platform is - * capable of defining it: - * - * WCHAR_MIN - * WCHAR_MAX - * (u)int64_t - * PTRDIFF_MIN - * PTRDIFF_MAX - * (u)intptr_t - * - * 9) The following have not been defined: - * - * WINT_MIN - * WINT_MAX - * - * 10) The criteria for defining (u)int_least(*)_t isn't clear, - * except for systems which don't have a type that precisely - * defined 8, 16, or 32 bit types (which this include file does - * not support anyways). Default definitions have been given. - * - * 11) The criteria for defining (u)int_fast(*)_t isn't something I - * would trust to any particular compiler vendor or the ANSI C - * committee. It is well known that "compatible systems" are - * commonly created that have very different performance - * characteristics from the systems they are compatible with, - * especially those whose vendors make both the compiler and the - * system. Default definitions have been given, but its strongly - * recommended that users never use these definitions for any - * reason (they do *NOT* deliver any serious guarantee of - * improved performance -- not in this file, nor any vendor's - * stdint.h). - * - * 12) The following macros: - * - * PRINTF_INTMAX_MODIFIER - * PRINTF_INT64_MODIFIER - * PRINTF_INT32_MODIFIER - * PRINTF_INT16_MODIFIER - * PRINTF_LEAST64_MODIFIER - * PRINTF_LEAST32_MODIFIER - * PRINTF_LEAST16_MODIFIER - * PRINTF_INTPTR_MODIFIER - * - * are strings which have been defined as the modifiers required - * for the "d", "u" and "x" printf formats to correctly output - * (u)intmax_t, (u)int64_t, (u)int32_t, (u)int16_t, (u)least64_t, - * (u)least32_t, (u)least16_t and (u)intptr_t types respectively. - * PRINTF_INTPTR_MODIFIER is not defined for some systems which - * provide their own stdint.h. PRINTF_INT64_MODIFIER is not - * defined if INT64_MAX is not defined. These are an extension - * beyond what C99 specifies must be in stdint.h. - * - * In addition, the following macros are defined: - * - * PRINTF_INTMAX_HEX_WIDTH - * PRINTF_INT64_HEX_WIDTH - * PRINTF_INT32_HEX_WIDTH - * PRINTF_INT16_HEX_WIDTH - * PRINTF_INT8_HEX_WIDTH - * PRINTF_INTMAX_DEC_WIDTH - * PRINTF_INT64_DEC_WIDTH - * PRINTF_INT32_DEC_WIDTH - * PRINTF_INT16_DEC_WIDTH - * PRINTF_UINT8_DEC_WIDTH - * PRINTF_UINTMAX_DEC_WIDTH - * PRINTF_UINT64_DEC_WIDTH - * PRINTF_UINT32_DEC_WIDTH - * PRINTF_UINT16_DEC_WIDTH - * PRINTF_UINT8_DEC_WIDTH - * - * Which specifies the maximum number of characters required to - * print the number of that type in either hexadecimal or decimal. - * These are an extension beyond what C99 specifies must be in - * stdint.h. - * - * Compilers tested (all with 0 warnings at their highest respective - * settings): Borland Turbo C 2.0, WATCOM C/C++ 11.0 (16 bits and 32 - * bits), Microsoft Visual C++ 6.0 (32 bit), Microsoft Visual Studio - * .net (VC7), Intel C++ 4.0, GNU gcc v3.3.3 - * - * This file should be considered a work in progress. Suggestions for - * improvements, especially those which increase coverage are strongly - * encouraged. - * - * Acknowledgements - * - * The following people have made significant contributions to the - * development and testing of this file: - * - * Chris Howie - * John Steele Scott - * Dave Thorup - * John Dill - * Florian Wobbe - * Christopher Sean Morrison - * Mikkel Fahnoe Jorgensen - * - */ -#pragma once - -#include -#include -#include - -/* - * For gcc with _STDINT_H, fill in the PRINTF_INT*_MODIFIER macros, and - * do nothing else. On the Mac OS X version of gcc this is _STDINT_H_. - */ - -#if ((defined(__SUNPRO_C) && __SUNPRO_C >= 0x570) || (defined(_MSC_VER) && _MSC_VER >= 1600) || (defined(__STDC__) && __STDC__ && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined (__WATCOMC__) && (defined (_STDINT_H_INCLUDED) || __WATCOMC__ >= 1250)) || (defined(__GNUC__) && (__GNUC__ > 3 || defined(_STDINT_H) || defined(_STDINT_H_) || defined (__UINT_FAST64_TYPE__)) )) && !defined (_PSTDINT_H_INCLUDED) -#include -#define _PSTDINT_H_INCLUDED -# if defined(__GNUC__) && (defined(__x86_64__) || defined(__ppc64__)) && !(defined(__APPLE__) && defined(__MACH__)) -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "l" -# endif -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -# else -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "ll" -# endif -# ifndef PRINTF_INT32_MODIFIER -# if (UINT_MAX == UINT32_MAX) -# define PRINTF_INT32_MODIFIER "" -# else -# define PRINTF_INT32_MODIFIER "l" -# endif -# endif -# endif -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "h" -# endif -# ifndef PRINTF_INTMAX_MODIFIER -# define PRINTF_INTMAX_MODIFIER PRINTF_INT64_MODIFIER -# endif -# ifndef PRINTF_INT64_HEX_WIDTH -# define PRINTF_INT64_HEX_WIDTH "16" -# endif -# ifndef PRINTF_UINT64_HEX_WIDTH -# define PRINTF_UINT64_HEX_WIDTH "16" -# endif -# ifndef PRINTF_INT32_HEX_WIDTH -# define PRINTF_INT32_HEX_WIDTH "8" -# endif -# ifndef PRINTF_UINT32_HEX_WIDTH -# define PRINTF_UINT32_HEX_WIDTH "8" -# endif -# ifndef PRINTF_INT16_HEX_WIDTH -# define PRINTF_INT16_HEX_WIDTH "4" -# endif -# ifndef PRINTF_UINT16_HEX_WIDTH -# define PRINTF_UINT16_HEX_WIDTH "4" -# endif -# ifndef PRINTF_INT8_HEX_WIDTH -# define PRINTF_INT8_HEX_WIDTH "2" -# endif -# ifndef PRINTF_UINT8_HEX_WIDTH -# define PRINTF_UINT8_HEX_WIDTH "2" -# endif -# ifndef PRINTF_INT64_DEC_WIDTH -# define PRINTF_INT64_DEC_WIDTH "19" -# endif -# ifndef PRINTF_UINT64_DEC_WIDTH -# define PRINTF_UINT64_DEC_WIDTH "20" -# endif -# ifndef PRINTF_INT32_DEC_WIDTH -# define PRINTF_INT32_DEC_WIDTH "10" -# endif -# ifndef PRINTF_UINT32_DEC_WIDTH -# define PRINTF_UINT32_DEC_WIDTH "10" -# endif -# ifndef PRINTF_INT16_DEC_WIDTH -# define PRINTF_INT16_DEC_WIDTH "5" -# endif -# ifndef PRINTF_UINT16_DEC_WIDTH -# define PRINTF_UINT16_DEC_WIDTH "5" -# endif -# ifndef PRINTF_INT8_DEC_WIDTH -# define PRINTF_INT8_DEC_WIDTH "3" -# endif -# ifndef PRINTF_UINT8_DEC_WIDTH -# define PRINTF_UINT8_DEC_WIDTH "3" -# endif -# ifndef PRINTF_INTMAX_HEX_WIDTH -# define PRINTF_INTMAX_HEX_WIDTH PRINTF_UINT64_HEX_WIDTH -# endif -# ifndef PRINTF_UINTMAX_HEX_WIDTH -# define PRINTF_UINTMAX_HEX_WIDTH PRINTF_UINT64_HEX_WIDTH -# endif -# ifndef PRINTF_INTMAX_DEC_WIDTH -# define PRINTF_INTMAX_DEC_WIDTH PRINTF_UINT64_DEC_WIDTH -# endif -# ifndef PRINTF_UINTMAX_DEC_WIDTH -# define PRINTF_UINTMAX_DEC_WIDTH PRINTF_UINT64_DEC_WIDTH -# endif - -/* - * Something really weird is going on with Open Watcom. Just pull some of - * these duplicated definitions from Open Watcom's stdint.h file for now. - */ - -# if defined (__WATCOMC__) && __WATCOMC__ >= 1250 -# if !defined (INT64_C) -# define INT64_C(x) (x + (INT64_MAX - INT64_MAX)) -# endif -# if !defined (UINT64_C) -# define UINT64_C(x) (x + (UINT64_MAX - UINT64_MAX)) -# endif -# if !defined (INT32_C) -# define INT32_C(x) (x + (INT32_MAX - INT32_MAX)) -# endif -# if !defined (UINT32_C) -# define UINT32_C(x) (x + (UINT32_MAX - UINT32_MAX)) -# endif -# if !defined (INT16_C) -# define INT16_C(x) (x) -# endif -# if !defined (UINT16_C) -# define UINT16_C(x) (x) -# endif -# if !defined (INT8_C) -# define INT8_C(x) (x) -# endif -# if !defined (UINT8_C) -# define UINT8_C(x) (x) -# endif -# if !defined (UINT64_MAX) -# define UINT64_MAX 18446744073709551615ULL -# endif -# if !defined (INT64_MAX) -# define INT64_MAX 9223372036854775807LL -# endif -# if !defined (UINT32_MAX) -# define UINT32_MAX 4294967295UL -# endif -# if !defined (INT32_MAX) -# define INT32_MAX 2147483647L -# endif -# if !defined (INTMAX_MAX) -# define INTMAX_MAX INT64_MAX -# endif -# if !defined (INTMAX_MIN) -# define INTMAX_MIN INT64_MIN -# endif -# endif -#endif - -/* - * I have no idea what is the truly correct thing to do on older Solaris. - * From some online discussions, this seems to be what is being - * recommended. For people who actually are developing on older Solaris, - * what I would like to know is, does this define all of the relevant - * macros of a complete stdint.h? Remember, in pstdint.h 64 bit is - * considered optional. - */ - -#if (defined(__SUNPRO_C) && __SUNPRO_C >= 0x420) && !defined(_PSTDINT_H_INCLUDED) -#include -#define _PSTDINT_H_INCLUDED -#endif - -#ifndef _PSTDINT_H_INCLUDED -#define _PSTDINT_H_INCLUDED - -#ifndef SIZE_MAX -# define SIZE_MAX ((size_t)-1) -#endif - -/* - * Deduce the type assignments from limits.h under the assumption that - * integer sizes in bits are powers of 2, and follow the ANSI - * definitions. - */ - -#ifndef UINT8_MAX -# define UINT8_MAX 0xff -#endif -#if !defined(uint8_t) && !defined(_UINT8_T) && !defined(vxWorks) -# if (UCHAR_MAX == UINT8_MAX) || defined (S_SPLINT_S) - typedef unsigned char uint8_t; -# define UINT8_C(v) ((uint8_t) v) -# else -# error "Platform not supported" -# endif -#endif - -#ifndef INT8_MAX -# define INT8_MAX 0x7f -#endif -#ifndef INT8_MIN -# define INT8_MIN INT8_C(0x80) -#endif -#if !defined(int8_t) && !defined(_INT8_T) && !defined(vxWorks) -# if (SCHAR_MAX == INT8_MAX) || defined (S_SPLINT_S) - typedef signed char int8_t; -# define INT8_C(v) ((int8_t) v) -# else -# error "Platform not supported" -# endif -#endif - -#ifndef UINT16_MAX -# define UINT16_MAX 0xffff -#endif -#if !defined(uint16_t) && !defined(_UINT16_T) && !defined(vxWorks) -#if (UINT_MAX == UINT16_MAX) || defined (S_SPLINT_S) - typedef unsigned int uint16_t; -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "" -# endif -# define UINT16_C(v) ((uint16_t) (v)) -#elif (USHRT_MAX == UINT16_MAX) - typedef unsigned short uint16_t; -# define UINT16_C(v) ((uint16_t) (v)) -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "h" -# endif -#else -#error "Platform not supported" -#endif -#endif - -#ifndef INT16_MAX -# define INT16_MAX 0x7fff -#endif -#ifndef INT16_MIN -# define INT16_MIN INT16_C(0x8000) -#endif -#if !defined(int16_t) && !defined(_INT16_T) && !defined(vxWorks) -#if (INT_MAX == INT16_MAX) || defined (S_SPLINT_S) - typedef signed int int16_t; -# define INT16_C(v) ((int16_t) (v)) -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "" -# endif -#elif (SHRT_MAX == INT16_MAX) - typedef signed short int16_t; -# define INT16_C(v) ((int16_t) (v)) -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "h" -# endif -#else -#error "Platform not supported" -#endif -#endif - -#ifndef UINT32_MAX -# define UINT32_MAX (0xffffffffUL) -#endif -#if !defined(uint32_t) && !defined(_UINT32_T) && !defined(vxWorks) -#if (ULONG_MAX == UINT32_MAX) || defined (S_SPLINT_S) - typedef unsigned long uint32_t; -# define UINT32_C(v) v ## UL -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "l" -# endif -#elif (UINT_MAX == UINT32_MAX) - typedef unsigned int uint32_t; -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -# define UINT32_C(v) v ## U -#elif (USHRT_MAX == UINT32_MAX) - typedef unsigned short uint32_t; -# define UINT32_C(v) ((unsigned short) (v)) -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -#else -#error "Platform not supported" -#endif -#endif - -#ifndef INT32_MAX -# define INT32_MAX (0x7fffffffL) -#endif -#ifndef INT32_MIN -# define INT32_MIN INT32_C(0x80000000) -#endif -#if !defined(int32_t) && !defined(_INT32_T) && !defined(vxWorks) -#if (LONG_MAX == INT32_MAX) || defined (S_SPLINT_S) - typedef signed long int32_t; -# define INT32_C(v) v ## L -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "l" -# endif -#elif (INT_MAX == INT32_MAX) - typedef signed int int32_t; -# define INT32_C(v) v -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -#elif (SHRT_MAX == INT32_MAX) - typedef signed short int32_t; -# define INT32_C(v) ((short) (v)) -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -#else -#error "Platform not supported" -#endif -#endif - -/* - * The macro stdint_int64_defined is temporarily used to record - * whether or not 64 integer support is available. It must be - * defined for any 64 integer extensions for new platforms that are - * added. - */ - -#undef stdint_int64_defined -#if (defined(__STDC__) && defined(__STDC_VERSION__)) || defined (S_SPLINT_S) -# if (__STDC__ && __STDC_VERSION__ >= 199901L) || defined (S_SPLINT_S) -# define stdint_int64_defined - typedef long long int64_t; - typedef unsigned long long uint64_t; -# define UINT64_C(v) v ## ULL -# define INT64_C(v) v ## LL -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "ll" -# endif -# endif -#endif - -#if !defined (stdint_int64_defined) -# if defined(__GNUC__) && !defined(vxWorks) -# define stdint_int64_defined - __extension__ typedef long long int64_t; - __extension__ typedef unsigned long long uint64_t; -# define UINT64_C(v) v ## ULL -# define INT64_C(v) v ## LL -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "ll" -# endif -# elif defined(__MWERKS__) || defined (__SUNPRO_C) || defined (__SUNPRO_CC) || defined (__APPLE_CC__) || defined (_LONG_LONG) || defined (_CRAYC) || defined (S_SPLINT_S) -# define stdint_int64_defined - typedef long long int64_t; - typedef unsigned long long uint64_t; -# define UINT64_C(v) v ## ULL -# define INT64_C(v) v ## LL -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "ll" -# endif -# elif (defined(__WATCOMC__) && defined(__WATCOM_INT64__)) || (defined(_MSC_VER) && _INTEGRAL_MAX_BITS >= 64) || (defined (__BORLANDC__) && __BORLANDC__ > 0x460) || defined (__alpha) || defined (__DECC) -# define stdint_int64_defined - typedef __int64 int64_t; - typedef unsigned __int64 uint64_t; -# define UINT64_C(v) v ## UI64 -# define INT64_C(v) v ## I64 -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "I64" -# endif -# endif -#endif - -#if !defined (LONG_LONG_MAX) && defined (INT64_C) -# define LONG_LONG_MAX INT64_C (9223372036854775807) -#endif -#ifndef ULONG_LONG_MAX -# define ULONG_LONG_MAX UINT64_C (18446744073709551615) -#endif - -#if !defined (INT64_MAX) && defined (INT64_C) -# define INT64_MAX INT64_C (9223372036854775807) -#endif -#if !defined (INT64_MIN) && defined (INT64_C) -# define INT64_MIN INT64_C (-9223372036854775808) -#endif -#if !defined (UINT64_MAX) && defined (INT64_C) -# define UINT64_MAX UINT64_C (18446744073709551615) -#endif - -/* - * Width of hexadecimal for number field. - */ - -#ifndef PRINTF_INT64_HEX_WIDTH -# define PRINTF_INT64_HEX_WIDTH "16" -#endif -#ifndef PRINTF_INT32_HEX_WIDTH -# define PRINTF_INT32_HEX_WIDTH "8" -#endif -#ifndef PRINTF_INT16_HEX_WIDTH -# define PRINTF_INT16_HEX_WIDTH "4" -#endif -#ifndef PRINTF_INT8_HEX_WIDTH -# define PRINTF_INT8_HEX_WIDTH "2" -#endif -#ifndef PRINTF_INT64_DEC_WIDTH -# define PRINTF_INT64_DEC_WIDTH "19" -#endif -#ifndef PRINTF_INT32_DEC_WIDTH -# define PRINTF_INT32_DEC_WIDTH "10" -#endif -#ifndef PRINTF_INT16_DEC_WIDTH -# define PRINTF_INT16_DEC_WIDTH "5" -#endif -#ifndef PRINTF_INT8_DEC_WIDTH -# define PRINTF_INT8_DEC_WIDTH "3" -#endif -#ifndef PRINTF_UINT64_DEC_WIDTH -# define PRINTF_UINT64_DEC_WIDTH "20" -#endif -#ifndef PRINTF_UINT32_DEC_WIDTH -# define PRINTF_UINT32_DEC_WIDTH "10" -#endif -#ifndef PRINTF_UINT16_DEC_WIDTH -# define PRINTF_UINT16_DEC_WIDTH "5" -#endif -#ifndef PRINTF_UINT8_DEC_WIDTH -# define PRINTF_UINT8_DEC_WIDTH "3" -#endif - -/* - * Ok, lets not worry about 128 bit integers for now. Moore's law says - * we don't need to worry about that until about 2040 at which point - * we'll have bigger things to worry about. - */ - -#ifdef stdint_int64_defined - typedef int64_t intmax_t; - typedef uint64_t uintmax_t; -# define INTMAX_MAX INT64_MAX -# define INTMAX_MIN INT64_MIN -# define UINTMAX_MAX UINT64_MAX -# define UINTMAX_C(v) UINT64_C(v) -# define INTMAX_C(v) INT64_C(v) -# ifndef PRINTF_INTMAX_MODIFIER -# define PRINTF_INTMAX_MODIFIER PRINTF_INT64_MODIFIER -# endif -# ifndef PRINTF_INTMAX_HEX_WIDTH -# define PRINTF_INTMAX_HEX_WIDTH PRINTF_INT64_HEX_WIDTH -# endif -# ifndef PRINTF_INTMAX_DEC_WIDTH -# define PRINTF_INTMAX_DEC_WIDTH PRINTF_INT64_DEC_WIDTH -# endif -#else - typedef int32_t intmax_t; - typedef uint32_t uintmax_t; -# define INTMAX_MAX INT32_MAX -# define UINTMAX_MAX UINT32_MAX -# define UINTMAX_C(v) UINT32_C(v) -# define INTMAX_C(v) INT32_C(v) -# ifndef PRINTF_INTMAX_MODIFIER -# define PRINTF_INTMAX_MODIFIER PRINTF_INT32_MODIFIER -# endif -# ifndef PRINTF_INTMAX_HEX_WIDTH -# define PRINTF_INTMAX_HEX_WIDTH PRINTF_INT32_HEX_WIDTH -# endif -# ifndef PRINTF_INTMAX_DEC_WIDTH -# define PRINTF_INTMAX_DEC_WIDTH PRINTF_INT32_DEC_WIDTH -# endif -#endif - -/* - * Because this file currently only supports platforms which have - * precise powers of 2 as bit sizes for the default integers, the - * least definitions are all trivial. Its possible that a future - * version of this file could have different definitions. - */ - -#ifndef stdint_least_defined - typedef int8_t int_least8_t; - typedef uint8_t uint_least8_t; - typedef int16_t int_least16_t; - typedef uint16_t uint_least16_t; - typedef int32_t int_least32_t; - typedef uint32_t uint_least32_t; -# define PRINTF_LEAST32_MODIFIER PRINTF_INT32_MODIFIER -# define PRINTF_LEAST16_MODIFIER PRINTF_INT16_MODIFIER -# define UINT_LEAST8_MAX UINT8_MAX -# define INT_LEAST8_MAX INT8_MAX -# define UINT_LEAST16_MAX UINT16_MAX -# define INT_LEAST16_MAX INT16_MAX -# define UINT_LEAST32_MAX UINT32_MAX -# define INT_LEAST32_MAX INT32_MAX -# define INT_LEAST8_MIN INT8_MIN -# define INT_LEAST16_MIN INT16_MIN -# define INT_LEAST32_MIN INT32_MIN -# ifdef stdint_int64_defined - typedef int64_t int_least64_t; - typedef uint64_t uint_least64_t; -# define PRINTF_LEAST64_MODIFIER PRINTF_INT64_MODIFIER -# define UINT_LEAST64_MAX UINT64_MAX -# define INT_LEAST64_MAX INT64_MAX -# define INT_LEAST64_MIN INT64_MIN -# endif -#endif -#undef stdint_least_defined - -/* - * The ANSI C committee has defined *int*_fast*_t types as well. This, - * of course, defies rationality -- you can't know what will be fast - * just from the type itself. Even for a given architecture, compatible - * implementations might have different performance characteristics. - * Developers are warned to stay away from these types when using this - * or any other stdint.h. - */ - -typedef int_least8_t int_fast8_t; -typedef uint_least8_t uint_fast8_t; -typedef int_least16_t int_fast16_t; -typedef uint_least16_t uint_fast16_t; -typedef int_least32_t int_fast32_t; -typedef uint_least32_t uint_fast32_t; -#define UINT_FAST8_MAX UINT_LEAST8_MAX -#define INT_FAST8_MAX INT_LEAST8_MAX -#define UINT_FAST16_MAX UINT_LEAST16_MAX -#define INT_FAST16_MAX INT_LEAST16_MAX -#define UINT_FAST32_MAX UINT_LEAST32_MAX -#define INT_FAST32_MAX INT_LEAST32_MAX -#define INT_FAST8_MIN INT_LEAST8_MIN -#define INT_FAST16_MIN INT_LEAST16_MIN -#define INT_FAST32_MIN INT_LEAST32_MIN -#ifdef stdint_int64_defined - typedef int_least64_t int_fast64_t; - typedef uint_least64_t uint_fast64_t; -# define UINT_FAST64_MAX UINT_LEAST64_MAX -# define INT_FAST64_MAX INT_LEAST64_MAX -# define INT_FAST64_MIN INT_LEAST64_MIN -#endif - -#undef stdint_int64_defined - -/* - * Whatever piecemeal, per compiler thing we can do about the wchar_t - * type limits. - */ - -#if defined(__WATCOMC__) || defined(_MSC_VER) || defined (__GNUC__) && !defined(vxWorks) -# include -# ifndef WCHAR_MIN -# define WCHAR_MIN 0 -# endif -# ifndef WCHAR_MAX -# define WCHAR_MAX ((wchar_t)-1) -# endif -#endif - -/* - * Whatever piecemeal, per compiler/platform thing we can do about the - * (u)intptr_t types and limits. - */ - -#if (defined (_MSC_VER) && defined (_UINTPTR_T_DEFINED)) || defined (_UINTPTR_T) -# define STDINT_H_UINTPTR_T_DEFINED -#endif - -#ifndef STDINT_H_UINTPTR_T_DEFINED -# if defined (__alpha__) || defined (__ia64__) || defined (__x86_64__) || defined (_WIN64) || defined (__ppc64__) -# define stdint_intptr_bits 64 -# elif defined (__WATCOMC__) || defined (__TURBOC__) -# if defined(__TINY__) || defined(__SMALL__) || defined(__MEDIUM__) -# define stdint_intptr_bits 16 -# else -# define stdint_intptr_bits 32 -# endif -# elif defined (__i386__) || defined (_WIN32) || defined (WIN32) || defined (__ppc64__) -# define stdint_intptr_bits 32 -# elif defined (__INTEL_COMPILER) -/* TODO -- what did Intel do about x86-64? */ -# else -/* #error "This platform might not be supported yet" */ -# endif - -# ifdef stdint_intptr_bits -# define stdint_intptr_glue3_i(a,b,c) a##b##c -# define stdint_intptr_glue3(a,b,c) stdint_intptr_glue3_i(a,b,c) -# ifndef PRINTF_INTPTR_MODIFIER -# define PRINTF_INTPTR_MODIFIER stdint_intptr_glue3(PRINTF_INT,stdint_intptr_bits,_MODIFIER) -# endif -# ifndef PTRDIFF_MAX -# define PTRDIFF_MAX stdint_intptr_glue3(INT,stdint_intptr_bits,_MAX) -# endif -# ifndef PTRDIFF_MIN -# define PTRDIFF_MIN stdint_intptr_glue3(INT,stdint_intptr_bits,_MIN) -# endif -# ifndef UINTPTR_MAX -# define UINTPTR_MAX stdint_intptr_glue3(UINT,stdint_intptr_bits,_MAX) -# endif -# ifndef INTPTR_MAX -# define INTPTR_MAX stdint_intptr_glue3(INT,stdint_intptr_bits,_MAX) -# endif -# ifndef INTPTR_MIN -# define INTPTR_MIN stdint_intptr_glue3(INT,stdint_intptr_bits,_MIN) -# endif -# ifndef INTPTR_C -# define INTPTR_C(x) stdint_intptr_glue3(INT,stdint_intptr_bits,_C)(x) -# endif -# ifndef UINTPTR_C -# define UINTPTR_C(x) stdint_intptr_glue3(UINT,stdint_intptr_bits,_C)(x) -# endif - typedef stdint_intptr_glue3(uint,stdint_intptr_bits,_t) uintptr_t; - typedef stdint_intptr_glue3( int,stdint_intptr_bits,_t) intptr_t; -# else -/* TODO -- This following is likely wrong for some platforms, and does - nothing for the definition of uintptr_t. */ - typedef ptrdiff_t intptr_t; -# endif -# define STDINT_H_UINTPTR_T_DEFINED -#endif - -/* - * Assumes sig_atomic_t is signed and we have a 2s complement machine. - */ - -#ifndef SIG_ATOMIC_MAX -# define SIG_ATOMIC_MAX ((((sig_atomic_t) 1) << (sizeof (sig_atomic_t)*CHAR_BIT-1)) - 1) -#endif - -#endif - -#if defined (__TEST_PSTDINT_FOR_CORRECTNESS) - -/* - * Please compile with the maximum warning settings to make sure macros are - * not defined more than once. - */ - -#include -#include -#include - -#define glue3_aux(x,y,z) x ## y ## z -#define glue3(x,y,z) glue3_aux(x,y,z) - -#define DECLU(bits) glue3(uint,bits,_t) glue3(u,bits,) = glue3(UINT,bits,_C) (0); -#define DECLI(bits) glue3(int,bits,_t) glue3(i,bits,) = glue3(INT,bits,_C) (0); - -#define DECL(us,bits) glue3(DECL,us,) (bits) - -#define TESTUMAX(bits) glue3(u,bits,) = ~glue3(u,bits,); if (glue3(UINT,bits,_MAX) != glue3(u,bits,)) printf ("Something wrong with UINT%d_MAX\n", bits) - -#define REPORTERROR(msg) { err_n++; if (err_first <= 0) err_first = __LINE__; printf msg; } - -#define X_SIZE_MAX ((size_t)-1) - -int main () { - int err_n = 0; - int err_first = 0; - DECL(I,8) - DECL(U,8) - DECL(I,16) - DECL(U,16) - DECL(I,32) - DECL(U,32) -#ifdef INT64_MAX - DECL(I,64) - DECL(U,64) -#endif - intmax_t imax = INTMAX_C(0); - uintmax_t umax = UINTMAX_C(0); - char str0[256], str1[256]; - - sprintf (str0, "%" PRINTF_INT32_MODIFIER "d", INT32_C(2147483647)); - if (0 != strcmp (str0, "2147483647")) REPORTERROR (("Something wrong with PRINTF_INT32_MODIFIER : %s\n", str0)); - if (atoi(PRINTF_INT32_DEC_WIDTH) != (int) strlen(str0)) REPORTERROR (("Something wrong with PRINTF_INT32_DEC_WIDTH : %s\n", PRINTF_INT32_DEC_WIDTH)); - sprintf (str0, "%" PRINTF_INT32_MODIFIER "u", UINT32_C(4294967295)); - if (0 != strcmp (str0, "4294967295")) REPORTERROR (("Something wrong with PRINTF_INT32_MODIFIER : %s\n", str0)); - if (atoi(PRINTF_UINT32_DEC_WIDTH) != (int) strlen(str0)) REPORTERROR (("Something wrong with PRINTF_UINT32_DEC_WIDTH : %s\n", PRINTF_UINT32_DEC_WIDTH)); -#ifdef INT64_MAX - sprintf (str1, "%" PRINTF_INT64_MODIFIER "d", INT64_C(9223372036854775807)); - if (0 != strcmp (str1, "9223372036854775807")) REPORTERROR (("Something wrong with PRINTF_INT32_MODIFIER : %s\n", str1)); - if (atoi(PRINTF_INT64_DEC_WIDTH) != (int) strlen(str1)) REPORTERROR (("Something wrong with PRINTF_INT64_DEC_WIDTH : %s, %d\n", PRINTF_INT64_DEC_WIDTH, (int) strlen(str1))); - sprintf (str1, "%" PRINTF_INT64_MODIFIER "u", UINT64_C(18446744073709550591)); - if (0 != strcmp (str1, "18446744073709550591")) REPORTERROR (("Something wrong with PRINTF_INT32_MODIFIER : %s\n", str1)); - if (atoi(PRINTF_UINT64_DEC_WIDTH) != (int) strlen(str1)) REPORTERROR (("Something wrong with PRINTF_UINT64_DEC_WIDTH : %s, %d\n", PRINTF_UINT64_DEC_WIDTH, (int) strlen(str1))); -#endif - - sprintf (str0, "%d %x\n", 0, ~0); - - sprintf (str1, "%d %x\n", i8, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with i8 : %s\n", str1)); - sprintf (str1, "%u %x\n", u8, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with u8 : %s\n", str1)); - sprintf (str1, "%d %x\n", i16, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with i16 : %s\n", str1)); - sprintf (str1, "%u %x\n", u16, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with u16 : %s\n", str1)); - sprintf (str1, "%" PRINTF_INT32_MODIFIER "d %x\n", i32, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with i32 : %s\n", str1)); - sprintf (str1, "%" PRINTF_INT32_MODIFIER "u %x\n", u32, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with u32 : %s\n", str1)); -#ifdef INT64_MAX - sprintf (str1, "%" PRINTF_INT64_MODIFIER "d %x\n", i64, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with i64 : %s\n", str1)); -#endif - sprintf (str1, "%" PRINTF_INTMAX_MODIFIER "d %x\n", imax, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with imax : %s\n", str1)); - sprintf (str1, "%" PRINTF_INTMAX_MODIFIER "u %x\n", umax, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with umax : %s\n", str1)); - - TESTUMAX(8); - TESTUMAX(16); - TESTUMAX(32); -#ifdef INT64_MAX - TESTUMAX(64); -#endif - -#define STR(v) #v -#define Q(v) printf ("sizeof " STR(v) " = %u\n", (unsigned) sizeof (v)); - if (err_n) { - printf ("pstdint.h is not correct. Please use sizes below to correct it:\n"); - } - - Q(int) - Q(unsigned) - Q(long int) - Q(short int) - Q(int8_t) - Q(int16_t) - Q(int32_t) -#ifdef INT64_MAX - Q(int64_t) -#endif - -#if UINT_MAX < X_SIZE_MAX - printf ("UINT_MAX < X_SIZE_MAX\n"); -#else - printf ("UINT_MAX >= X_SIZE_MAX\n"); -#endif - printf ("%" PRINTF_INT64_MODIFIER "u vs %" PRINTF_INT64_MODIFIER "u\n", UINT_MAX, X_SIZE_MAX); - - return EXIT_SUCCESS; -} - -#endif \ No newline at end of file diff --git a/pycbsdk/.gitignore b/pycbsdk/.gitignore new file mode 100644 index 00000000..f3c1db24 --- /dev/null +++ b/pycbsdk/.gitignore @@ -0,0 +1 @@ +src/pycbsdk/_version.py diff --git a/pycbsdk/README.md b/pycbsdk/README.md new file mode 100644 index 00000000..5bb05ac8 --- /dev/null +++ b/pycbsdk/README.md @@ -0,0 +1,171 @@ +# pycbsdk + +[![PyPI version](https://badge.fury.io/py/pycbsdk.svg)](https://badge.fury.io/py/pycbsdk) + +Python bindings for the [CereLink](https://github.com/CerebusOSS/CereLink) SDK, +providing real-time access to Blackrock Neurotech Cerebus neural signal processors. + +Built on [cffi](https://cffi.readthedocs.io/) (ABI mode) — no compiler needed at +install time. + +## Installation + +```bash +pip install pycbsdk + +# With numpy support (zero-copy array access for continuous data) +pip install pycbsdk[numpy] +``` + +**Windows prerequisite:** The bundled shared library requires the Microsoft Visual C++ +Redistributable. Most Windows systems already have it installed. If you get a DLL load +error, download it from +[Microsoft](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist). + +## Quick Start + +```python +from pycbsdk import Session +import time + +with Session("HUB1") as session: + # Register a callback for spike events + @session.on_event("FRONTEND") + def on_spike(header, data): + print(f"Spike on channel {header.chid} at t={header.time}") + + # Register a callback for 30kHz continuous data + @session.on_group(5, as_array=True) + def on_continuous(header, samples): + # samples is a numpy int16 array of shape (n_channels,) + print(f"Group packet: {len(samples)} channels") + + time.sleep(10) + print(session.stats) +``` + +## Features + +- **Callback-driven**: decorator-based registration for event, group, config, and + catch-all packet callbacks +- **Context manager**: automatic cleanup on exit +- **numpy integration** (optional): zero-copy arrays for continuous data, + ring buffer accumulator, blocking `read_continuous()` collector +- **Device support**: LEGACY_NSP, NSP, HUB1, HUB2, HUB3, NPLAY + +## API Overview + +### Session + +```python +session = Session(device_type="HUB1", callback_queue_depth=16384) +``` + +**Callbacks** (decorator style): + +| Decorator | Description | +|-----------|-------------| +| `@session.on_event("FRONTEND")` | Spike / event packets for a channel type | +| `@session.on_group(5)` | Continuous sample group (1-6) | +| `@session.on_group(5, as_array=True)` | Same, but data as numpy array | +| `@session.on_config(pkt_type)` | Config / system packets | +| `@session.on_packet()` | All packets (catch-all) | +| `session.on_error(fn)` | Error messages | + +**Configuration access**: + +- `session.get_channel_label(chan_id)` — channel label string +- `session.get_channel_smpgroup(chan_id)` — channel's sample group (0-6) +- `session.get_group_channels(group_id)` — list of channel IDs in a group +- `session.runlevel` — current device run level +- `Session.max_chans()`, `Session.num_fe_chans()`, `Session.num_analog_chans()` + +**Commands**: + +- `session.send_comment("marker text", rgba=0xFF0000)` — inject a comment +- `session.set_digital_output(chan_id, value)` — set digital output +- `session.set_runlevel(level)` — change system run level +- `session.set_channel_sample_group(n, "FRONTEND", group_id)` — configure sampling +- `session.set_channel_spike_sorting(n, "FRONTEND", sort_options)` — configure spike sorting + +**CCF Configuration Files**: + +- `session.save_ccf("config.ccf")` — save current device config to XML file +- `session.load_ccf("config.ccf")` — load config from file and apply to device + +**Recording Control** (requires Central): + +- `session.start_central_recording("filename", comment="session 1")` — start recording +- `session.stop_central_recording()` — stop recording + +**Clock Synchronization**: + +```python +# Convert device timestamp to Python's time.monotonic() +@session.on_event("FRONTEND") +def on_spike(header, data): + t = session.device_to_monotonic(header.time) + latency_ms = (time.monotonic() - t) * 1000 + print(f"Spike latency: {latency_ms:.1f} ms") +``` + +- `session.device_to_monotonic(device_time_ns)` — convert device timestamp to `time.monotonic()` seconds +- `session.clock_offset_ns` — raw clock offset (device_ns - steady_clock_ns), or None +- `session.clock_uncertainty_ns` — uncertainty (half-RTT), or None +- `session.send_clock_probe()` — send a sync probe + +**Statistics**: + +```python +stats = session.stats # Stats dataclass +print(stats.packets_received, stats.packets_dropped) +session.reset_stats() +``` + +### numpy Integration + +Requires `pip install pycbsdk[numpy]`. + +```python +# Blocking data collection +data = session.read_continuous(group_id=5, duration=2.0) +# data.shape == (n_channels, ~60000), dtype int16 + +# Ring buffer for ongoing collection +reader = session.continuous_reader(group_id=5, buffer_seconds=10) +import time; time.sleep(5) +data = reader.read() # most recent samples +data = reader.read(1000) # last 1000 samples +print(reader.total_samples, reader.dropped) +reader.close() +``` + +## Supported Devices + +| Device Type | Description | +|-------------|-------------| +| `LEGACY_NSP` | Legacy NSP (default) | +| `NSP` | NSP | +| `HUB1` | Gemini Hub 1 | +| `HUB2` | Gemini Hub 2 | +| `HUB3` | Gemini Hub 3 | +| `NPLAY` | nPlay | + +## Development + +```bash +# Build the shared library +cmake -S . -B build -DCBSDK_BUILD_SHARED=ON -DCMAKE_BUILD_TYPE=Release +cmake --build build --target cbsdk_shared --config Release + +# Install pycbsdk in development mode +cd pycbsdk +pip install -e ".[dev,numpy]" + +# Point to the shared library +export CBSDK_LIB_PATH=/path/to/libcbsdk.dll # or .so / .dylib +``` + +## License + +BSD 2-Clause. See [LICENSE.txt](https://github.com/CerebusOSS/CereLink/blob/master/LICENSE.txt). diff --git a/pycbsdk/pyproject.toml b/pycbsdk/pyproject.toml new file mode 100644 index 00000000..7c580d71 --- /dev/null +++ b/pycbsdk/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["setuptools>=64", "wheel", "cffi>=1.15", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "pycbsdk" +dynamic = ["version"] +description = "Python bindings for CereLink SDK (Blackrock Neurotech Cerebus devices)" +requires-python = ">=3.9" +license = "BSD-2-Clause" +authors = [ + { name = "Chadwick Boulay", email = "chadwick.boulay@gmail.com" }, +] +keywords = ["neuroscience", "electrophysiology", "blackrock", "cerebus", "neural", "bci"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: Medical Science Apps.", + "Topic :: System :: Hardware :: Hardware Drivers", +] +readme = "README.md" +dependencies = [ + "cffi>=1.15", +] + +[project.urls] +Homepage = "https://github.com/CerebusOSS/CereLink" +Repository = "https://github.com/CerebusOSS/CereLink" +Issues = "https://github.com/CerebusOSS/CereLink/issues" + +[project.optional-dependencies] +numpy = ["numpy"] +dev = ["pytest"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +pycbsdk = ["*.dll", "*.so", "*.so.*", "*.dylib"] + +[tool.setuptools_scm] +root = ".." +version_file = "src/pycbsdk/_version.py" +relative_to = "pyproject.toml" diff --git a/pycbsdk/setup.py b/pycbsdk/setup.py new file mode 100644 index 00000000..23759d42 --- /dev/null +++ b/pycbsdk/setup.py @@ -0,0 +1,20 @@ +"""Setup script for pycbsdk. + +Forces platform-specific wheel tags (py3-none-{platform}) because the +package bundles a pre-built shared library (cbsdk.dll / libcbsdk.so / libcbsdk.dylib). +""" + +from setuptools import setup + +try: + from wheel.bdist_wheel import bdist_wheel + + class PlatformWheel(bdist_wheel): + """Tag wheel as platform-specific but Python-version-independent.""" + + def get_tag(self): + return "py3", "none", self.plat_name.replace("-", "_").replace(".", "_") + + setup(cmdclass={"bdist_wheel": PlatformWheel}) +except ImportError: + setup() diff --git a/pycbsdk/src/pycbsdk/__init__.py b/pycbsdk/src/pycbsdk/__init__.py new file mode 100644 index 00000000..5a07b09b --- /dev/null +++ b/pycbsdk/src/pycbsdk/__init__.py @@ -0,0 +1,33 @@ +""" +pycbsdk - Python bindings for CereLink SDK (Blackrock Neurotech Cerebus devices). + +Usage:: + + from pycbsdk import Session, DeviceType, ChannelType + + with Session(DeviceType.HUB1) as session: + @session.on_event(ChannelType.FRONTEND) + def on_spike(header, data): + print(f"Spike on ch {header.chid}, t={header.time}") + + import time + time.sleep(10) + + print(session.stats) +""" + +from .session import ( + Session, DeviceType, ChannelType, SampleRate, ChanInfoField, + ProtocolVersion, Stats, ContinuousReader, +) + +try: + from ._version import __version__ +except ImportError: + __version__ = "0.0.0" + +__all__ = [ + "Session", "DeviceType", "ChannelType", "SampleRate", "ChanInfoField", + "ProtocolVersion", "Stats", "ContinuousReader", + "__version__", +] diff --git a/pycbsdk/src/pycbsdk/_cdef.py b/pycbsdk/src/pycbsdk/_cdef.py new file mode 100644 index 00000000..c547a925 --- /dev/null +++ b/pycbsdk/src/pycbsdk/_cdef.py @@ -0,0 +1,338 @@ +""" +cffi C declarations for the cbsdk C API. + +These must match the types and function signatures in cbsdk.h exactly. +Complex structs (cbPKT_CHANINFO, cbPKT_SYSINFO, etc.) are NOT defined here; +instead, FFI-friendly accessor functions are used. +""" + +CDEF = """ +/////////////////////////////////////////////////////////////////////////// +// Enums +/////////////////////////////////////////////////////////////////////////// + +typedef enum { + CBSDK_RESULT_SUCCESS = 0, + CBSDK_RESULT_INVALID_PARAMETER = -1, + CBSDK_RESULT_ALREADY_RUNNING = -2, + CBSDK_RESULT_NOT_RUNNING = -3, + CBSDK_RESULT_SHMEM_ERROR = -4, + CBSDK_RESULT_DEVICE_ERROR = -5, + CBSDK_RESULT_INTERNAL_ERROR = -6, +} cbsdk_result_t; + +typedef enum { + CBPROTO_DEVICE_TYPE_LEGACY_NSP = 0, + CBPROTO_DEVICE_TYPE_NSP = 1, + CBPROTO_DEVICE_TYPE_HUB1 = 2, + CBPROTO_DEVICE_TYPE_HUB2 = 3, + CBPROTO_DEVICE_TYPE_HUB3 = 4, + CBPROTO_DEVICE_TYPE_NPLAY = 5, + CBPROTO_DEVICE_TYPE_CUSTOM = 6, +} cbproto_device_type_t; + +typedef enum { + CBPROTO_CHANNEL_TYPE_FRONTEND = 0, + CBPROTO_CHANNEL_TYPE_ANALOG_IN = 1, + CBPROTO_CHANNEL_TYPE_ANALOG_OUT = 2, + CBPROTO_CHANNEL_TYPE_AUDIO = 3, + CBPROTO_CHANNEL_TYPE_DIGITAL_IN = 4, + CBPROTO_CHANNEL_TYPE_SERIAL = 5, + CBPROTO_CHANNEL_TYPE_DIGITAL_OUT = 6, +} cbproto_channel_type_t; + +typedef enum { + CBPROTO_GROUP_RATE_NONE = 0, + CBPROTO_GROUP_RATE_500Hz = 1, + CBPROTO_GROUP_RATE_1000Hz = 2, + CBPROTO_GROUP_RATE_2000Hz = 3, + CBPROTO_GROUP_RATE_10000Hz = 4, + CBPROTO_GROUP_RATE_30000Hz = 5, + CBPROTO_GROUP_RATE_RAW = 6 +} cbproto_group_rate_t; + +/////////////////////////////////////////////////////////////////////////// +// Packet Structures (protocol-defined, stable layout) +/////////////////////////////////////////////////////////////////////////// + +// Packet header: 16 bytes +typedef struct { + uint64_t time; + uint16_t chid; + uint16_t type; + uint16_t dlen; + uint8_t instrument; + uint8_t reserved; +} cbPKT_HEADER; + +// Generic packet: 1024 bytes (header + 1008 byte payload) +typedef struct { + cbPKT_HEADER cbpkt_header; + union { + uint8_t data_u8[1008]; + uint16_t data_u16[504]; + uint32_t data_u32[252]; + }; +} cbPKT_GENERIC; + +// Group (continuous data) packet: header + int16 samples +typedef struct { + cbPKT_HEADER cbpkt_header; + int16_t data[272]; +} cbPKT_GROUP; + +/////////////////////////////////////////////////////////////////////////// +// Configuration and Statistics Structures +/////////////////////////////////////////////////////////////////////////// + +typedef struct { + int device_type; // cbproto_device_type_t + size_t callback_queue_depth; + _Bool enable_realtime_priority; + _Bool drop_on_overflow; + int recv_buffer_size; + _Bool non_blocking; + const char* custom_device_address; + const char* custom_client_address; + uint16_t custom_device_port; + uint16_t custom_client_port; +} cbsdk_config_t; + +typedef struct { + uint64_t packets_received_from_device; + uint64_t bytes_received_from_device; + uint64_t packets_stored_to_shmem; + uint64_t packets_queued_for_callback; + uint64_t packets_delivered_to_callback; + uint64_t packets_dropped; + uint64_t queue_current_depth; + uint64_t queue_max_depth; + uint64_t packets_sent_to_device; + uint64_t shmem_store_errors; + uint64_t receive_errors; + uint64_t send_errors; +} cbsdk_stats_t; + +typedef struct { + int16_t digmin; + int16_t digmax; + int32_t anamin; + int32_t anamax; + int32_t anagain; + char anaunit[8]; +} cbsdk_channel_scaling_t; + +/////////////////////////////////////////////////////////////////////////// +// Callback Types +/////////////////////////////////////////////////////////////////////////// + +typedef uint32_t cbsdk_callback_handle_t; + +typedef void (*cbsdk_packet_callback_fn)(const cbPKT_GENERIC* pkts, size_t count, void* user_data); +typedef void (*cbsdk_event_callback_fn)(const cbPKT_GENERIC* pkt, void* user_data); +typedef void (*cbsdk_group_callback_fn)(const cbPKT_GROUP* pkt, void* user_data); +typedef void (*cbsdk_config_callback_fn)(const cbPKT_GENERIC* pkt, void* user_data); +typedef void (*cbsdk_error_callback_fn)(const char* error_message, void* user_data); + +/////////////////////////////////////////////////////////////////////////// +// Opaque Handle +/////////////////////////////////////////////////////////////////////////// + +typedef struct cbsdk_session_impl* cbsdk_session_t; + +/////////////////////////////////////////////////////////////////////////// +// Functions +/////////////////////////////////////////////////////////////////////////// + +// Config +cbsdk_config_t cbsdk_config_default(void); + +// Session lifecycle +cbsdk_result_t cbsdk_session_create(cbsdk_session_t* session, const cbsdk_config_t* config); +void cbsdk_session_destroy(cbsdk_session_t session); +cbsdk_result_t cbsdk_session_start(cbsdk_session_t session); +void cbsdk_session_stop(cbsdk_session_t session); +_Bool cbsdk_session_is_running(cbsdk_session_t session); + +// Legacy callbacks +void cbsdk_session_set_packet_callback(cbsdk_session_t session, + cbsdk_packet_callback_fn callback, void* user_data); +void cbsdk_session_set_error_callback(cbsdk_session_t session, + cbsdk_error_callback_fn callback, void* user_data); + +// Typed callback registration +cbsdk_callback_handle_t cbsdk_session_register_packet_callback( + cbsdk_session_t session, cbsdk_packet_callback_fn callback, void* user_data); +cbsdk_callback_handle_t cbsdk_session_register_event_callback( + cbsdk_session_t session, cbproto_channel_type_t channel_type, + cbsdk_event_callback_fn callback, void* user_data); +cbsdk_callback_handle_t cbsdk_session_register_group_callback( + cbsdk_session_t session, cbproto_group_rate_t rate, + cbsdk_group_callback_fn callback, void* user_data); +cbsdk_callback_handle_t cbsdk_session_register_config_callback( + cbsdk_session_t session, uint16_t packet_type, + cbsdk_config_callback_fn callback, void* user_data); +void cbsdk_session_unregister_callback(cbsdk_session_t session, + cbsdk_callback_handle_t handle); + +// Statistics +void cbsdk_session_get_stats(cbsdk_session_t session, cbsdk_stats_t* stats); +void cbsdk_session_reset_stats(cbsdk_session_t session); + +// Configuration access +uint32_t cbsdk_session_get_runlevel(cbsdk_session_t session); +uint32_t cbsdk_session_get_protocol_version(cbsdk_session_t session); +uint32_t cbsdk_session_get_proc_ident(cbsdk_session_t session, char* buf, uint32_t buf_size); +uint32_t cbsdk_session_get_spike_length(cbsdk_session_t session); +uint32_t cbsdk_session_get_spike_pretrigger(cbsdk_session_t session); +cbsdk_result_t cbsdk_session_set_spike_length(cbsdk_session_t session, + uint32_t spike_length, uint32_t spike_pretrigger); +uint32_t cbsdk_get_max_chans(void); +uint32_t cbsdk_get_num_fe_chans(void); +uint32_t cbsdk_get_num_analog_chans(void); +const char* cbsdk_session_get_channel_label(cbsdk_session_t session, uint32_t chan_id); +uint32_t cbsdk_session_get_channel_smpgroup(cbsdk_session_t session, uint32_t chan_id); +uint32_t cbsdk_session_get_channel_chancaps(cbsdk_session_t session, uint32_t chan_id); +const char* cbsdk_session_get_group_label(cbsdk_session_t session, uint32_t group_id); +cbsdk_result_t cbsdk_session_get_group_list(cbsdk_session_t session, + uint32_t group_id, uint16_t* list, uint32_t* count); + +// Channel configuration +cbsdk_result_t cbsdk_session_set_channel_sample_group( + cbsdk_session_t session, size_t n_chans, cbproto_channel_type_t chan_type, + cbproto_group_rate_t rate, _Bool disable_others); +cbsdk_result_t cbsdk_session_set_ac_input_coupling( + cbsdk_session_t session, size_t n_chans, cbproto_channel_type_t chan_type, + _Bool enabled); + +// Per-channel getters +cbproto_channel_type_t cbsdk_session_get_channel_type(cbsdk_session_t session, uint32_t chan_id); +uint32_t cbsdk_session_get_channel_smpfilter(cbsdk_session_t session, uint32_t chan_id); +uint32_t cbsdk_session_get_channel_spkfilter(cbsdk_session_t session, uint32_t chan_id); +uint32_t cbsdk_session_get_channel_spkopts(cbsdk_session_t session, uint32_t chan_id); +int32_t cbsdk_session_get_channel_spkthrlevel(cbsdk_session_t session, uint32_t chan_id); +uint32_t cbsdk_session_get_channel_ainpopts(cbsdk_session_t session, uint32_t chan_id); +uint32_t cbsdk_session_get_channel_lncrate(cbsdk_session_t session, uint32_t chan_id); +uint32_t cbsdk_session_get_channel_refelecchan(cbsdk_session_t session, uint32_t chan_id); +int16_t cbsdk_session_get_channel_amplrejpos(cbsdk_session_t session, uint32_t chan_id); +int16_t cbsdk_session_get_channel_amplrejneg(cbsdk_session_t session, uint32_t chan_id); +cbsdk_result_t cbsdk_session_get_channel_scaling( + cbsdk_session_t session, uint32_t chan_id, cbsdk_channel_scaling_t* scaling); + +// Per-channel setters +cbsdk_result_t cbsdk_session_set_channel_label(cbsdk_session_t session, + uint32_t chan_id, const char* label); +cbsdk_result_t cbsdk_session_set_channel_smpfilter(cbsdk_session_t session, + uint32_t chan_id, uint32_t filter_id); +cbsdk_result_t cbsdk_session_set_channel_spkfilter(cbsdk_session_t session, + uint32_t chan_id, uint32_t filter_id); +cbsdk_result_t cbsdk_session_set_channel_ainpopts(cbsdk_session_t session, + uint32_t chan_id, uint32_t ainpopts); +cbsdk_result_t cbsdk_session_set_channel_lncrate(cbsdk_session_t session, + uint32_t chan_id, uint32_t lncrate); +cbsdk_result_t cbsdk_session_set_channel_spkopts(cbsdk_session_t session, + uint32_t chan_id, uint32_t spkopts); +cbsdk_result_t cbsdk_session_set_channel_spkthrlevel(cbsdk_session_t session, + uint32_t chan_id, int32_t level); +cbsdk_result_t cbsdk_session_set_channel_autothreshold(cbsdk_session_t session, + uint32_t chan_id, _Bool enabled); + +// Channel info field selector +typedef enum { + CBSDK_CHANINFO_FIELD_SMPGROUP = 0, + CBSDK_CHANINFO_FIELD_SMPFILTER = 1, + CBSDK_CHANINFO_FIELD_SPKFILTER = 2, + CBSDK_CHANINFO_FIELD_AINPOPTS = 3, + CBSDK_CHANINFO_FIELD_SPKOPTS = 4, + CBSDK_CHANINFO_FIELD_SPKTHRLEVEL = 5, + CBSDK_CHANINFO_FIELD_LNCRATE = 6, + CBSDK_CHANINFO_FIELD_REFELECCHAN = 7, + CBSDK_CHANINFO_FIELD_AMPLREJPOS = 8, + CBSDK_CHANINFO_FIELD_AMPLREJNEG = 9, + CBSDK_CHANINFO_FIELD_CHANCAPS = 10, + CBSDK_CHANINFO_FIELD_BANK = 11, + CBSDK_CHANINFO_FIELD_TERM = 12, +} cbsdk_chaninfo_field_t; + +// Generic single-channel field getter +int64_t cbsdk_session_get_channel_field(cbsdk_session_t session, + uint32_t chan_id, cbsdk_chaninfo_field_t field); + +// Bulk channel queries + +cbsdk_result_t cbsdk_session_get_matching_channels( + cbsdk_session_t session, size_t n_chans, cbproto_channel_type_t chan_type, + uint32_t* out_ids, uint32_t* out_count); +cbsdk_result_t cbsdk_session_get_channels_field( + cbsdk_session_t session, size_t n_chans, cbproto_channel_type_t chan_type, + cbsdk_chaninfo_field_t field, int64_t* out_values, uint32_t* out_count); +cbsdk_result_t cbsdk_session_get_channels_labels( + cbsdk_session_t session, size_t n_chans, cbproto_channel_type_t chan_type, + char* out_buf, size_t label_stride, uint32_t* out_count); + +cbsdk_result_t cbsdk_session_get_channels_positions( + cbsdk_session_t session, size_t n_chans, cbproto_channel_type_t chan_type, + int32_t* out_positions, uint32_t* out_count); + +// Bulk configuration access +uint32_t cbsdk_session_get_sysfreq(cbsdk_session_t session); +uint32_t cbsdk_get_num_filters(void); +const char* cbsdk_session_get_filter_label(cbsdk_session_t session, uint32_t filter_id); +uint32_t cbsdk_session_get_filter_hpfreq(cbsdk_session_t session, uint32_t filter_id); +uint32_t cbsdk_session_get_filter_hporder(cbsdk_session_t session, uint32_t filter_id); +uint32_t cbsdk_session_get_filter_lpfreq(cbsdk_session_t session, uint32_t filter_id); +uint32_t cbsdk_session_get_filter_lporder(cbsdk_session_t session, uint32_t filter_id); + +// Instrument time +cbsdk_result_t cbsdk_session_get_time(cbsdk_session_t session, uint64_t* time); + +// Patient information +cbsdk_result_t cbsdk_session_set_patient_info(cbsdk_session_t session, + const char* id, const char* firstname, const char* lastname, + uint32_t dob_month, uint32_t dob_day, uint32_t dob_year); + +// Analog output +cbsdk_result_t cbsdk_session_set_analog_output_monitor(cbsdk_session_t session, + uint32_t aout_chan_id, uint32_t monitor_chan_id, + _Bool track_last, _Bool spike_only); + +// Commands +cbsdk_result_t cbsdk_session_send_comment(cbsdk_session_t session, + const char* comment, uint32_t rgba, uint8_t charset); +cbsdk_result_t cbsdk_session_send_packet(cbsdk_session_t session, + const cbPKT_GENERIC* pkt); +cbsdk_result_t cbsdk_session_set_digital_output(cbsdk_session_t session, + uint32_t chan_id, uint16_t value); +cbsdk_result_t cbsdk_session_set_runlevel(cbsdk_session_t session, + uint32_t runlevel); + +// CCF configuration files +cbsdk_result_t cbsdk_session_load_channel_map(cbsdk_session_t session, const char* filepath, uint32_t bank_offset); +cbsdk_result_t cbsdk_session_save_ccf(cbsdk_session_t session, const char* filename); +cbsdk_result_t cbsdk_session_load_ccf(cbsdk_session_t session, const char* filename); + +// Recording control (Central) +cbsdk_result_t cbsdk_session_start_central_recording(cbsdk_session_t session, + const char* filename, const char* comment); +cbsdk_result_t cbsdk_session_stop_central_recording(cbsdk_session_t session); +cbsdk_result_t cbsdk_session_open_central_file_dialog(cbsdk_session_t session); +cbsdk_result_t cbsdk_session_close_central_file_dialog(cbsdk_session_t session); + +// Spike sorting +cbsdk_result_t cbsdk_session_set_channel_spike_sorting( + cbsdk_session_t session, size_t n_chans, cbproto_channel_type_t chan_type, + uint32_t sort_options); + +// Clock synchronization +cbsdk_result_t cbsdk_session_get_clock_offset(cbsdk_session_t session, int64_t* offset_ns); +cbsdk_result_t cbsdk_session_get_clock_uncertainty(cbsdk_session_t session, int64_t* uncertainty_ns); +cbsdk_result_t cbsdk_session_send_clock_probe(cbsdk_session_t session); + +// Utility +int64_t cbsdk_get_steady_clock_ns(void); + +// Error handling & version +const char* cbsdk_get_error_message(cbsdk_result_t result); +const char* cbsdk_get_version(void); + +""" diff --git a/pycbsdk/src/pycbsdk/_lib.py b/pycbsdk/src/pycbsdk/_lib.py new file mode 100644 index 00000000..095d9d9a --- /dev/null +++ b/pycbsdk/src/pycbsdk/_lib.py @@ -0,0 +1,123 @@ +""" +Library loading for pycbsdk. + +Finds and loads the cbsdk shared library (libcbsdk.dll / libcbsdk.so / libcbsdk.dylib). +""" + +import os +import sys +import cffi + +from ._cdef import CDEF + +ffi = cffi.FFI() +ffi.cdef(CDEF) + + +def _find_library() -> str: + """Find the cbsdk shared library. + + Search order: + 1. CBSDK_LIB_PATH environment variable (explicit path to the .dll/.so/.dylib) + 2. CBSDK_LIB_DIR environment variable (directory containing the library) + 3. Next to this Python package (for bundled wheels) + 4. Common build directories relative to the CereLink repo root + 5. System library paths (via cffi default search) + """ + # Platform-specific library names + if sys.platform == "win32": + lib_names = ["cbsdk.dll", "libcbsdk.dll", "cbsdkd.dll", "libcbsdkd.dll"] + elif sys.platform == "darwin": + lib_names = ["libcbsdk.dylib"] + else: + lib_names = ["libcbsdk.so"] + + # 1. Explicit path + explicit = os.environ.get("CBSDK_LIB_PATH") + if explicit and os.path.isfile(explicit): + return explicit + + # 2. Explicit directory + lib_dir = os.environ.get("CBSDK_LIB_DIR") + if lib_dir: + for name in lib_names: + path = os.path.join(lib_dir, name) + if os.path.isfile(path): + return path + + # 3. Next to this package (bundled in wheel) + pkg_dir = os.path.dirname(os.path.abspath(__file__)) + for name in lib_names: + path = os.path.join(pkg_dir, name) + if os.path.isfile(path): + return path + + # 4. Common build directories (for development) + # Walk up from this file to find the CereLink repo root + repo_root = pkg_dir + for _ in range(10): + parent = os.path.dirname(repo_root) + if parent == repo_root: + break + repo_root = parent + if os.path.isfile(os.path.join(repo_root, "CMakeLists.txt")): + for build_dir in ["cmake-build-debug", "cmake-build-release", "build"]: + for subpath in [ + os.path.join("src", "cbsdk"), + "", + ]: + for name in lib_names: + path = os.path.join(repo_root, build_dir, subpath, name) + if os.path.isfile(path): + return path + + # 5. Let cffi/OS try to find it on the system path + for name in lib_names: + # Strip lib prefix and extension for dlopen-style search + return name + + return lib_names[0] # Fallback, will produce a clear error + + +def load_library(): + """Load the cbsdk shared library and return the cffi lib object.""" + path = _find_library() + + # On Windows, add DLL search directories for runtime dependencies + # (e.g., MinGW's libstdc++, libgcc, libwinpthread) + _dll_dirs = [] + if sys.platform == "win32" and hasattr(os, "add_dll_directory"): + # Add the directory containing the library itself + lib_dir = os.path.dirname(os.path.abspath(path)) + if os.path.isdir(lib_dir): + _dll_dirs.append(os.add_dll_directory(lib_dir)) + + # Add directories from CBSDK_DLL_DIRS (semicolon-separated) + extra_dirs = os.environ.get("CBSDK_DLL_DIRS", "") + for d in extra_dirs.split(";"): + d = d.strip() + if d and os.path.isdir(d): + _dll_dirs.append(os.add_dll_directory(d)) + + # Common MinGW locations + for mingw_hint in [ + os.environ.get("MINGW_BIN", ""), + os.path.expandvars( + r"%LOCALAPPDATA%\Programs\CLion\bin\mingw\bin" + ), + ]: + if mingw_hint and os.path.isdir(mingw_hint): + _dll_dirs.append(os.add_dll_directory(mingw_hint)) + + try: + return ffi.dlopen(path) + except OSError as e: + raise OSError( + f"Could not load cbsdk shared library: {e}\n" + f"Searched for: {path}\n" + f"Set CBSDK_LIB_PATH to the full path of the cbsdk shared library,\n" + f"or CBSDK_LIB_DIR to the directory containing it.\n" + f"On Windows, set CBSDK_DLL_DIRS to semicolon-separated directories\n" + f"containing runtime dependencies (e.g., MinGW bin directory).\n" + f"Build with: cmake -DCBSDK_BUILD_SHARED=ON" + ) from e diff --git a/pycbsdk/src/pycbsdk/_numpy.py b/pycbsdk/src/pycbsdk/_numpy.py new file mode 100644 index 00000000..ed9d448c --- /dev/null +++ b/pycbsdk/src/pycbsdk/_numpy.py @@ -0,0 +1,35 @@ +"""numpy integration utilities for pycbsdk.""" + +import numpy as np + +from ._lib import ffi + +# Structured dtype matching cbPKT_HEADER (16 bytes, little-endian) +HEADER_DTYPE = np.dtype([ + ('time', ' None: + """Connect to a device and configure channels. + + Args: + device_type: Device to connect to. + channel_type: Which channel type to configure. + rate: Sample rate group (``SampleRate.NONE`` to disable). + n_chans: Number of channels to configure (0 = all available). + disable_spikes: Disable spike sorting on configured channels. + dc_coupling: Switch to DC input coupling on configured channels. + timeout: Connection timeout in seconds. + """ + with Session(device_type=device_type) as session: + deadline = time.monotonic() + timeout + while not session.running: + if time.monotonic() > deadline: + raise TimeoutError( + f"Session for {device_type.name} did not start within {timeout}s" + ) + time.sleep(0.1) + # Let initial config settle + time.sleep(0.5) + + # Determine actual channel count + available = len(session.get_matching_channel_ids(channel_type)) + if n_chans <= 0 or n_chans > available: + n_chans = available + + print(f"Configuring {n_chans}/{available} {channel_type.name} channels " + f"to {rate.name} ({rate.hz} Hz)" if rate != SampleRate.NONE + else f"Disabling sampling on {n_chans}/{available} {channel_type.name} channels") + + # Set sample group (disable_others=True to turn off remaining channels) + session.set_channel_sample_group(n_chans, channel_type, rate, disable_others=True) + + if dc_coupling and rate != SampleRate.NONE: + session.set_ac_input_coupling(n_chans, channel_type, False) + print(" AC input coupling disabled (DC mode)") + if disable_spikes and rate != SampleRate.NONE: + session.set_channel_spike_sorting(n_chans, channel_type, _SPKOPTS_NOSORT) + print(" Spike sorting disabled") + + # Wait for config confirmations + time.sleep(0.5) + + # Verify + group_chans = session.get_group_channels(int(rate)) if rate != SampleRate.NONE else [] + print(f"Verification: group {rate.name} now has {len(group_chans)} channels") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python -m pycbsdk.cli.configure_channels", + description="Configure channels on a Cerebus device.", + ) + parser.add_argument( + "--device", default="NPLAY", + help="Device type (default: NPLAY). " + f"Choices: {', '.join(dt.name for dt in DeviceType)}", + ) + parser.add_argument( + "--type", dest="channel_type", default="FRONTEND", + help="Channel type to configure (default: FRONTEND). " + f"Choices: {', '.join(ct.name for ct in ChannelType)}", + ) + parser.add_argument( + "--rate", default="SR_RAW", + help="Sample rate group (default: SR_RAW). " + f"Choices: {', '.join(r.name for r in SampleRate)}", + ) + parser.add_argument( + "--n-chans", type=int, default=0, + help="Number of channels to configure (default: 0 = all available).", + ) + parser.add_argument( + "--disable-spikes", action="store_true", + help="Disable spike sorting on configured channels.", + ) + parser.add_argument( + "--dc-coupling", action="store_true", + help="Switch to DC input coupling on configured channels.", + ) + parser.add_argument( + "--timeout", type=float, default=10.0, + help="Connection timeout in seconds (default: 10).", + ) + args = parser.parse_args(argv) + + try: + device_type = _coerce_enum(DeviceType, args.device) + except (ValueError, TypeError) as e: + parser.error(str(e)) + return 1 + + try: + channel_type = _coerce_enum(ChannelType, args.channel_type) + except (ValueError, TypeError) as e: + parser.error(str(e)) + return 1 + + try: + rate = _coerce_enum(SampleRate, args.rate, _RATE_ALIASES) + except (ValueError, TypeError) as e: + parser.error(str(e)) + return 1 + + try: + configure( + device_type, channel_type, rate, args.n_chans, + disable_spikes=args.disable_spikes, + dc_coupling=args.dc_coupling, + timeout=args.timeout, + ) + return 0 + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pycbsdk/src/pycbsdk/cli/scan_devices.py b/pycbsdk/src/pycbsdk/cli/scan_devices.py new file mode 100644 index 00000000..10e45536 --- /dev/null +++ b/pycbsdk/src/pycbsdk/cli/scan_devices.py @@ -0,0 +1,230 @@ +"""Scan for Cerebus devices and report configuration. + +Usage:: + + python -m pycbsdk.cli.scan_devices # scan all known device types + python -m pycbsdk.cli.scan_devices NPLAY HUB1 # scan specific devices + python -m pycbsdk.cli.scan_devices --timeout 5 # custom timeout (seconds) +""" + +from __future__ import annotations + +import argparse +import sys +import time +from collections import Counter + +from pycbsdk import ( + Session, DeviceType, ChannelType, SampleRate, + ChanInfoField, ProtocolVersion, +) + +# Spike processing extract bit (cbAINPSPK_EXTRACT) +_SPKOPTS_EXTRACT = 0x0000_0001 + + +def _device_type_names() -> list[str]: + """Return device type names in scan order, excluding CUSTOM.""" + return [dt.name for dt in DeviceType if dt != DeviceType.CUSTOM] + + +def _format_hz(hz: int) -> str: + if hz >= 1000: + return f"{hz // 1000}kHz" + return f"{hz}Hz" + + +def scan_device(device_type: DeviceType, timeout: float = 3.0) -> dict | None: + """Try to connect to a device and collect its configuration. + + Returns a dict of info on success, or None if connection fails. + """ + try: + with Session(device_type=device_type) as session: + # Give the handshake time to complete + deadline = time.monotonic() + timeout + while not session.running and time.monotonic() < deadline: + time.sleep(0.1) + + if not session.running: + return None + + info: dict = {} + info["device_type"] = device_type.name + info["protocol_version"] = session.protocol_version + info["proc_ident"] = session.proc_ident + info["runlevel"] = session.runlevel + info["sysfreq"] = session.sysfreq + info["spike_length"] = session.spike_length + info["spike_pretrigger"] = session.spike_pretrigger + info["clock_offset_ns"] = session.clock_offset_ns + info["clock_uncertainty_ns"] = session.clock_uncertainty_ns + + # --- Channel summary by type --- + chan_summary = {} + for ct in ChannelType: + ids = session.get_matching_channel_ids(ct) + if not ids: + continue + groups = session.get_channels_field(ct, ChanInfoField.SMPGROUP) + spkopts = session.get_channels_field(ct, ChanInfoField.SPKOPTS) + + # Count channels enabled in each sample group + group_counts: Counter[int] = Counter() + for g in groups: + if g > 0: + group_counts[g] += 1 + + enabled_by_rate: dict[str, int] = {} + for gid, cnt in sorted(group_counts.items()): + try: + rate = SampleRate(gid) + label = _format_hz(rate.hz) + except ValueError: + label = f"group{gid}" + enabled_by_rate[label] = cnt + + spike_enabled = sum(1 for s in spkopts if s & _SPKOPTS_EXTRACT) + + chan_summary[ct.name] = { + "total": len(ids), + "sampling_enabled": sum(group_counts.values()), + "by_rate": enabled_by_rate, + "spike_enabled": spike_enabled, + } + info["channels"] = chan_summary + + # --- Sample group summary --- + group_summary = {} + for gid in range(1, 7): + chans = session.get_group_channels(gid) + if not chans: + continue + label = session.get_group_label(gid) or "" + try: + rate = SampleRate(gid) + hz = rate.hz + except ValueError: + hz = 0 + group_summary[gid] = { + "label": label, + "rate": _format_hz(hz) if hz else "?", + "n_channels": len(chans), + } + info["groups"] = group_summary + + stats = session.stats + info["packets_received"] = stats.packets_received + info["packets_dropped"] = stats.packets_dropped + + return info + + except Exception as e: + # Connection failed — device not present + return {"error": str(e), "device_type": device_type.name} + + +def print_report(info: dict) -> None: + """Print a human-readable report for one device.""" + dt = info["device_type"] + if "error" in info: + print(f" {dt}: connection failed ({info['error']})") + return + + proto = info["protocol_version"] + proto_str = proto.name if isinstance(proto, ProtocolVersion) else str(proto) + ident = info["proc_ident"] or "(unknown)" + + print(f" {dt}: {ident}") + print(f" Protocol: {proto_str}") + print(f" Runlevel: {info['runlevel']}") + print(f" System freq: {info['sysfreq']} Hz") + print(f" Spike length: {info['spike_length']} samples " + f"(pretrigger: {info['spike_pretrigger']})") + + offset = info["clock_offset_ns"] + uncertainty = info["clock_uncertainty_ns"] + if offset is not None: + print(f" Clock offset: {offset} ns " + f"(uncertainty: {uncertainty} ns)") + else: + print(f" Clock offset: (not synced)") + + print(f" Packets: {info['packets_received']} received, " + f"{info['packets_dropped']} dropped") + + channels = info.get("channels", {}) + if channels: + print(f" Channels:") + for ct_name, summary in channels.items(): + total = summary["total"] + enabled = summary["sampling_enabled"] + spike = summary["spike_enabled"] + by_rate = summary["by_rate"] + + rate_parts = [f"{cnt}@{rate}" for rate, cnt in by_rate.items()] + rate_str = ", ".join(rate_parts) if rate_parts else "none" + + parts = [f"{total} total"] + if enabled > 0: + parts.append(f"{enabled} sampling ({rate_str})") + if spike > 0: + parts.append(f"{spike} spike-enabled") + + print(f" {ct_name:12s} {', '.join(parts)}") + + groups = info.get("groups", {}) + if groups: + print(f" Sample groups:") + for gid, g in groups.items(): + label = g["label"] + print(f" Group {gid} ({g['rate']:>5s}): " + f"{g['n_channels']} channels" + f"{f' [{label}]' if label else ''}") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python -m pycbsdk.cli.scan_devices", + description="Scan for Cerebus devices and report configuration.", + ) + parser.add_argument( + "devices", nargs="*", metavar="DEVICE", + help=f"Device types to scan (default: all). " + f"Choices: {', '.join(_device_type_names())}", + ) + parser.add_argument( + "--timeout", type=float, default=3.0, + help="Connection timeout per device in seconds (default: 3)", + ) + args = parser.parse_args(argv) + + if args.devices: + try: + targets = [DeviceType[d.upper()] for d in args.devices] + except KeyError as e: + parser.error(f"Unknown device type: {e}. " + f"Choices: {', '.join(_device_type_names())}") + return 1 # unreachable + else: + targets = [dt for dt in DeviceType if dt != DeviceType.CUSTOM] + + print(f"Scanning {len(targets)} device type(s)...\n") + + found = 0 + for dt in targets: + info = scan_device(dt, timeout=args.timeout) + if info is None: + print(f" {dt.name}: no response") + else: + if "error" not in info: + found += 1 + print_report(info) + print() + + print(f"Found {found} device(s).") + return 0 if found > 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pycbsdk/src/pycbsdk/cli/spike_rates.py b/pycbsdk/src/pycbsdk/cli/spike_rates.py new file mode 100644 index 00000000..1bd9896c --- /dev/null +++ b/pycbsdk/src/pycbsdk/cli/spike_rates.py @@ -0,0 +1,241 @@ +"""Live spike rate monitor for Cerebus devices. + +Connects to a device, listens for spike events, and periodically prints +per-channel firing rate statistics. + +Usage:: + + python -m pycbsdk.cli.spike_rates # defaults: NPLAY, 1s window + python -m pycbsdk.cli.spike_rates HUB1 # specify device + python -m pycbsdk.cli.spike_rates --interval 2.0 # update every 2s + python -m pycbsdk.cli.spike_rates --top 10 # show top-10 channels +""" + +from __future__ import annotations + +import argparse +import sys +import time +import threading +from collections import deque + +from pycbsdk import Session, DeviceType, ChannelType, ChanInfoField + + +# cbAINPSPK flags +_SPKOPTS_EXTRACT = 0x0000_0001 +_SPKOPTS_THRAUTO = 0x0000_0400 +_SPKOPTS_HOOPSORT = 0x0001_0000 +_SPKOPTS_PCASORT = 0x0006_0000 # all PCA manual + auto + + +def _format_rate(hz: float) -> str: + if hz >= 100: + return f"{hz:7.0f}" + elif hz >= 10: + return f"{hz:7.1f}" + else: + return f"{hz:7.2f}" + + +class SpikeRateMonitor: + """Accumulates spike events and computes windowed firing rates.""" + + def __init__(self, session: Session, channel_ids: list[int], + window_sec: float = 1.0): + self._session = session + self._window_sec = window_sec + self._channel_ids = channel_ids + self._ch_index = {ch: i for i, ch in enumerate(channel_ids)} + n = len(channel_ids) + + self._lock = threading.Lock() + # Per-channel ring buffers of monotonic-clock spike times (seconds). + # Converted from device timestamps via session.device_to_monotonic(), + # falling back to time.monotonic() if clock sync isn't ready. + self._spike_times: list[deque[float]] = [deque() for _ in range(n)] + self._total_spikes = [0] * n + + def on_spike(self, header, data) -> None: + """Callback for spike event packets.""" + chid = header.chid + idx = self._ch_index.get(chid) + if idx is None: + return + + now = time.monotonic() + try: + t = self._session.device_to_monotonic(header.time) + # Reject if converted time is far from wall clock (e.g. clock sync + # not converged, or NPlay looping with stale offset). + if abs(t - now) > self._window_sec * 2: + t = now + except Exception: + t = now + + with self._lock: + self._spike_times[idx].append(t) + self._total_spikes[idx] += 1 + + def _prune(self) -> None: + """Remove spikes older than the window. Must hold self._lock.""" + cutoff = time.monotonic() - self._window_sec + for buf in self._spike_times: + while buf and buf[0] < cutoff: + buf.popleft() + + def snapshot(self) -> dict: + """Return current rate statistics.""" + with self._lock: + self._prune() + counts = [len(buf) for buf in self._spike_times] + total = list(self._total_spikes) + + rates = [c / self._window_sec for c in counts] + active = [(self._channel_ids[i], rates[i], total[i]) + for i in range(len(self._channel_ids)) if counts[i] > 0] + active.sort(key=lambda x: x[1], reverse=True) + + n_active = len(active) + mean_rate = sum(rates) / len(rates) if rates else 0.0 + total_rate = sum(rates) + max_rate = max(rates) if rates else 0.0 + min_active = min(r for _, r, _ in active) if active else 0.0 + total_spikes = sum(total) + + return { + "rates": rates, + "active": active, + "n_channels": len(self._channel_ids), + "n_active": n_active, + "mean_rate": mean_rate, + "total_rate": total_rate, + "max_rate": max_rate, + "min_active_rate": min_active, + "total_spikes": total_spikes, + } + + +def print_stats(snap: dict, top_n: int = 0, show_channels: bool = True) -> None: + """Print a compact rate summary.""" + n_ch = snap["n_channels"] + n_active = snap["n_active"] + mean = snap["mean_rate"] + total = snap["total_rate"] + active = snap["active"] + + # Header line + print(f"\033[2J\033[H", end="") # clear screen + print(f"Spike Rate Monitor | {n_active}/{n_ch} channels active | " + f"{snap['total_spikes']} total spikes") + print(f"{'─' * 72}") + + # Aggregate stats + if n_active > 0: + print(f" Mean rate (all channels): {_format_rate(mean)} Hz") + print(f" Mean rate (active only): {_format_rate(total / n_active)} Hz") + print(f" Total spike rate: {_format_rate(total)} Hz") + print(f" Range (active): {_format_rate(snap['min_active_rate'])} – " + f"{_format_rate(snap['max_rate'])} Hz") + else: + print(f" No spikes detected in window.") + + if show_channels and active: + print(f"\n {'Chan':>6s} {'Rate (Hz)':>9s} {'Total':>8s} {'Bar'}") + print(f" {'─' * 6} {'─' * 9} {'─' * 8} {'─' * 40}") + + display = active[:top_n] if top_n > 0 else active + max_rate = active[0][1] if active else 1.0 + for chid, rate, total_count in display: + bar_len = int(40 * rate / max_rate) if max_rate > 0 else 0 + bar = "█" * bar_len + print(f" {chid:>6d} {_format_rate(rate)} {total_count:>8d} {bar}") + + if top_n > 0 and len(active) > top_n: + print(f" ... and {len(active) - top_n} more active channels") + + print() + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python -m pycbsdk.cli.spike_rates", + description="Live spike rate monitor for Cerebus devices.", + ) + parser.add_argument( + "device", nargs="?", default="NPLAY", + help="Device type (default: NPLAY)", + ) + parser.add_argument( + "--interval", "-i", type=float, default=1.0, + help="Update interval in seconds (default: 1.0)", + ) + parser.add_argument( + "--window", "-w", type=float, default=None, + help="Rate window in seconds (default: same as interval)", + ) + parser.add_argument( + "--top", "-n", type=int, default=0, + help="Show only top N channels by rate (default: all active)", + ) + parser.add_argument( + "--no-channels", action="store_true", + help="Hide per-channel breakdown, show only aggregate stats", + ) + args = parser.parse_args(argv) + + window = args.window if args.window is not None else args.interval + + try: + device_type = DeviceType[args.device.upper()] + except KeyError: + names = ", ".join(dt.name for dt in DeviceType if dt != DeviceType.CUSTOM) + parser.error(f"Unknown device: {args.device}. Choices: {names}") + return 1 + + print(f"Connecting to {device_type.name}...") + try: + with Session(device_type=device_type) as session: + # Find spike-enabled frontend channels + fe_ids = session.get_matching_channel_ids(ChannelType.FRONTEND) + spkopts = session.get_channels_field(ChannelType.FRONTEND, ChanInfoField.SPKOPTS) + spike_ids = [ch for ch, opts in zip(fe_ids, spkopts) + if opts & _SPKOPTS_EXTRACT] + + if not spike_ids: + print("No channels with spike processing enabled.") + return 1 + + # Summarise threshold config + n_auto = sum(1 for opts in spkopts if opts & _SPKOPTS_EXTRACT and opts & _SPKOPTS_THRAUTO) + n_manual = len(spike_ids) - n_auto + n_sorting = sum(1 for opts in spkopts + if opts & _SPKOPTS_EXTRACT and opts & (_SPKOPTS_HOOPSORT | _SPKOPTS_PCASORT)) + print(f"Monitoring {len(spike_ids)} spike-enabled channels " + f"({n_auto} auto-threshold, {n_manual} manual-threshold, " + f"{n_sorting} with sorting)") + + monitor = SpikeRateMonitor(session, spike_ids, window_sec=window) + + @session.on_event(ChannelType.FRONTEND) + def _on_spike(header, data): + monitor.on_spike(header, data) + + try: + while True: + time.sleep(args.interval) + snap = monitor.snapshot() + print_stats(snap, top_n=args.top, + show_channels=not args.no_channels) + except KeyboardInterrupt: + print("\nStopped.") + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pycbsdk/src/pycbsdk/session.py b/pycbsdk/src/pycbsdk/session.py new file mode 100644 index 00000000..56aba0c6 --- /dev/null +++ b/pycbsdk/src/pycbsdk/session.py @@ -0,0 +1,1618 @@ +""" +Pythonic session wrapper for the CereLink SDK. +""" + +from __future__ import annotations + +import enum +import time as _time +import threading +from dataclasses import dataclass, field +from typing import Callable, Optional + +from ._lib import ffi, load_library + +lib = None # Lazy-loaded + + +def _get_lib(): + global lib + if lib is None: + lib = load_library() + return lib + + +class DeviceType(enum.IntEnum): + """Device type selection. + + Values match ``cbproto_device_type_t``. + """ + LEGACY_NSP = 0 + NSP = 1 + HUB1 = 2 + HUB2 = 3 + HUB3 = 4 + NPLAY = 5 + CUSTOM = 6 + +class ChannelType(enum.IntEnum): + """Channel type classification. + + Values match ``cbproto_channel_type_t``. + """ + FRONTEND = 0 + ANALOG_IN = 1 + ANALOG_OUT = 2 + AUDIO = 3 + DIGITAL_IN = 4 + SERIAL = 5 + DIGITAL_OUT = 6 + +class SampleRate(enum.IntEnum): + """Continuous sampling rate selection. + + Values match ``cbproto_group_rate_t`` so they can be passed directly to cffi. + """ + NONE = 0 + SR_500 = 1 # 500 Hz + SR_1kHz = 2 # 1 000 Hz + SR_2kHz = 3 # 2 000 Hz + SR_10kHz = 4 # 10 000 Hz + SR_30kHz = 5 # 30 000 Hz + SR_RAW = 6 # Raw (30 000 Hz) + + @property + def hz(self) -> int: + """Sample rate in Hz.""" + return _RATE_HZ[self] + + +class ProtocolVersion(enum.IntEnum): + """Protocol version detected during device handshake. + + Values match ``cbproto_protocol_version_t``. + """ + UNKNOWN = 0 + V3_11 = 1 # Legacy 32-bit timestamps + V4_0 = 2 # Legacy 64-bit timestamps + V4_1 = 3 # 64-bit timestamps, 16-bit packet types + CURRENT = 4 # 4.2+ (current) + + +class ChanInfoField(enum.IntEnum): + """Channel info field selector for bulk extraction. + + Values match ``cbsdk_chaninfo_field_t``. + """ + SMPGROUP = 0 + SMPFILTER = 1 + SPKFILTER = 2 + AINPOPTS = 3 + SPKOPTS = 4 + SPKTHRLEVEL = 5 + LNCRATE = 6 + REFELECCHAN = 7 + AMPLREJPOS = 8 + AMPLREJNEG = 9 + CHANCAPS = 10 + BANK = 11 + TERM = 12 + + +_RATE_HZ = { + SampleRate.NONE: 0, + SampleRate.SR_500: 500, + SampleRate.SR_1kHz: 1000, + SampleRate.SR_2kHz: 2000, + SampleRate.SR_10kHz: 10000, + SampleRate.SR_30kHz: 30000, + SampleRate.SR_RAW: 30000, +} + +# Aliases for lenient string → SampleRate coercion (user might type "30kHz" +# instead of "SR_30kHz"). +_RATE_ALIASES = { + "500HZ": SampleRate.SR_500, + "1KHZ": SampleRate.SR_1kHz, + "2KHZ": SampleRate.SR_2kHz, + "10KHZ": SampleRate.SR_10kHz, + "30KHZ": SampleRate.SR_30kHz, + "RAW": SampleRate.SR_RAW, +} + + +def _coerce_enum(enum_cls, value, aliases=None): + """Coerce *value* to *enum_cls*, accepting enum members, ints, or strings. + + String lookup is case-insensitive: tries the canonical member name first, + then *aliases* (if provided). + """ + if isinstance(value, enum_cls): + return value + if isinstance(value, int): + return enum_cls(value) + if isinstance(value, str): + key = value.upper() + # Exact member name match (e.g., "FRONTEND", "SR_30kHz") + try: + return enum_cls[key] + except KeyError: + pass + # Alias match (e.g., "30kHz" → SR_30kHz) + if aliases and key in aliases: + return aliases[key] + members = ", ".join(enum_cls.__members__) + extra = "" + if aliases: + extra = " (or: " + ", ".join( + k for k in aliases if k not in enum_cls.__members__ + ) + ")" + raise ValueError( + f"Unknown {enum_cls.__name__}: {value!r}. " + f"Must be one of: {members}{extra}" + ) + raise TypeError( + f"Expected {enum_cls.__name__}, int, or str, got {type(value).__name__}" + ) + + +def _check(result: int, msg: str = ""): + """Check a cbsdk_result_t and raise on error.""" + if result != 0: + _lib = _get_lib() + err = ffi.string(_lib.cbsdk_get_error_message(result)).decode() + raise RuntimeError(f"{msg}: {err}" if msg else err) + + +@dataclass +class Stats: + """SDK session statistics.""" + packets_received: int = 0 + bytes_received: int = 0 + packets_to_shmem: int = 0 + packets_queued: int = 0 + packets_delivered: int = 0 + packets_dropped: int = 0 + queue_depth: int = 0 + queue_max_depth: int = 0 + packets_sent: int = 0 + shmem_errors: int = 0 + receive_errors: int = 0 + send_errors: int = 0 + + +class Session: + """CereLink SDK session. + + Connects to a Blackrock Neurotech Cerebus device (or attaches to an + existing session's shared memory) and delivers packets via callbacks. + + Args: + device_type: Device type (e.g., ``DeviceType.HUB1``). + callback_queue_depth: Number of packets to buffer (default: 16384). + + Example:: + + session = Session(DeviceType.HUB1) + + @session.on_event(ChannelType.FRONTEND) + def on_spike(header, data): + print(f"Spike on ch {header.chid}") + + # ... do work ... + + session.close() + + Can also be used as a context manager:: + + with Session(DeviceType.HUB1) as session: + # session is connected and running + ... + """ + + def __init__( + self, + device_type: DeviceType = DeviceType.LEGACY_NSP, + callback_queue_depth: int = 16384, + ): + _lib = _get_lib() + + config = _lib.cbsdk_config_default() + config.device_type = int(_coerce_enum(DeviceType, device_type)) + config.callback_queue_depth = callback_queue_depth + + session_p = ffi.new("cbsdk_session_t *") + _check(_lib.cbsdk_session_create(session_p, ffi.addressof(config)), "Failed to create session") + self._session = session_p[0] + self._handles: list[int] = [] + # prevent Python callback pointers from being garbage collected + self._callback_refs: list = [] + self._lock = threading.Lock() + self._closed = False + # Calibrate monotonic ↔ steady_clock offset for device_to_monotonic() + self._mono_to_steady_offset_ns = self._calibrate_monotonic_offset() + + def __enter__(self): + return self + + def __exit__(self, *exc): + self.close() + + def close(self): + """Stop and destroy the session.""" + if self._closed: + return + self._closed = True + _lib = _get_lib() + # Unregister all callbacks first to avoid calling into dead Python objects + for handle in self._handles: + _lib.cbsdk_session_unregister_callback(self._session, handle) + self._handles.clear() + self._callback_refs.clear() + _lib.cbsdk_session_destroy(self._session) + self._session = ffi.NULL + + @property + def running(self) -> bool: + """Whether the session is running.""" + return bool(_get_lib().cbsdk_session_is_running(self._session)) + + # --- Callbacks --- + + def on_event( + self, channel_type: ChannelType | None = ChannelType.FRONTEND + ) -> Callable: + """Decorator to register a callback for event packets (spikes, etc.). + + The callback receives ``(header, data)`` where ``header`` is the packet + header (with ``.time``, ``.chid``, ``.type``, ``.dlen``) and ``data`` + is a cffi buffer of the raw payload bytes. + + Args: + channel_type: Channel type filter, or ``None`` for all event + channels. + """ + ct = None if channel_type is None else _coerce_enum(ChannelType, channel_type) + def decorator(fn): + self._register_event_callback(ct, fn) + return fn + return decorator + + def on_group(self, rate: SampleRate = SampleRate.SR_30kHz, *, as_array: bool = False) -> Callable: + """Decorator to register a callback for continuous sample group packets. + + The callback receives ``(header, data)`` where ``data`` is either a + cffi pointer to ``int16_t[N]`` or (if *as_array* is True) a numpy + ``int16`` array of shape ``(n_channels,)``. + + Args: + rate: Sample rate to subscribe to. + as_array: If True, deliver data as a numpy int16 array (zero-copy). + Requires numpy. + """ + rate = _coerce_enum(SampleRate, rate, _RATE_ALIASES) + def decorator(fn): + if as_array: + self._register_group_callback_numpy(int(rate), fn) + else: + self._register_group_callback(int(rate), fn) + return fn + return decorator + + def on_config(self, packet_type: int) -> Callable: + """Decorator to register a callback for config/system packets. + + Args: + packet_type: Packet type to match. + """ + def decorator(fn): + self._register_config_callback(packet_type, fn) + return fn + return decorator + + def on_packet(self) -> Callable: + """Decorator to register a callback for ALL packets (catch-all).""" + def decorator(fn): + self._register_packet_callback(fn) + return fn + return decorator + + def on_error(self, fn: Callable[[str], None]): + """Register a callback for errors. + + Args: + fn: Called with an error message string. + """ + _lib = _get_lib() + + @ffi.callback("void(const char*, void*)") + def c_error_cb(error_message, user_data): + try: + fn(ffi.string(error_message).decode()) + except Exception: + pass # Never let exceptions propagate into C + + _lib.cbsdk_session_set_error_callback(self._session, c_error_cb, ffi.NULL) + self._callback_refs.append(c_error_cb) + + def _register_event_callback(self, channel_type: ChannelType | None, fn): + _lib = _get_lib() + if channel_type is None: + c_channel_type = ffi.cast("cbproto_channel_type_t", -1) + else: + c_channel_type = int(channel_type) + + @ffi.callback("void(const cbPKT_GENERIC*, void*)") + def c_event_cb(pkt, user_data): + try: + fn(pkt.cbpkt_header, pkt.data_u8) + except Exception: + pass + + handle = _lib.cbsdk_session_register_event_callback( + self._session, c_channel_type, c_event_cb, ffi.NULL + ) + if handle == 0: + raise RuntimeError("Failed to register event callback") + self._handles.append(handle) + self._callback_refs.append(c_event_cb) + + def _register_group_callback(self, rate: int, fn): + _lib = _get_lib() + + @ffi.callback("void(const cbPKT_GROUP*, void*)") + def c_group_cb(pkt, user_data): + try: + fn(pkt.cbpkt_header, pkt.data) + except Exception: + pass + + handle = _lib.cbsdk_session_register_group_callback( + self._session, int(rate), c_group_cb, ffi.NULL + ) + if handle == 0: + raise RuntimeError("Failed to register group callback") + self._handles.append(handle) + self._callback_refs.append(c_group_cb) + + def _register_group_callback_numpy(self, rate: int, fn): + from ._numpy import group_data_as_array + + _lib = _get_lib() + channels = self.get_group_channels(rate) + n_ch = len(channels) + + @ffi.callback("void(const cbPKT_GROUP*, void*)") + def c_group_cb(pkt, user_data): + try: + n = n_ch if n_ch > 0 else pkt.cbpkt_header.dlen * 2 + arr = group_data_as_array(pkt.data, n) + fn(pkt.cbpkt_header, arr) + except Exception: + pass + + handle = _lib.cbsdk_session_register_group_callback( + self._session, int(rate), c_group_cb, ffi.NULL + ) + if handle == 0: + raise RuntimeError("Failed to register group callback") + self._handles.append(handle) + self._callback_refs.append(c_group_cb) + + def _register_config_callback(self, packet_type: int, fn): + _lib = _get_lib() + + @ffi.callback("void(const cbPKT_GENERIC*, void*)") + def c_config_cb(pkt, user_data): + try: + fn(pkt.cbpkt_header, pkt.data_u32) + except Exception: + pass + + handle = _lib.cbsdk_session_register_config_callback( + self._session, packet_type, c_config_cb, ffi.NULL + ) + if handle == 0: + raise RuntimeError("Failed to register config callback") + self._handles.append(handle) + self._callback_refs.append(c_config_cb) + + def _register_packet_callback(self, fn): + _lib = _get_lib() + + @ffi.callback("void(const cbPKT_GENERIC*, size_t, void*)") + def c_packet_cb(pkts, count, user_data): + try: + for i in range(count): + pkt = pkts[i] + fn(pkt.cbpkt_header, pkt.data_u8) + except Exception: + pass + + handle = _lib.cbsdk_session_register_packet_callback( + self._session, c_packet_cb, ffi.NULL + ) + if handle == 0: + raise RuntimeError("Failed to register packet callback") + self._handles.append(handle) + self._callback_refs.append(c_packet_cb) + + # --- Stats --- + + @property + def stats(self) -> Stats: + """Get current session statistics.""" + _lib = _get_lib() + c_stats = ffi.new("cbsdk_stats_t *") + _lib.cbsdk_session_get_stats(self._session, c_stats) + return Stats( + packets_received=c_stats.packets_received_from_device, + bytes_received=c_stats.bytes_received_from_device, + packets_to_shmem=c_stats.packets_stored_to_shmem, + packets_queued=c_stats.packets_queued_for_callback, + packets_delivered=c_stats.packets_delivered_to_callback, + packets_dropped=c_stats.packets_dropped, + queue_depth=c_stats.queue_current_depth, + queue_max_depth=c_stats.queue_max_depth, + packets_sent=c_stats.packets_sent_to_device, + shmem_errors=c_stats.shmem_store_errors, + receive_errors=c_stats.receive_errors, + send_errors=c_stats.send_errors, + ) + + def reset_stats(self): + """Reset statistics counters to zero.""" + _get_lib().cbsdk_session_reset_stats(self._session) + + # --- Configuration Access --- + + @property + def runlevel(self) -> int: + """Get the current device run level.""" + return _get_lib().cbsdk_session_get_runlevel(self._session) + + @property + def protocol_version(self) -> ProtocolVersion: + """Protocol version detected during device handshake. + + Returns ``ProtocolVersion.UNKNOWN`` in CLIENT mode (no device session). + """ + v = _get_lib().cbsdk_session_get_protocol_version(self._session) + try: + return ProtocolVersion(v) + except ValueError: + return ProtocolVersion.UNKNOWN + + @property + def proc_ident(self) -> str: + """Processor identification string from PROCREP (e.g. 'Gemini Hub 1'). + + Returns empty string if unavailable (e.g. CLIENT mode or no PROCREP received). + """ + buf = ffi.new("char[64]") + _get_lib().cbsdk_session_get_proc_ident(self._session, buf, 64) + return ffi.string(buf).decode("utf-8", errors="replace") + + @property + def spike_length(self) -> int: + """Global spike event length in samples.""" + return _get_lib().cbsdk_session_get_spike_length(self._session) + + @property + def spike_pretrigger(self) -> int: + """Global spike pre-trigger length in samples.""" + return _get_lib().cbsdk_session_get_spike_pretrigger(self._session) + + def set_spike_length(self, spike_length: int, spike_pretrigger: int): + """Set the global spike event length and pre-trigger. + + Args: + spike_length: Total spike waveform length in samples. + spike_pretrigger: Pre-trigger samples (must be < spike_length). + """ + _check( + _get_lib().cbsdk_session_set_spike_length( + self._session, spike_length, spike_pretrigger + ), + "Failed to set spike length", + ) + + @staticmethod + def max_chans() -> int: + """Total number of channels (cbMAXCHANS).""" + return _get_lib().cbsdk_get_max_chans() + + @staticmethod + def num_fe_chans() -> int: + """Number of front-end channels.""" + return _get_lib().cbsdk_get_num_fe_chans() + + @staticmethod + def num_analog_chans() -> int: + """Number of analog channels (front-end + analog input).""" + return _get_lib().cbsdk_get_num_analog_chans() + + def get_channel_label(self, chan_id: int) -> Optional[str]: + """Get a channel's label (1-based channel ID).""" + _lib = _get_lib() + ptr = _lib.cbsdk_session_get_channel_label(self._session, chan_id) + if ptr == ffi.NULL: + return None + return ffi.string(ptr).decode() + + def get_channel_smpgroup(self, chan_id: int) -> int: + """Get a channel's sample group (0 = disabled, 1-6).""" + return _get_lib().cbsdk_session_get_channel_smpgroup(self._session, chan_id) + + def get_channel_chancaps(self, chan_id: int) -> int: + """Get a channel's capability flags.""" + return _get_lib().cbsdk_session_get_channel_chancaps(self._session, chan_id) + + def get_channel_type(self, chan_id: int) -> Optional[ChannelType]: + """Get a channel's type classification. + + Returns a :class:`ChannelType` member, or ``None`` if the channel is + invalid or not connected. + """ + _lib = _get_lib() + result = _lib.cbsdk_session_get_channel_type(self._session, chan_id) + ct = int(ffi.cast("int", result)) + try: + return ChannelType(ct) + except ValueError: + return None + + def get_channel_smpfilter(self, chan_id: int) -> int: + """Get a channel's continuous-time pathway filter ID.""" + return _get_lib().cbsdk_session_get_channel_smpfilter(self._session, chan_id) + + def get_channel_spkfilter(self, chan_id: int) -> int: + """Get a channel's spike pathway filter ID.""" + return _get_lib().cbsdk_session_get_channel_spkfilter(self._session, chan_id) + + def get_channel_spkopts(self, chan_id: int) -> int: + """Get a channel's spike processing options (cbAINPSPK_* flags).""" + return _get_lib().cbsdk_session_get_channel_spkopts(self._session, chan_id) + + def get_channel_spkthrlevel(self, chan_id: int) -> int: + """Get a channel's spike threshold level.""" + return _get_lib().cbsdk_session_get_channel_spkthrlevel(self._session, chan_id) + + def get_channel_ainpopts(self, chan_id: int) -> int: + """Get a channel's analog input options (cbAINP_* flags).""" + return _get_lib().cbsdk_session_get_channel_ainpopts(self._session, chan_id) + + def get_channel_lncrate(self, chan_id: int) -> int: + """Get a channel's line noise cancellation adaptation rate.""" + return _get_lib().cbsdk_session_get_channel_lncrate(self._session, chan_id) + + def get_channel_refelecchan(self, chan_id: int) -> int: + """Get a channel's software reference electrode channel.""" + return _get_lib().cbsdk_session_get_channel_refelecchan(self._session, chan_id) + + def get_channel_amplrejpos(self, chan_id: int) -> int: + """Get a channel's positive amplitude rejection threshold.""" + return _get_lib().cbsdk_session_get_channel_amplrejpos(self._session, chan_id) + + def get_channel_amplrejneg(self, chan_id: int) -> int: + """Get a channel's negative amplitude rejection threshold.""" + return _get_lib().cbsdk_session_get_channel_amplrejneg(self._session, chan_id) + + def get_channel_scaling(self, chan_id: int) -> Optional[dict]: + """Get a channel's input scaling information. + + Args: + chan_id: 1-based channel ID. + + Returns: + Dict with keys ``digmin``, ``digmax``, ``anamin``, ``anamax``, + ``anagain``, ``anaunit``, or ``None`` if the channel is invalid. + """ + _lib = _get_lib() + scaling = ffi.new("cbsdk_channel_scaling_t *") + result = _lib.cbsdk_session_get_channel_scaling(self._session, chan_id, scaling) + if result != 0: + return None + return { + "digmin": scaling.digmin, + "digmax": scaling.digmax, + "anamin": scaling.anamin, + "anamax": scaling.anamax, + "anagain": scaling.anagain, + "anaunit": ffi.string(scaling.anaunit).decode(), + } + + def get_channel_field(self, chan_id: int, field: ChanInfoField) -> int: + """Get any numeric field from a single channel by field selector. + + This is the generic counterpart to the dedicated per-channel getters. + Useful when the field is determined at runtime. + + Args: + chan_id: 1-based channel ID. + field: Which field to extract (e.g., ``ChanInfoField.BANK``). + + Returns: + Field value as int (widened from the native type). + """ + return _get_lib().cbsdk_session_get_channel_field( + self._session, chan_id, int(field)) + + def get_group_label(self, group_id: int) -> Optional[str]: + """Get a sample group's label (group_id 1-6).""" + _lib = _get_lib() + ptr = _lib.cbsdk_session_get_group_label(self._session, group_id) + if ptr == ffi.NULL: + return None + return ffi.string(ptr).decode() + + def get_group_channels(self, group_id: int) -> list[int]: + """Get the list of channel IDs in a sample group.""" + _lib = _get_lib() + max_chans = _lib.cbsdk_get_num_analog_chans() + buf = ffi.new(f"uint16_t[{max_chans}]") + count = ffi.new("uint32_t *", max_chans) + result = _lib.cbsdk_session_get_group_list(self._session, group_id, buf, count) + if result != 0: + return [] + return [buf[i] for i in range(count[0])] + + # --- Bulk Channel Queries --- + + def get_matching_channel_ids( + self, + channel_type: ChannelType, + n_chans: int = 0, + ) -> list[int]: + """Get 1-based IDs of channels matching a type. + + Args: + channel_type: Channel type filter (e.g., ``ChannelType.FRONTEND``). + n_chans: Max channels to return (0 or omit for all). + + Returns: + List of 1-based channel IDs. + """ + _lib = _get_lib() + max_chans = _lib.cbsdk_get_max_chans() + if n_chans <= 0: + n_chans = max_chans + buf = ffi.new(f"uint32_t[{max_chans}]") + count = ffi.new("uint32_t *", max_chans) + _check( + _lib.cbsdk_session_get_matching_channels( + self._session, n_chans, + int(_coerce_enum(ChannelType, channel_type)), + buf, count + ), + "Failed to get matching channel IDs", + ) + return [buf[i] for i in range(count[0])] + + def get_channels_field( + self, + channel_type: ChannelType, + field: ChanInfoField, + n_chans: int = 0, + ) -> list[int]: + """Get a numeric field from all channels matching a type. + + Args: + channel_type: Channel type filter (e.g., ``ChannelType.FRONTEND``). + field: Which field to extract (e.g., ``ChanInfoField.SMPGROUP``). + n_chans: Max channels to query (0 or omit for all). + + Returns: + List of field values (same order as :meth:`get_matching_channel_ids`). + """ + _lib = _get_lib() + max_chans = _lib.cbsdk_get_max_chans() + if n_chans <= 0: + n_chans = max_chans + buf = ffi.new(f"int64_t[{max_chans}]") + count = ffi.new("uint32_t *", max_chans) + _check( + _lib.cbsdk_session_get_channels_field( + self._session, n_chans, + int(_coerce_enum(ChannelType, channel_type)), + int(_coerce_enum(ChanInfoField, field)), + buf, count + ), + "Failed to get channel field", + ) + return [buf[i] for i in range(count[0])] + + def get_channels_labels( + self, + channel_type: ChannelType, + n_chans: int = 0, + ) -> list[str]: + """Get labels from all channels matching a type. + + Args: + channel_type: Channel type filter (e.g., ``ChannelType.FRONTEND``). + n_chans: Max channels to query (0 or omit for all). + + Returns: + List of label strings (same order as :meth:`get_matching_channel_ids`). + """ + _lib = _get_lib() + max_chans = _lib.cbsdk_get_max_chans() + if n_chans <= 0: + n_chans = max_chans + label_stride = 16 # cbLEN_STR_LABEL = 16 (including null) + buf = ffi.new(f"char[{max_chans * label_stride}]") + count = ffi.new("uint32_t *", max_chans) + _check( + _lib.cbsdk_session_get_channels_labels( + self._session, n_chans, + int(_coerce_enum(ChannelType, channel_type)), + buf, label_stride, count + ), + "Failed to get channel labels", + ) + result = [] + for i in range(count[0]): + s = ffi.string(buf + i * label_stride).decode() + result.append(s) + return result + + def get_channels_positions( + self, + channel_type: ChannelType, + n_chans: int = 0, + ) -> list[tuple[int, int, int, int]]: + """Get positions from all channels matching a type. + + Each position is a 4-tuple ``(x, y, z, w)`` of ``int32`` values, + corresponding to the ``cbPKT_CHANINFO.position[4]`` field. + + Args: + channel_type: Channel type filter (e.g., ``ChannelType.FRONTEND``). + n_chans: Max channels to query (0 or omit for all). + + Returns: + List of ``(x, y, z, w)`` tuples (same order as + :meth:`get_matching_channel_ids`). + """ + _lib = _get_lib() + max_chans = _lib.cbsdk_get_max_chans() + if n_chans <= 0: + n_chans = max_chans + buf = ffi.new(f"int32_t[{max_chans * 4}]") + count = ffi.new("uint32_t *", max_chans) + _check( + _lib.cbsdk_session_get_channels_positions( + self._session, n_chans, + int(_coerce_enum(ChannelType, channel_type)), + buf, count + ), + "Failed to get channel positions", + ) + result = [] + for i in range(count[0]): + base = i * 4 + result.append((buf[base], buf[base + 1], buf[base + 2], buf[base + 3])) + return result + + # --- Channel Configuration --- + + def set_channel_sample_group( + self, + n_chans: int, + channel_type: ChannelType, + rate: SampleRate, + disable_others: bool = False, + ): + """Set sampling rate for channels of a specific type. + + Args: + n_chans: Number of channels to configure. + channel_type: Channel type filter (e.g., ``ChannelType.FRONTEND``). + rate: Sample rate (e.g., ``SampleRate.SR_30kHz``, ``SampleRate.NONE`` + to disable). + disable_others: Disable sampling on unselected channels. + """ + _lib = _get_lib() + _check( + _lib.cbsdk_session_set_channel_sample_group( + self._session, n_chans, + int(_coerce_enum(ChannelType, channel_type)), + int(_coerce_enum(SampleRate, rate, _RATE_ALIASES)), + disable_others + ), + "Failed to set channel sample group", + ) + + def set_ac_input_coupling( + self, + n_chans: int, + channel_type: ChannelType, + enabled: bool, + ): + """Set AC/DC input coupling for channels of a specific type. + + Args: + n_chans: Number of channels to configure. + channel_type: Channel type filter (e.g., ``ChannelType.FRONTEND``). + enabled: ``True`` for AC coupling (offset correction on), + ``False`` for DC coupling. + """ + _lib = _get_lib() + _check( + _lib.cbsdk_session_set_ac_input_coupling( + self._session, n_chans, + int(_coerce_enum(ChannelType, channel_type)), + enabled + ), + "Failed to set AC input coupling", + ) + + def set_channel_label(self, chan_id: int, label: str): + """Set a channel's label.""" + _check( + _get_lib().cbsdk_session_set_channel_label( + self._session, chan_id, label.encode() + ), + "Failed to set channel label", + ) + + def set_channel_smpfilter(self, chan_id: int, filter_id: int): + """Set a channel's continuous-time pathway filter.""" + _check( + _get_lib().cbsdk_session_set_channel_smpfilter( + self._session, chan_id, filter_id + ), + "Failed to set channel smpfilter", + ) + + def set_channel_spkfilter(self, chan_id: int, filter_id: int): + """Set a channel's spike pathway filter.""" + _check( + _get_lib().cbsdk_session_set_channel_spkfilter( + self._session, chan_id, filter_id + ), + "Failed to set channel spkfilter", + ) + + def set_channel_ainpopts(self, chan_id: int, ainpopts: int): + """Set a channel's analog input options (cbAINP_* flags).""" + _check( + _get_lib().cbsdk_session_set_channel_ainpopts( + self._session, chan_id, ainpopts + ), + "Failed to set channel ainpopts", + ) + + def set_channel_lncrate(self, chan_id: int, rate: int): + """Set a channel's line noise cancellation adaptation rate.""" + _check( + _get_lib().cbsdk_session_set_channel_lncrate( + self._session, chan_id, rate + ), + "Failed to set channel lncrate", + ) + + def set_channel_spkopts(self, chan_id: int, spkopts: int): + """Set a channel's spike processing options (cbAINPSPK_* flags).""" + _check( + _get_lib().cbsdk_session_set_channel_spkopts( + self._session, chan_id, spkopts + ), + "Failed to set channel spkopts", + ) + + def set_channel_spkthrlevel(self, chan_id: int, level: int): + """Set a channel's spike threshold level.""" + _check( + _get_lib().cbsdk_session_set_channel_spkthrlevel( + self._session, chan_id, level + ), + "Failed to set channel spkthrlevel", + ) + + def set_channel_autothreshold(self, chan_id: int, enabled: bool): + """Enable or disable auto-thresholding for a channel.""" + _check( + _get_lib().cbsdk_session_set_channel_autothreshold( + self._session, chan_id, enabled + ), + "Failed to set channel autothreshold", + ) + + def configure_channel(self, chan_id: int, **kwargs): + """Configure one or more attributes of a single channel. + + This is a convenience method that dispatches to the individual setters. + Each keyword argument maps to a channel attribute. + + Args: + chan_id: 1-based channel ID. + + Keyword Args: + label (str): Channel label (max 15 chars). + smpgroup (SampleRate): Sample rate (e.g., ``SampleRate.SR_30kHz``). + smpfilter (int): Continuous-time filter ID. + spkfilter (int): Spike pathway filter ID. + ainpopts (int): Analog input option flags. + lncrate (int): LNC adaptation rate. + spkopts (int): Spike processing option flags. + spkthrlevel (int): Spike threshold level. + autothreshold (bool): Auto-threshold enable. + + Example:: + + session.configure_channel(1, + smpgroup=SampleRate.SR_30kHz, + smpfilter=6, + autothreshold=True, + ) + """ + _dispatch = { + "label": self.set_channel_label, + "smpfilter": self.set_channel_smpfilter, + "spkfilter": self.set_channel_spkfilter, + "ainpopts": self.set_channel_ainpopts, + "lncrate": self.set_channel_lncrate, + "spkopts": self.set_channel_spkopts, + "spkthrlevel": self.set_channel_spkthrlevel, + "autothreshold": self.set_channel_autothreshold, + } + for key, value in kwargs.items(): + if key == "smpgroup": + # Special case: smpgroup goes through the batch setter for one channel + chan_type = self.get_channel_type(chan_id) + if chan_type is not None: + _lib = _get_lib() + _check( + _lib.cbsdk_session_set_channel_sample_group( + self._session, 1, int(chan_type), + int(_coerce_enum(SampleRate, value, _RATE_ALIASES)), + False + ), + "Failed to set smpgroup", + ) + elif key in _dispatch: + _dispatch[key](chan_id, value) + else: + raise ValueError(f"Unknown channel attribute: {key!r}") + + # --- Channel Mapping (CMP) Files --- + + def load_channel_map(self, filepath: str, bank_offset: int = 0): + """Load a channel mapping file (.cmp) and apply electrode positions. + + CMP files define physical electrode positions on arrays. Because the device + does not persist position data, positions are stored locally and overlaid + onto channel info whenever updated config data arrives from the device. + + Can be called multiple times for different front-end ports on a Hub device, + each with a different array and CMP file. + + Args: + filepath: Path to the .cmp file. + bank_offset: Offset added to CMP bank indices for multi-port Hubs. + CMP bank letter A becomes absolute bank (1 + bank_offset). + Port 1: offset 0 (A=bank 1). Port 2: offset 4 (A=bank 5), etc. + """ + _check( + _get_lib().cbsdk_session_load_channel_map( + self._session, filepath.encode(), bank_offset + ), + "Failed to load channel map", + ) + + # --- CCF Configuration Files --- + + def save_ccf(self, filename: str): + """Save the current device configuration to a CCF (XML) file. + + Args: + filename: Path to the CCF file to write. + """ + _check( + _get_lib().cbsdk_session_save_ccf(self._session, filename.encode()), + "Failed to save CCF", + ) + + def load_ccf(self, filename: str): + """Load a CCF file and apply its configuration to the device. + + Reads the CCF file and sends the configuration packets to the device. + The device must be connected in STANDALONE mode. + + Args: + filename: Path to the CCF file to read. + """ + _check( + _get_lib().cbsdk_session_load_ccf(self._session, filename.encode()), + "Failed to load CCF", + ) + + # --- Instrument Time --- + + @property + def time(self) -> int: + """Most recent device timestamp from shared memory. + + On Gemini (protocol 4.0+) this is PTP nanoseconds. + On legacy NSP (protocol 3.x) this is 30kHz ticks. + + To convert to host ``time.monotonic()``, use :meth:`device_to_monotonic`. + """ + _lib = _get_lib() + t = ffi.new("uint64_t *") + _check(_lib.cbsdk_session_get_time(self._session, t), "Failed to get time") + return t[0] + + # --- Patient Information --- + + def set_patient_info( + self, + id: str, + firstname: str = "", + lastname: str = "", + dob_month: int = 0, + dob_day: int = 0, + dob_year: int = 0, + ): + """Set patient information for recorded files. + + Must be called before starting recording for info to be included. + + Args: + id: Patient identification string (required). + firstname: Patient first name. + lastname: Patient last name. + dob_month: Birth month (1-12, 0 = unset). + dob_day: Birth day (1-31, 0 = unset). + dob_year: Birth year (e.g. 1990, 0 = unset). + """ + _lib = _get_lib() + _check( + _lib.cbsdk_session_set_patient_info( + self._session, + id.encode(), + firstname.encode() if firstname else ffi.NULL, + lastname.encode() if lastname else ffi.NULL, + dob_month, dob_day, dob_year, + ), + "Failed to set patient info", + ) + + # --- Analog Output --- + + def set_analog_output_monitor( + self, + aout_channel: int, + monitor_channel: int, + track_last: bool = True, + spike_only: bool = False, + ): + """Route a channel's signal to an analog/audio output for monitoring. + + Args: + aout_channel: 1-based channel ID of the analog/audio output. + monitor_channel: 1-based channel ID of the channel to monitor. + track_last: If True, track last channel clicked in Central. + spike_only: If True, monitor spike signal; if False, monitor continuous. + """ + _check( + _get_lib().cbsdk_session_set_analog_output_monitor( + self._session, aout_channel, monitor_channel, + track_last, spike_only, + ), + "Failed to set analog output monitor", + ) + + # --- Recording Control --- + + def start_central_recording(self, filename: str, comment: str = ""): + """Start Central file recording on the device. + + Requires Central to be running. + + Args: + filename: Base filename without extension. + comment: Recording comment. + """ + _lib = _get_lib() + _check( + _lib.cbsdk_session_start_central_recording( + self._session, filename.encode(), + comment.encode() if comment else ffi.NULL, + ), + "Failed to start Central recording", + ) + + def stop_central_recording(self): + """Stop Central file recording on the device.""" + _check( + _get_lib().cbsdk_session_stop_central_recording(self._session), + "Failed to stop Central recording", + ) + + def open_central_file_dialog(self): + """Open Central's File Storage dialog. + + Must be called before start_central_recording(). + Wait ~250ms after this call for the dialog to initialize. + """ + _check( + _get_lib().cbsdk_session_open_central_file_dialog(self._session), + "Failed to open Central file dialog", + ) + + def close_central_file_dialog(self): + """Close Central's File Storage dialog.""" + _check( + _get_lib().cbsdk_session_close_central_file_dialog(self._session), + "Failed to close Central file dialog", + ) + + # --- Spike Sorting --- + + def set_channel_spike_sorting( + self, + n_chans: int, + channel_type: ChannelType, + sort_options: int, + ): + """Set spike sorting options for channels of a specific type. + + Args: + n_chans: Number of channels to configure. + channel_type: Channel type filter (e.g., ``ChannelType.FRONTEND``). + sort_options: Spike sorting option flags (cbAINPSPK_*). + """ + _lib = _get_lib() + _check( + _lib.cbsdk_session_set_channel_spike_sorting( + self._session, n_chans, + int(_coerce_enum(ChannelType, channel_type)), + sort_options + ), + "Failed to set spike sorting", + ) + + # --- Clock Synchronization --- + + @staticmethod + def _calibrate_monotonic_offset() -> int: + """Compute offset between time.monotonic() and C++ steady_clock. + + On Linux, macOS, and Windows with Python 3.12+, both clocks use the + same underlying source (CLOCK_MONOTONIC / mach_absolute_time / + QueryPerformanceCounter) so the offset is exactly 0. + + On older Windows Python (<3.12), time.monotonic() may use + GetTickCount64 while steady_clock uses QueryPerformanceCounter, + so we measure the offset empirically. + + Returns: + steady_clock_ns - monotonic_ns (int). + """ + import sys + import platform + + if platform.system() != "Windows" or sys.version_info >= (3, 12): + return 0 + + # Windows < 3.12: clocks may differ, measure empirically + _lib = _get_lib() + t1 = _time.monotonic() + steady_ns = _lib.cbsdk_get_steady_clock_ns() + t2 = _time.monotonic() + mono_ns = int((t1 + t2) / 2 * 1_000_000_000) + return steady_ns - mono_ns + + @property + def clock_offset_ns(self) -> Optional[int]: + """Clock offset in nanoseconds (device_ns - host_ns), or None if unavailable.""" + _lib = _get_lib() + offset = ffi.new("int64_t *") + result = _lib.cbsdk_session_get_clock_offset(self._session, offset) + if result != 0: + return None + return offset[0] + + @property + def clock_uncertainty_ns(self) -> Optional[int]: + """Clock uncertainty (half-RTT) in nanoseconds, or None if unavailable.""" + _lib = _get_lib() + uncertainty = ffi.new("int64_t *") + result = _lib.cbsdk_session_get_clock_uncertainty(self._session, uncertainty) + if result != 0: + return None + return uncertainty[0] + + def send_clock_probe(self): + """Send a clock synchronization probe to the device.""" + _check( + _get_lib().cbsdk_session_send_clock_probe(self._session), + "Failed to send clock probe", + ) + + def device_to_monotonic(self, device_time_ns: int) -> float: + """Convert a device timestamp to ``time.monotonic()`` seconds. + + Chains two offsets: + 1. device_ns → steady_clock_ns (via clock_offset_ns from device sync) + 2. steady_clock_ns → monotonic_ns (via calibration at session creation) + + Args: + device_time_ns: Device timestamp in nanoseconds (e.g., header.time). + + Returns: + Corresponding ``time.monotonic()`` value in seconds. + + Raises: + RuntimeError: If no clock sync data is available yet. + + Example:: + + @session.on_event("FRONTEND") + def on_spike(header, data): + t = session.device_to_monotonic(header.time) + latency_ms = (time.monotonic() - t) * 1000 + print(f"Spike latency: {latency_ms:.1f} ms") + """ + offset = self.clock_offset_ns + if offset is None: + raise RuntimeError("No clock sync data available") + steady_ns = device_time_ns - offset + mono_ns = steady_ns - self._mono_to_steady_offset_ns + return mono_ns / 1_000_000_000 + + # --- Commands --- + + def send_comment(self, comment: str, rgba: int = 0, charset: int = 0): + """Send a comment to the device (appears in recorded data).""" + _lib = _get_lib() + _check( + _lib.cbsdk_session_send_comment( + self._session, comment.encode(), rgba, charset + ), + "Failed to send comment", + ) + + def set_digital_output(self, chan_id: int, value: int): + """Set a digital output channel value.""" + _check( + _get_lib().cbsdk_session_set_digital_output(self._session, chan_id, value), + "Failed to set digital output", + ) + + def set_runlevel(self, runlevel: int): + """Set the system run level.""" + _check( + _get_lib().cbsdk_session_set_runlevel(self._session, runlevel), + "Failed to set run level", + ) + + # --- numpy Data Collection --- + + def continuous_reader( + self, rate: SampleRate = SampleRate.SR_30kHz, buffer_seconds: float = 10.0 + ) -> ContinuousReader: + """Create a ring buffer that accumulates continuous group data. + + Registers a group callback internally. Call :meth:`ContinuousReader.read` + to retrieve the most recent samples as a numpy array. + + Args: + rate: Sample rate to subscribe to. + buffer_seconds: Ring buffer duration in seconds. + + Returns: + A :class:`ContinuousReader` instance. + """ + rate = _coerce_enum(SampleRate, rate, _RATE_ALIASES) + n_channels = len(self.get_group_channels(int(rate))) + if n_channels == 0: + raise ValueError(f"No channels configured for {rate.name}") + buffer_samples = int(buffer_seconds * rate.hz) + return ContinuousReader(self, rate, n_channels, buffer_samples) + + def read_continuous(self, rate: SampleRate = SampleRate.SR_30kHz, duration: float = 1.0): + """Collect continuous data for a specified duration. + + Blocks for *duration* seconds while accumulating group samples, + then returns the collected data. + + Args: + rate: Sample rate to subscribe to. + duration: Collection duration in seconds. + + Returns: + numpy.ndarray of shape ``(n_channels, n_samples)``, dtype ``int16``. + """ + import time + import numpy as np + + rate = _coerce_enum(SampleRate, rate, _RATE_ALIASES) + n_channels = len(self.get_group_channels(int(rate))) + if n_channels == 0: + raise ValueError(f"No channels configured for {rate.name}") + + max_samples = int(duration * rate.hz * 1.2) # 20% headroom + buf = np.zeros((n_channels, max_samples), dtype=np.int16) + count = [0] + + _lib = _get_lib() + + @ffi.callback("void(const cbPKT_GROUP*, void*)") + def c_group_cb(pkt, user_data): + try: + if count[0] < max_samples: + src = ffi.buffer(pkt.data, n_channels * 2) + arr = np.frombuffer(src, dtype=np.int16, count=n_channels) + buf[:, count[0]] = arr + count[0] += 1 + except Exception: + pass + + handle = _lib.cbsdk_session_register_group_callback( + self._session, int(rate), c_group_cb, ffi.NULL + ) + if handle == 0: + raise RuntimeError("Failed to register group callback") + self._handles.append(handle) + self._callback_refs.append(c_group_cb) + + try: + time.sleep(duration) + finally: + _lib.cbsdk_session_unregister_callback(self._session, handle) + try: + self._handles.remove(handle) + except ValueError: + pass + + return buf[:, :count[0]] + + # --- Bulk Configuration --- + + @property + def sysfreq(self) -> int: + """System sampling frequency in Hz (e.g., 30000).""" + return _get_lib().cbsdk_session_get_sysfreq(self._session) + + @staticmethod + def num_filters() -> int: + """Number of available filters (cbMAXFILTS).""" + return _get_lib().cbsdk_get_num_filters() + + def get_filter_info(self, filter_id: int) -> Optional[dict]: + """Get a filter's description. + + Args: + filter_id: Filter ID (0 to num_filters()-1). + + Returns: + Dict with keys ``label``, ``hpfreq``, ``hporder``, ``lpfreq``, + ``lporder``. Frequencies are in milliHertz. Returns None if invalid. + """ + _lib = _get_lib() + ptr = _lib.cbsdk_session_get_filter_label(self._session, filter_id) + if ptr == ffi.NULL: + return None + return { + "label": ffi.string(ptr).decode(), + "hpfreq": _lib.cbsdk_session_get_filter_hpfreq(self._session, filter_id), + "hporder": _lib.cbsdk_session_get_filter_hporder(self._session, filter_id), + "lpfreq": _lib.cbsdk_session_get_filter_lpfreq(self._session, filter_id), + "lporder": _lib.cbsdk_session_get_filter_lporder(self._session, filter_id), + } + + def get_channel_config(self, chan_id: int) -> Optional[dict]: + """Get full configuration for a single channel. + + Args: + chan_id: 1-based channel ID. + + Returns: + Dict with channel configuration fields, or None if the channel + is invalid or not connected. + """ + chan_type = self.get_channel_type(chan_id) + if chan_type is None: + return None + return { + "label": self.get_channel_label(chan_id) or "", + "type": chan_type, + "chancaps": self.get_channel_chancaps(chan_id), + "smpgroup": self.get_channel_smpgroup(chan_id), + "smpfilter": self.get_channel_smpfilter(chan_id), + "spkfilter": self.get_channel_spkfilter(chan_id), + "spkopts": self.get_channel_spkopts(chan_id), + "spkthrlevel": self.get_channel_spkthrlevel(chan_id), + "ainpopts": self.get_channel_ainpopts(chan_id), + "lncrate": self.get_channel_lncrate(chan_id), + "refelecchan": self.get_channel_refelecchan(chan_id), + "amplrejpos": self.get_channel_amplrejpos(chan_id), + "amplrejneg": self.get_channel_amplrejneg(chan_id), + } + + def get_config(self) -> dict: + """Get bulk device configuration. + + Returns a dictionary with system-level info, per-channel configuration + (only for existing/connected channels), and group memberships. + + Returns: + Dict with keys: + - ``sysfreq`` (int): System sampling frequency in Hz. + - ``channels`` (dict): Mapping of chan_id -> channel config dict. + - ``groups`` (dict): Mapping of group_id -> group info dict. + - ``filters`` (dict): Mapping of filter_id -> filter info dict. + + Example:: + + config = session.get_config() + for chan_id, info in config["channels"].items(): + print(f"Ch {chan_id}: {info['label']} type={info['type']}") + """ + _lib = _get_lib() + max_chans = _lib.cbsdk_get_max_chans() + + channels = {} + for chan_id in range(1, max_chans + 1): + info = self.get_channel_config(chan_id) + if info is not None: + channels[chan_id] = info + + groups = {} + for group_id in range(1, 7): + label = self.get_group_label(group_id) + ch_list = self.get_group_channels(group_id) + groups[group_id] = { + "label": label or "", + "channels": ch_list, + } + + num_filt = _lib.cbsdk_get_num_filters() + filters = {} + for filt_id in range(num_filt): + info = self.get_filter_info(filt_id) + if info is not None and info["label"]: + filters[filt_id] = info + + return { + "sysfreq": self.sysfreq, + "channels": channels, + "groups": groups, + "filters": filters, + } + + # --- Version --- + + @staticmethod + def version() -> str: + """Get the SDK version string.""" + return ffi.string(_get_lib().cbsdk_get_version()).decode() + + +class ContinuousReader: + """Ring buffer that accumulates continuous group data into a numpy array. + + Created via :meth:`Session.continuous_reader`. + + Example:: + + reader = session.continuous_reader(rate=SampleRate.SR_30kHz, buffer_seconds=10) + import time + time.sleep(2) + data = reader.read() # (n_channels, ~60000) int16 array + reader.close() + + Attributes: + n_channels: Number of channels in the group. + sample_rate: Sample rate in Hz. + """ + + def __init__(self, session: Session, rate: SampleRate, n_channels: int, + buffer_samples: int): + import numpy as np + + self._session = session + self._rate = rate + self.n_channels = n_channels + self.sample_rate = rate.hz + self._buffer_samples = buffer_samples + self._buffer = np.zeros((n_channels, buffer_samples), dtype=np.int16) + self._write_pos = 0 + self._total_samples = 0 + self._closed = False + self._handle = None + self._cb_ref = None + self._register() + + def _register(self): + from ._numpy import group_data_as_array + + _lib = _get_lib() + n_ch = self.n_channels + + @ffi.callback("void(const cbPKT_GROUP*, void*)") + def c_group_cb(pkt, user_data): + try: + arr = group_data_as_array(pkt.data, n_ch) + pos = self._write_pos % self._buffer_samples + self._buffer[:, pos] = arr + self._write_pos += 1 + self._total_samples += 1 + except Exception: + pass + + self._cb_ref = c_group_cb + handle = _lib.cbsdk_session_register_group_callback( + self._session._session, int(self._rate), c_group_cb, ffi.NULL + ) + if handle == 0: + raise RuntimeError("Failed to register group callback") + self._handle = handle + self._session._handles.append(handle) + self._session._callback_refs.append(c_group_cb) + + def read(self, n_samples: int | None = None): + """Read the most recent samples from the ring buffer. + + Args: + n_samples: Number of samples to read. If ``None``, reads all + available (up to buffer size). + + Returns: + numpy.ndarray of shape ``(n_channels, n_samples)``, dtype ``int16``. + Always returns a copy. + """ + import numpy as np + + available = min(self._total_samples, self._buffer_samples) + if n_samples is None: + n_samples = available + n_samples = min(n_samples, available) + if n_samples == 0: + return np.zeros((self.n_channels, 0), dtype=np.int16) + + end = self._write_pos % self._buffer_samples + start = (end - n_samples) % self._buffer_samples + if start < end: + return self._buffer[:, start:end].copy() + else: + return np.concatenate([ + self._buffer[:, start:], + self._buffer[:, :end], + ], axis=1) + + @property + def available(self) -> int: + """Number of samples currently in the buffer.""" + return min(self._total_samples, self._buffer_samples) + + @property + def total_samples(self) -> int: + """Total number of samples received (may exceed buffer size).""" + return self._total_samples + + @property + def dropped(self) -> int: + """Number of samples lost due to buffer overflow.""" + return max(0, self._total_samples - self._buffer_samples) + + def close(self): + """Unregister the callback and release resources.""" + if self._closed: + return + self._closed = True + if self._handle is not None: + _get_lib().cbsdk_session_unregister_callback( + self._session._session, self._handle + ) + try: + self._session._handles.remove(self._handle) + except ValueError: + pass + + def __del__(self): + self.close() diff --git a/src/cbdev/CMakeLists.txt b/src/cbdev/CMakeLists.txt new file mode 100644 index 00000000..e436aa3f --- /dev/null +++ b/src/cbdev/CMakeLists.txt @@ -0,0 +1,63 @@ +# cbdev - Device Transport Layer +# Handles UDP socket communication with devices + +project(cbdev + DESCRIPTION "CereLink Device Transport Layer" + LANGUAGES CXX +) + +# Library sources +set(CBDEV_SOURCES + src/device_session.cpp + src/device_session_311.cpp + src/device_session_400.cpp + src/device_session_410.cpp + src/device_factory.cpp + src/protocol_detector.cpp + src/clock_sync.cpp +) + +# Build as STATIC library +add_library(cbdev STATIC ${CBDEV_SOURCES}) + +target_include_directories(cbdev + PUBLIC + $ + $ +) + +# Dependencies +target_link_libraries(cbdev + PUBLIC + cbproto # Protocol definitions + cbutil # Result and shared utilities +) + +# C++17 required +target_compile_features(cbdev PUBLIC cxx_std_17) + +# Platform-specific networking libraries +if(WIN32) + target_link_libraries(cbdev PRIVATE wsock32 ws2_32 iphlpapi) + +elseif(APPLE) + # macOS networking (includes the IP_BOUND_IF fix!) + target_link_libraries(cbdev PRIVATE pthread) +else() + # Linux networking + target_link_libraries(cbdev PRIVATE pthread) +endif() + +# Installation +install(TARGETS cbdev + EXPORT CBSDKTargets +) + +# Install public headers only (minimal API surface) +install(FILES + include/cbdev/result.h + include/cbdev/connection.h + include/cbdev/device_session.h + include/cbdev/device_factory.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cbdev +) diff --git a/src/cbdev/README.md b/src/cbdev/README.md new file mode 100644 index 00000000..bed8e7cf --- /dev/null +++ b/src/cbdev/README.md @@ -0,0 +1,82 @@ +# cbdev - Device Transport Layer + +Low-level C++ library for UDP communication with Cerebus neural recording devices. + +## Scope + +- UDP socket management (send/receive) with platform-specific handling (Windows/macOS/Linux) +- Automatic protocol version detection and packet translation (3.11, 4.0, 4.1, current) +- Device handshake, configuration request, and run-level control +- Receive thread with callback registration +- Clock synchronization (probe-based offset estimation) + +cbdev does **not** handle shared memory (see `cbshm`) or high-level data management (see `cbsdk`). + +## Public API + +### Headers + +| Header | Contents | +|--------------------|---------------------------------------------------------------------------------------------------------| +| `connection.h` | `ConnectionParams`, `DeviceType`, `ProtocolVersion`, `ChannelType`, `DeviceRate` enums, factory helpers | +| `device_session.h` | `IDeviceSession` interface, callback types | +| `device_factory.h` | `createDeviceSession()` factory function | +| `result.h` | `Result` error-handling type | + +### Usage + +```cpp +#include + +// Create session for a specific device type (auto-detects protocol) +auto params = cbdev::ConnectionParams::forDevice(cbdev::DeviceType::HUB1); +auto result = cbdev::createDeviceSession(params); +if (result.isError()) { /* handle error */ } +auto device = std::move(result.value()); + +// Register callback and start receiving +auto handle = device->registerReceiveCallback([](const cbPKT_GENERIC& pkt) { + // Called on receive thread -- keep fast +}); +device->startReceiveThread(); + +// Synchronous handshake (brings device to RUNNING) +device->performHandshakeSync(std::chrono::milliseconds(2000)); + +// Send packets, query config, etc. +const auto& chanInfo = *device->getChanInfo(1); +device->sendPacket(myPacket); + +// Clock sync +device->sendClockProbe(); +auto offset = device->getOffsetNs(); // device_ns - host_ns + +// Cleanup +device->stopReceiveThread(); +device->unregisterCallback(handle); +``` + +## Supported Devices + +| Device | Address | Protocol | +|------------|-----------------|------------------------| +| Legacy NSP | 192.168.137.128 | 3.11 (auto-translated) | +| Gemini NSP | 192.168.137.128 | 4.x | +| Hub1 | 192.168.137.200 | 4.x | +| Hub2 | 192.168.137.201 | 4.x | +| Hub3 | 192.168.137.202 | 4.x | +| nPlay | 127.0.0.1 | 3.11 or 4.x | + +## Architecture + +``` +createDeviceSession(params, version) + │ + ├─ CURRENT ──► DeviceSession (direct UDP) + ├─ 4.10 ────► DeviceSession_410 → DeviceSession + ├─ 4.00 ────► DeviceSession_400 → DeviceSession + ├─ 3.11 ────► DeviceSession_311 → DeviceSession + └─ UNKNOWN ─► ProtocolDetector → (one of the above) +``` + +Protocol wrappers (`DeviceSession_*`) extend `DeviceSessionWrapper`, overriding only `receivePackets()` and `sendPacket()` to translate between the device's wire format and the current protocol used internally. diff --git a/src/cbdev/include/cbdev/connection.h b/src/cbdev/include/cbdev/connection.h new file mode 100644 index 00000000..51c3a8a6 --- /dev/null +++ b/src/cbdev/include/cbdev/connection.h @@ -0,0 +1,165 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file connection.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Device connection parameters for Cerebus devices +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_DEVICE_CONFIG_H +#define CBDEV_DEVICE_CONFIG_H + +#include +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Connection Configuration +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Device type enumeration (C++ wrapper around C enum for type safety) +enum class DeviceType : uint32_t { + LEGACY_NSP = CBPROTO_DEVICE_TYPE_LEGACY_NSP, ///< Neural Signal Processor (legacy) + NSP = CBPROTO_DEVICE_TYPE_NSP, ///< Gemini NSP + HUB1 = CBPROTO_DEVICE_TYPE_HUB1, ///< Hub 1 (legacy addressing) + HUB2 = CBPROTO_DEVICE_TYPE_HUB2, ///< Hub 2 (legacy addressing) + HUB3 = CBPROTO_DEVICE_TYPE_HUB3, ///< Hub 3 (legacy addressing) + NPLAY = CBPROTO_DEVICE_TYPE_NPLAY, ///< nPlayServer (legacy, ports 51001/51002) + CUSTOM = CBPROTO_DEVICE_TYPE_CUSTOM, ///< Custom IP/port configuration +}; + +/// Protocol version enumeration (C++ wrapper around C enum for type safety) +enum class ProtocolVersion : uint32_t { + UNKNOWN = CBPROTO_PROTOCOL_UNKNOWN, ///< Unknown or undetected protocol + PROTOCOL_311 = CBPROTO_PROTOCOL_311, ///< Legacy cbproto_311 (32-bit timestamps, deprecated) + PROTOCOL_400 = CBPROTO_PROTOCOL_400, ///< Legacy cbproto_400 (64-bit timestamps, deprecated) + PROTOCOL_410 = CBPROTO_PROTOCOL_410, ///< Protocol 4.1 (64-bit timestamps, 16-bit packet types) + PROTOCOL_CURRENT = CBPROTO_PROTOCOL_CURRENT ///< Current protocol (64-bit timestamps) +}; + +/// Channel type enumeration (C++ wrapper around C enum for type safety) +enum class ChannelType : uint32_t { + FRONTEND = CBPROTO_CHANNEL_TYPE_FRONTEND, ///< Front-end analog input (isolated) + ANALOG_IN = CBPROTO_CHANNEL_TYPE_ANALOG_IN, ///< Analog input (non-isolated) + ANALOG_OUT = CBPROTO_CHANNEL_TYPE_ANALOG_OUT, ///< Analog output (non-audio) + AUDIO = CBPROTO_CHANNEL_TYPE_AUDIO, ///< Audio output + DIGITAL_IN = CBPROTO_CHANNEL_TYPE_DIGITAL_IN, ///< Digital input + SERIAL = CBPROTO_CHANNEL_TYPE_SERIAL, ///< Serial input + DIGITAL_OUT = CBPROTO_CHANNEL_TYPE_DIGITAL_OUT ///< Digital output +}; + +/// Sampling rate enumeration +enum class DeviceRate : uint32_t { + NONE = CBPROTO_GROUP_RATE_NONE, + SR_500 = CBPROTO_GROUP_RATE_500Hz, + SR_1000 = CBPROTO_GROUP_RATE_1000Hz, + SR_2000 = CBPROTO_GROUP_RATE_2000Hz, + SR_10000 = CBPROTO_GROUP_RATE_10000Hz, + SR_30000 = CBPROTO_GROUP_RATE_30000Hz, + SR_RAW = CBPROTO_GROUP_RATE_RAW +}; + +/// Convert protocol version to string for logging +/// @param version Protocol version +/// @return Human-readable string +const char* protocolVersionToString(ProtocolVersion version); + +/// Convert device type to string for logging +/// @param type Device type +/// @return Human-readable string +const char* deviceTypeToString(DeviceType type); + +/// Convert channel type to string for logging +/// @param type Channel type +/// @return Human-readable string +const char* channelTypeToString(ChannelType type); + +/// Convert device rate to string for logging +/// @param rate Device rate +/// @return Human-readable string +const char* deviceRateToString(DeviceRate rate); + + +/// Connection parameters for device communication +/// Note: This contains network/socket configuration only. +/// Device operating configuration (sample rates, channels, etc.) is in shared memory. +struct ConnectionParams { + DeviceType type = DeviceType::LEGACY_NSP; + + // Network addresses + std::string device_address; ///< Device IP address (where to send packets) + std::string client_address; ///< Client IP address (where to bind receive socket) + + // Ports + uint16_t recv_port = cbNET_UDP_PORT_CNT; ///< Port to receive packets on (client side) + uint16_t send_port = cbNET_UDP_PORT_BCAST; ///< Port to send packets to (device side) + + // Socket options + bool broadcast = false; ///< Enable broadcast mode + bool non_blocking = false; ///< Non-blocking socket (false = blocking, better for dedicated receive thread) + int recv_buffer_size = 8388608; ///< Receive buffer size (6MB default) + int send_buffer_size = 0; ///< Send buffer size (0 = OS default, typically 64KB-256KB) + + // Connection options + bool autorun = true; ///< Auto-start device on connect (true = performStartupHandshake, false = requestConfiguration only) + + /// Create connection parameters for a known device type + static ConnectionParams forDevice(DeviceType type); + + /// Create custom connection parameters with explicit addresses + static ConnectionParams custom(const std::string& device_addr, + const std::string& client_addr = "0.0.0.0", + uint16_t recv_port = cbNET_UDP_PORT_CNT, + uint16_t send_port = cbNET_UDP_PORT_BCAST); +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Device Defaults +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Default device addresses and ports (from upstream/cbproto/cbproto.h) +namespace ConnectionDefaults { + // Device addresses + constexpr const char* LEGACY_NSP_ADDRESS = cbNET_UDP_ADDR_CNT; // Legacy NSP + constexpr const char* NSP_ADDRESS = cbNET_UDP_ADDR_GEMINI_NSP; // Gemini NSP + constexpr const char* HUB1_ADDRESS = cbNET_UDP_ADDR_GEMINI_HUB; // Gemini Hub1 + constexpr const char* HUB2_ADDRESS = cbNET_UDP_ADDR_GEMINI_HUB2; // Gemini Hub2 + constexpr const char* HUB3_ADDRESS = cbNET_UDP_ADDR_GEMINI_HUB3; // Gemini Hub3 + constexpr const char* NPLAY_ADDRESS = "127.0.0.1"; // nPlayServer (loopback) + + // Client/Host addresses (empty = auto-detect based on device type and platform) + constexpr const char* DEFAULT_CLIENT_ADDRESS = ""; // Auto-detect (was 192.168.137.199) + + // Ports + constexpr uint16_t LEGACY_NSP_RECV_PORT = cbNET_UDP_PORT_BCAST; + constexpr uint16_t LEGACY_NSP_SEND_PORT = cbNET_UDP_PORT_CNT; + constexpr uint16_t NSP_PORT = cbNET_UDP_PORT_GEMINI_NSP; + constexpr uint16_t HUB1_PORT = cbNET_UDP_PORT_GEMINI_HUB; // cbNET_UDP_PORT_GEMINI_HUB (both send & recv) + constexpr uint16_t HUB2_PORT = cbNET_UDP_PORT_GEMINI_HUB2; // cbNET_UDP_PORT_GEMINI_HUB2 (both send & recv) + constexpr uint16_t HUB3_PORT = cbNET_UDP_PORT_GEMINI_HUB3; // cbNET_UDP_PORT_GEMINI_HUB3 (both send & recv) +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Utility Functions +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Detect local IP address for binding receive socket +/// Platform-specific logic: +/// - NPLAY: "0.0.0.0" on all platforms (loopback, receive broadcast) +/// - macOS: "0.0.0.0" (bind all interfaces, route via IP_BOUND_IF) +/// - Linux: broadcast address "192.168.137.255" (required to receive subnet broadcasts) +/// - Windows: unicast address on 192.168.137.x adapter, or "0.0.0.0" +/// @param type Device type (determines loopback vs subnet binding) +/// @return IP address string, or "0.0.0.0" if detection fails +std::string detectClientAddress(DeviceType type); + +/// @deprecated Use detectClientAddress(DeviceType) instead +inline std::string detectLocalIP() { return detectClientAddress(DeviceType::LEGACY_NSP); } + +} // namespace cbdev + +#endif // CBDEV_DEVICE_CONFIG_H diff --git a/src/cbdev/include/cbdev/device_factory.h b/src/cbdev/include/cbdev/device_factory.h new file mode 100644 index 00000000..c6eb3233 --- /dev/null +++ b/src/cbdev/include/cbdev/device_factory.h @@ -0,0 +1,57 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_factory.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Factory for creating protocol-specific device session implementations +/// +/// Creates the appropriate DeviceSession implementation based on detected or specified protocol +/// version. Handles protocol auto-detection if version is UNKNOWN. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_DEVICE_FACTORY_H +#define CBDEV_DEVICE_FACTORY_H + +#include +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Create device session with appropriate protocol implementation +/// +/// Factory function that creates the correct DeviceSession implementation based on protocol +/// version. If version is UNKNOWN, automatically detects the device protocol. +/// +/// @param config Device configuration (IP addresses, ports, device type) +/// @param version Protocol version (UNKNOWN triggers auto-detection) +/// @return Unique pointer to IDeviceSession implementation, or error +/// +/// @note Auto-detection blocks briefly (up to 500ms) to probe device +/// @note For PROTOCOL_CURRENT, creates standard DeviceSession +/// @note For PROTOCOL_311, creates DeviceSession_311 with translation +/// +/// @example +/// ```cpp +/// // Auto-detect protocol +/// auto result = createDeviceSession(config, ProtocolVersion::UNKNOWN); +/// if (result.isOk()) { +/// auto device = std::move(result.value()); +/// // Use device->receivePackets(), etc. +/// } +/// +/// // Force specific protocol +/// auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); +/// ``` +/// +Result> createDeviceSession( + const ConnectionParams& config, + ProtocolVersion version = ProtocolVersion::UNKNOWN +); + +} // namespace cbdev + +#endif // CBDEV_DEVICE_FACTORY_H diff --git a/src/cbdev/include/cbdev/device_session.h b/src/cbdev/include/cbdev/device_session.h new file mode 100644 index 00000000..94b29474 --- /dev/null +++ b/src/cbdev/include/cbdev/device_session.h @@ -0,0 +1,330 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Device session interface for protocol abstraction +/// +/// Defines the interface for device communication, allowing different protocol implementations +/// (e.g., current protocol vs legacy cbproto_311) without affecting SDK code. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_DEVICE_SESSION_INTERFACE_H +#define CBDEV_DEVICE_SESSION_INTERFACE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Callback Types for Receive Thread +/// @{ + +/// Callback invoked for each received packet +/// @param pkt The received packet (reference valid only during callback invocation) +/// @note Callbacks run on the receive thread - keep them fast to avoid packet loss! +using ReceiveCallback = std::function; + +/// Callback invoked after all packets in a UDP datagram have been processed +/// @note Use this for batch operations like signaling shared memory +using DatagramCompleteCallback = std::function; + +/// Handle for callback registration (used for unregistration) +using CallbackHandle = uint32_t; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Abstract interface for device communication +/// +/// Implementations handle protocol-specific details (e.g., packet format translation for legacy +/// devices) while presenting a uniform interface to SDK. +/// +class IDeviceSession { +public: + virtual ~IDeviceSession() = default; + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Packet Reception + /// @{ + + /// Receive UDP datagram from device into provided buffer + /// @param buffer Destination buffer for received data + /// @param buffer_size Maximum bytes to receive + /// @return Number of bytes received, or error + /// @note Non-blocking. Returns 0 if no data available. + /// @note For protocol-translating implementations, translation happens before returning + virtual Result receivePackets(void* buffer, size_t buffer_size) = 0; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Packet Transmission + /// @{ + + /// Send single packet to device + /// @param pkt Packet to send (in current protocol format) + /// @return Success or error + /// @note For protocol-translating implementations, translation happens before sending + virtual Result sendPacket(const cbPKT_GENERIC& pkt) = 0; + + /// Send multiple packets to device, coalesced into minimal UDP datagrams + /// @param pkts Vector of packets to send + /// @return Success or error + /// @note Packets are batched into datagrams up to MTU size to reduce packet loss + virtual Result sendPackets(const std::vector& pkts) = 0; + + /// Send raw bytes to device (for protocol translation) + /// @param buffer Buffer containing raw bytes to send + /// @param size Number of bytes to send + /// @return Success or error + /// @note This is primarily for use by protocol wrapper implementations + virtual Result sendRaw(const void* buffer, size_t size) = 0; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Device Control + /// @{ + + /// Set device system runlevel (pure virtual - must be implemented) + /// Sends cbPKTYPE_SETRUNLEVEL to change device operating state. + /// Does NOT wait for response - caller must handle SYSREP monitoring. + /// @param runlevel Desired runlevel (cbRUNLEVEL_*) + /// @param resetque Channel for reset to queue on + /// @param runflags Lock recording after reset + /// @return Success or error + virtual Result setSystemRunLevel(uint32_t runlevel, uint32_t resetque, uint32_t runflags) = 0; + + /// Convenience overload with default resetque + Result setSystemRunLevel(const uint32_t runlevel, const uint32_t resetque) { + return setSystemRunLevel(runlevel, resetque, 0); + } + + /// Convenience overload with default resetque and runflags + Result setSystemRunLevel(const uint32_t runlevel) { + return setSystemRunLevel(runlevel, 0, 0); + } + + /// Set device system runlevel (synchronous) + /// Sends cbPKTYPE_SETRUNLEVEL and waits for cbPKTTYPE_SYSREPRUNLEV response. + /// @param runlevel Desired runlevel (cbRUNLEVEL_*) + /// @param resetque Channel for reset to queue on + /// @param runflags Lock recording after reset + /// @param timeout Maximum time to wait for response + /// @return Success if response received, error on send failure or timeout + virtual Result setSystemRunLevelSync(uint32_t runlevel, uint32_t resetque, uint32_t runflags, + std::chrono::milliseconds timeout) = 0; + + /// Request all configuration from the device (asynchronous) + /// Sends cbPKTTYPE_REQCONFIGALL which triggers the device to send all config packets. + /// Does NOT wait for response - caller must handle config flood and final SYSREP. + /// @return Success or error + virtual Result requestConfiguration() = 0; + + /// Request all configuration from the device (synchronous) + /// Sends cbPKTTYPE_REQCONFIGALL and waits for cbPKTTYPE_SYSREP response. + /// @param timeout Maximum time to wait for configuration + /// @return Success if config received, error on send failure or timeout + virtual Result requestConfigurationSync(std::chrono::milliseconds timeout) = 0; + + /// Perform complete device handshake sequence (synchronous) + /// Attempts to bring device to RUNNING state through the following sequence: + /// 1. Try to set RUNNING directly + /// 2. If not RUNNING, perform HARDRESET (→ STANDBY) + /// 3. Request configuration + /// 4. If still not RUNNING, perform RESET (→ RUNNING) + /// @param timeout Maximum total time for entire handshake sequence + /// @return Success if device reaches RUNNING state, error otherwise + virtual Result performHandshakeSync(std::chrono::milliseconds timeout) = 0; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name State + /// @{ + + /// Check if device connection is active + /// @return true if socket is open and connected + [[nodiscard]] virtual bool isConnected() const = 0; + + /// Get device configuration + /// @return Current device configuration + [[nodiscard]] virtual const ConnectionParams& getConnectionParams() const = 0; + + /// Get protocol version of this session + /// @return Protocol version being used by this session + /// @note For auto-detected sessions, returns the detected version + [[nodiscard]] virtual ProtocolVersion getProtocolVersion() const = 0; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Configuration Access + /// @{ + + /// Get full device configuration + /// @return Reference to device configuration buffer + /// @note Configuration is updated automatically when config packets are received + [[nodiscard]] virtual const cbproto::DeviceConfig& getDeviceConfig() const = 0; + + /// Get system information + /// @return Reference to system info packet + [[nodiscard]] virtual const cbPKT_SYSINFO& getSysInfo() const = 0; + + /// Get channel information for specific channel + /// @param chan_id Channel ID (1-based, 1 to cbMAXCHANS) + /// @return Pointer to channel info, or nullptr if invalid channel ID + [[nodiscard]] virtual const cbPKT_CHANINFO* getChanInfo(uint32_t chan_id) const = 0; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Channel Configuration + /// @{ + + /// Count channels matching a specific type + /// @param chanType Channel type filter (e.g., ChannelType::FRONTEND) + /// @param maxCount Maximum number to count (use cbMAXCHANS for all) + /// @return Number of channels matching the type criteria + [[nodiscard]] virtual size_t countChannelsOfType(ChannelType chanType, size_t maxCount = cbMAXCHANS) const = 0; + + /// Set sampling group for first N channels of a specific type + /// Groups 1-4 disable groups 1-5 but not 6. Group 5 disables all others. Group 6 disables 5 but no others. + /// Group 0 disables all groups including raw. + /// @param nChans Number of channels to configure (use cbMAXCHANS for all channels of type) + /// @param chanType Channel type filter (e.g., ChannelType::FRONTEND) + /// @param group_id Group ID (0-6) + /// @param disableOthers Whether channels not in the first nChans of type chanType should have their group set to 0 + /// @return Success or error + virtual Result setChannelsGroupByType(size_t nChans, ChannelType chanType, DeviceRate group_id, bool disableOthers) = 0; + + virtual Result setChannelsGroupSync(size_t nChans, ChannelType chanType, DeviceRate group_id, std::chrono::milliseconds timeout) = 0; + + /// Set AC input coupling (offset correction) for first N channels of a specific type (asynchronous) + /// @param nChans Number of channels to configure (use cbMAXCHANS for all channels of type) + /// @param chanType Channel type filter + /// @param enabled true to enable AC coupling, false to disable + /// @return Success or error + virtual Result setChannelsACInputCouplingByType(size_t nChans, ChannelType chanType, bool enabled) = 0; + + /// Set AC input coupling synchronously (blocks until CHANREP received) + /// @param nChans Number of channels to configure + /// @param chanType Channel type filter + /// @param enabled true to enable AC coupling, false to disable + /// @param timeout Maximum time to wait for response + /// @return Success if response received, error on send failure or timeout + virtual Result setChannelsACInputCouplingSync(size_t nChans, ChannelType chanType, bool enabled, std::chrono::milliseconds timeout) = 0; + + /// Set spike sorting options for first N channels + /// @param nChans Number of channels to configure + /// @param chanType Channel type filter + /// @param sortOptions Spike sorting options (cbAINPSPK_* flags) + /// @return Success or error + virtual Result setChannelsSpikeSortingByType(size_t nChans, ChannelType chanType, uint32_t sortOptions) = 0; + + virtual Result setChannelsSpikeSortingSync(size_t nChans, ChannelType chanType, uint32_t sortOptions, std::chrono::milliseconds timeout) = 0; + + /// Set full channel configuration + /// Sends a cbPKT_CHANINFO (as CHANSET) to the device + /// @param chaninfo Complete channel info packet + /// @return Success or error + virtual Result setChannelConfig(const cbPKT_CHANINFO& chaninfo) = 0; + + /// Set digital output value + /// @param chan_id 1-based channel ID of a digital output channel + /// @param value Digital output value + /// @return Success or error + virtual Result setDigitalOutput(uint32_t chan_id, uint16_t value) = 0; + + /// Send a comment into the data stream + /// @param comment Comment text (max cbMAX_COMMENT-1 chars) + /// @param rgba Color as RGBA uint32_t (0 = white) + /// @param charset Character set (0 = ANSI) + /// @return Success or error + virtual Result sendComment(const std::string& comment, uint32_t rgba = 0, uint8_t charset = 0) = 0; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Receive Thread and Callbacks + /// @{ + + /// Register a callback to be invoked for each received packet + /// @param callback Function to call for each packet + /// @return Handle for unregistration, or 0 on failure + /// @note Callbacks run on the receive thread - keep them fast to avoid packet loss! + /// @note Use the cbsdk API to leverage shared memory and queueing for slower callbacks. + /// @note Multiple callbacks can be registered and will be called in registration order + virtual CallbackHandle registerReceiveCallback(ReceiveCallback callback) = 0; + + /// Register a callback to be invoked after all packets in a datagram are processed + /// @param callback Function to call after datagram processing + /// @return Handle for unregistration, or 0 on failure + /// @note Use this for batch operations like signaling shared memory + virtual CallbackHandle registerDatagramCompleteCallback(DatagramCompleteCallback callback) = 0; + + /// Unregister a previously registered callback + /// @param handle Handle returned by registerReceiveCallback or registerDatagramCompleteCallback + virtual void unregisterCallback(CallbackHandle handle) = 0; + + /// Start the receive thread + /// @return Success or error if thread cannot be started + /// @note Thread calls receivePackets() in a loop and invokes registered callbacks + virtual Result startReceiveThread() = 0; + + /// Stop the receive thread + /// @note Blocks until thread terminates + virtual void stopReceiveThread() = 0; + + /// Check if receive thread is running + /// @return true if thread is active + [[nodiscard]] virtual bool isReceiveThreadRunning() const = 0; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Clock Synchronization + /// @{ + + /// Convert a device timestamp (nanoseconds) to the host's steady_clock time_point. + /// @param device_time_ns Device timestamp in nanoseconds + /// @return Corresponding host time, or nullopt if no sync data available + [[nodiscard]] virtual std::optional + toLocalTime(uint64_t device_time_ns) const = 0; + + /// Convert a host steady_clock time_point to device timestamp (nanoseconds). + /// @param local_time Host time + /// @return Corresponding device timestamp in nanoseconds, or nullopt if no sync data available + [[nodiscard]] virtual std::optional + toDeviceTime(std::chrono::steady_clock::time_point local_time) const = 0; + + /// Send a clock synchronization probe (no-op SYSSETRUNLEV(RUNNING)). + /// Captures T1 (host send time) and completes measurement when SYSREPRUNLEV is received. + /// @return Success or error + virtual Result sendClockProbe() = 0; + + /// Current offset estimate: device_ns - steady_clock_ns. + /// @return Offset in nanoseconds, or nullopt if no sync data available + [[nodiscard]] virtual std::optional getOffsetNs() const = 0; + + /// Uncertainty (half-RTT) from best probe, or INT64_MAX for one-way only. + /// @return Uncertainty in nanoseconds, or nullopt if no sync data available + [[nodiscard]] virtual std::optional getUncertaintyNs() const = 0; + + /// @} +}; + +} // namespace cbdev + +#endif // CBDEV_DEVICE_SESSION_INTERFACE_H diff --git a/src/cbdev/include/cbdev/result.h b/src/cbdev/include/cbdev/result.h new file mode 100644 index 00000000..fa491901 --- /dev/null +++ b/src/cbdev/include/cbdev/result.h @@ -0,0 +1,20 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file result.h +/// +/// @brief Result type for cbdev (alias for cbutil::Result) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_RESULT_H +#define CBDEV_RESULT_H + +#include + +namespace cbdev { + +template +using Result = cbutil::Result; + +} // namespace cbdev + +#endif // CBDEV_RESULT_H diff --git a/src/cbdev/src/clock_sync.cpp b/src/cbdev/src/clock_sync.cpp new file mode 100644 index 00000000..b5d9336c --- /dev/null +++ b/src/cbdev/src/clock_sync.cpp @@ -0,0 +1,116 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file clock_sync.cpp +/// @author CereLink Development Team +/// @date 2025-02-18 +/// +/// @brief Implementation of ClockSync offset estimator (probes-only with asymmetry correction) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "clock_sync.h" +#include +#include + +namespace cbdev { + +namespace { + +inline int64_t to_ns(ClockSync::time_point tp) { + return std::chrono::duration_cast( + tp.time_since_epoch()).count(); +} + +inline ClockSync::time_point from_ns(int64_t ns) { + return ClockSync::time_point(std::chrono::nanoseconds(ns)); +} + +} // anonymous namespace + +ClockSync::ClockSync() + : m_config{} {} + +ClockSync::ClockSync(Config config) + : m_config(std::move(config)) {} + +void ClockSync::addProbeSample(time_point t1_local, uint64_t t3_device_ns, time_point t4_local) { + std::lock_guard lock(m_mutex); + + const int64_t t1_ns = to_ns(t1_local); + const int64_t t4_ns = to_ns(t4_local); + const int64_t rtt_ns = t4_ns - t1_ns; + + // offset = T3 - T1 - α * RTT + const int64_t offset_ns = static_cast(t3_device_ns) - t1_ns + - static_cast(std::round(m_config.forward_delay_fraction * rtt_ns)); + + ProbeSample sample; + sample.offset_ns = offset_ns; + sample.rtt_ns = rtt_ns; + sample.when = t4_local; + + m_probe_samples.push_back(sample); + + // Cap the number of stored probes + while (m_probe_samples.size() > m_config.max_probe_samples) { + m_probe_samples.pop_front(); + } + + pruneExpired(t4_local); + recomputeEstimate(); +} + +std::optional ClockSync::toLocalTime(uint64_t device_time_ns) const { + std::lock_guard lock(m_mutex); + if (!m_current_offset_ns) + return std::nullopt; + return from_ns(static_cast(device_time_ns) - *m_current_offset_ns); +} + +std::optional ClockSync::toDeviceTime(time_point local_time) const { + std::lock_guard lock(m_mutex); + if (!m_current_offset_ns) + return std::nullopt; + return static_cast(to_ns(local_time) + *m_current_offset_ns); +} + +bool ClockSync::hasSyncData() const { + std::lock_guard lock(m_mutex); + return m_current_offset_ns.has_value(); +} + +std::optional ClockSync::getOffsetNs() const { + std::lock_guard lock(m_mutex); + return m_current_offset_ns; +} + +std::optional ClockSync::getUncertaintyNs() const { + std::lock_guard lock(m_mutex); + return m_current_uncertainty_ns; +} + +void ClockSync::recomputeEstimate() { + // Find probe with minimum RTT — least queuing/jitter gives most reliable estimate + const ProbeSample* best_probe = nullptr; + for (const auto& p : m_probe_samples) { + if (!best_probe || p.rtt_ns < best_probe->rtt_ns) { + best_probe = &p; + } + } + + if (best_probe) { + m_current_offset_ns = best_probe->offset_ns; + m_current_uncertainty_ns = best_probe->rtt_ns / 2; + } else { + m_current_offset_ns = std::nullopt; + m_current_uncertainty_ns = std::nullopt; + } +} + +void ClockSync::pruneExpired(time_point now) { + const auto probe_cutoff = now - m_config.max_probe_age; + while (!m_probe_samples.empty() && m_probe_samples.front().when < probe_cutoff) { + m_probe_samples.pop_front(); + } +} + +} // namespace cbdev diff --git a/src/cbdev/src/clock_sync.h b/src/cbdev/src/clock_sync.h new file mode 100644 index 00000000..4752b0a1 --- /dev/null +++ b/src/cbdev/src/clock_sync.h @@ -0,0 +1,86 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file clock_sync.h +/// @author CereLink Development Team +/// @date 2025-02-18 +/// +/// @brief Device clock to host steady_clock offset estimator +/// +/// Estimates the offset between the device's nanosecond clock and the host's +/// std::chrono::steady_clock using request-response probes. +/// +/// Each probe gives T1 (host send), T3 (device timestamp), T4 (host recv). +/// Offset = T3 - T1 - α * (T4 - T1), where α is the forward delay fraction +/// (default 0.5 = symmetric NTP midpoint). The probe with minimum RTT is +/// selected as the best estimate, since minimum RTT implies the least +/// queuing/jitter. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_CLOCK_SYNC_H +#define CBDEV_CLOCK_SYNC_H + +#include +#include +#include +#include +#include + +namespace cbdev { + +class ClockSync { +public: + using clock = std::chrono::steady_clock; + using time_point = clock::time_point; + + struct Config { + double forward_delay_fraction = 0.5; // α: assumed D1/(D1+D2) + size_t max_probe_samples = 8; + std::chrono::seconds max_probe_age = std::chrono::seconds{15}; + }; + + ClockSync(); + explicit ClockSync(Config config); + + /// Add a request-response probe sample. + /// @param t1_local Host time just before sending the probe request + /// @param t3_device_ns Device timestamp from the response (nanoseconds) + /// @param t4_local Host time just after receiving the response + void addProbeSample(time_point t1_local, uint64_t t3_device_ns, time_point t4_local); + + /// Convert device timestamp to host steady_clock time_point. + [[nodiscard]] std::optional toLocalTime(uint64_t device_time_ns) const; + + /// Convert host steady_clock time_point to device timestamp (nanoseconds). + [[nodiscard]] std::optional toDeviceTime(time_point local_time) const; + + /// Returns true if at least one probe sample has been ingested. + [[nodiscard]] bool hasSyncData() const; + + /// Current offset estimate: device_ns - local_ns. + [[nodiscard]] std::optional getOffsetNs() const; + + /// Uncertainty (RTT/2) from the best probe. + [[nodiscard]] std::optional getUncertaintyNs() const; + +private: + mutable std::mutex m_mutex; + Config m_config; + + struct ProbeSample { + int64_t offset_ns; // T3 - T1 - α * RTT + int64_t rtt_ns; // T4 - T1 + time_point when; + }; + + std::deque m_probe_samples; + + std::optional m_current_offset_ns; + std::optional m_current_uncertainty_ns; + + void recomputeEstimate(); // called with lock held + void pruneExpired(time_point now); // called with lock held +}; + +} // namespace cbdev + +#endif // CBDEV_CLOCK_SYNC_H diff --git a/src/cbdev/src/device_factory.cpp b/src/cbdev/src/device_factory.cpp new file mode 100644 index 00000000..5d6c89ae --- /dev/null +++ b/src/cbdev/src/device_factory.cpp @@ -0,0 +1,101 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_factory.cpp +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Factory implementation for creating protocol-specific device sessions +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include +#include "device_session_impl.h" +#include "device_session_311.h" +#include "device_session_400.h" +#include "device_session_410.h" +#include "protocol_detector.h" + +namespace cbdev { + +Result> createDeviceSession( + const ConnectionParams& config, + ProtocolVersion version) { + + // Auto-detect protocol if unknown + if (version == ProtocolVersion::UNKNOWN) { + auto detect_result = detectProtocol( + config.device_address.c_str(), config.send_port, + config.client_address.c_str(), config.recv_port, + 2000 // 2 second timeout + ); + + if (detect_result.isError()) { + return Result>::error( + "Failed to detect protocol: " + detect_result.error() + ); + } + + version = detect_result.value(); + } + + // Create appropriate implementation based on protocol version + switch (version) { + case ProtocolVersion::PROTOCOL_CURRENT: { + // Create modern protocol implementation + auto result = DeviceSession::create(config); + if (result.isError()) { + return Result>::error(result.error()); + } + + // Move into unique_ptr + auto device = std::make_unique(std::move(result.value())); + return Result>::ok(std::move(device)); + } + + case ProtocolVersion::PROTOCOL_410: { + // Create modern protocol implementation + auto result = DeviceSession_410::create(config); + if (result.isError()) { + return Result>::error(result.error()); + } + + // Move into unique_ptr + auto device = std::make_unique(std::move(result.value())); + return Result>::ok(std::move(device)); + } + + case ProtocolVersion::PROTOCOL_400: { + // Create protocol 4.0 wrapper + auto result = DeviceSession_400::create(config); + if (result.isError()) { + return Result>::error(result.error()); + } + + // Move into unique_ptr + auto device = std::make_unique(std::move(result.value())); + return Result>::ok(std::move(device)); + } + + case ProtocolVersion::PROTOCOL_311: { + // Create protocol 3.11 wrapper + auto result = DeviceSession_311::create(config); + if (result.isError()) { + return Result>::error(result.error()); + } + + // Move into unique_ptr + auto device = std::make_unique(std::move(result.value())); + return Result>::ok(std::move(device)); + } + + case ProtocolVersion::UNKNOWN: + default: + return Result>::error( + "Cannot create device session: protocol version unknown or invalid" + ); + } +} + +} // namespace cbdev diff --git a/src/cbdev/src/device_session.cpp b/src/cbdev/src/device_session.cpp new file mode 100644 index 00000000..e25ff1a2 --- /dev/null +++ b/src/cbdev/src/device_session.cpp @@ -0,0 +1,1826 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session.cpp +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Minimal UDP socket wrapper for device communication +/// +/// Provides only socket operations - no threads, no callbacks, no statistics, no parsing. +/// All orchestration logic has been moved to SdkSession (cbsdk_v2). +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#ifdef _WIN32 + #pragma comment(lib, "iphlpapi.lib") + typedef int socklen_t; + #define INVALID_SOCKET_VALUE INVALID_SOCKET + #define SOCKET_ERROR_VALUE SOCKET_ERROR +#else + #include + #include + #include + #include + #include + #include + #include + #ifdef __APPLE__ + #include + #endif + typedef int SOCKET; + typedef struct sockaddr SOCKADDR; + typedef struct sockaddr_in SOCKADDR_IN; + #define INVALID_SOCKET_VALUE -1 + #define SOCKET_ERROR_VALUE -1 +#endif + +#include "device_session_impl.h" +#include "clock_sync.h" +#include +#include +#include +#include +#include +#include +#include +#include // for std::remove +#include // for std::tolower +#include // for std::function +#include // for std::gcd +#include +#include +#include + +namespace { + // Platform-specific socket close function + inline void closeSocket(SOCKET sock) { +#ifdef _WIN32 + closesocket(sock); +#else + ::close(sock); +#endif + } + + // High-resolution microsecond delay. + // On Windows, use QueryPerformanceCounter spin-wait to achieve sub-millisecond precision. + // On other platforms, steady_clock busy-wait is used as a fallback. + inline void hr_sleep_us(uint64_t microseconds) { + if (microseconds == 0) return; +#ifdef _WIN32 + LARGE_INTEGER freq; + LARGE_INTEGER start, target; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&start); + // Calculate target ticks: microseconds * (freq / 1e6) + const double ticks_per_us = static_cast(freq.QuadPart) / 1'000'000.0; + const LONGLONG delta_ticks = static_cast(microseconds * ticks_per_us); + target.QuadPart = start.QuadPart + delta_ticks; + for (;;) { + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + if (now.QuadPart >= target.QuadPart) break; + // Optional short pause to reduce power; comment out for maximum precision + // _mm_pause(); + } +#else + auto start = std::chrono::steady_clock::now(); + auto target = start + std::chrono::microseconds(static_cast(microseconds)); + while (std::chrono::steady_clock::now() < target) { + // busy-wait + } +#endif + } +} + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Platform-Specific Helpers +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifdef __APPLE__ +/// Helper function to find the interface index for a given IP address (macOS only) +/// This is used with IP_BOUND_IF to ensure packets are routed via the correct interface +static unsigned int GetInterfaceIndexForIP(const char* ip_address) { + if (ip_address == nullptr || std::strlen(ip_address) == 0) + return 0; + + struct ifaddrs *ifaddr; + unsigned int if_index = 0; + + if (getifaddrs(&ifaddr) == -1) + return 0; + + // Convert the IP address to compare + struct in_addr target_addr; + if (inet_pton(AF_INET, ip_address, &target_addr) != 1) { + freeifaddrs(ifaddr); + return 0; + } + + // Walk through linked list to find matching interface + for (const struct ifaddrs *ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr) + continue; + + if (ifa->ifa_addr->sa_family == AF_INET) { + auto *addr_in = reinterpret_cast(ifa->ifa_addr); + if (addr_in->sin_addr.s_addr == target_addr.s_addr) { + if_index = if_nametoindex(ifa->ifa_name); + break; + } + } + } + + freeifaddrs(ifaddr); + return if_index; +} +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// ConnectionParams Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +ConnectionParams ConnectionParams::forDevice(DeviceType type) { + ConnectionParams conn_params; + conn_params.type = type; + + switch (type) { + case DeviceType::LEGACY_NSP: + // Legacy NSP: different ports for send/recv + conn_params.device_address = ConnectionDefaults::LEGACY_NSP_ADDRESS; + conn_params.client_address = ""; // Auto-detect + conn_params.recv_port = ConnectionDefaults::LEGACY_NSP_RECV_PORT; + conn_params.send_port = ConnectionDefaults::LEGACY_NSP_SEND_PORT; + break; + + case DeviceType::NSP: + // Gemini NSP: same port for both send & recv + conn_params.device_address = ConnectionDefaults::NSP_ADDRESS; + conn_params.client_address = ""; // Auto-detect + conn_params.recv_port = ConnectionDefaults::NSP_PORT; + conn_params.send_port = ConnectionDefaults::NSP_PORT; + break; + + case DeviceType::HUB1: + // Gemini Hub1: same port for both send & recv + conn_params.device_address = ConnectionDefaults::HUB1_ADDRESS; + conn_params.client_address = ""; // Auto-detect + conn_params.recv_port = ConnectionDefaults::HUB1_PORT; + conn_params.send_port = ConnectionDefaults::HUB1_PORT; + break; + + case DeviceType::HUB2: + // Gemini Hub2: same port for both send & recv + conn_params.device_address = ConnectionDefaults::HUB2_ADDRESS; + conn_params.client_address = ""; // Auto-detect + conn_params.recv_port = ConnectionDefaults::HUB2_PORT; + conn_params.send_port = ConnectionDefaults::HUB2_PORT; + break; + + case DeviceType::HUB3: + // Gemini Hub3: same port for both send & recv + conn_params.device_address = ConnectionDefaults::HUB3_ADDRESS; + conn_params.client_address = ""; // Auto-detect + conn_params.recv_port = ConnectionDefaults::HUB3_PORT; + conn_params.send_port = ConnectionDefaults::HUB3_PORT; + break; + + case DeviceType::NPLAY: + // nPlayServer: loopback, different ports for send/recv + conn_params.device_address = ConnectionDefaults::NPLAY_ADDRESS; + conn_params.client_address = ""; // Auto-detect + conn_params.recv_port = ConnectionDefaults::LEGACY_NSP_RECV_PORT; + conn_params.send_port = ConnectionDefaults::LEGACY_NSP_SEND_PORT; + break; + + case DeviceType::CUSTOM: + // User must set addresses manually + break; + } + + return conn_params; +} + +ConnectionParams ConnectionParams::custom(const std::string& device_addr, + const std::string& client_addr, + const uint16_t recv_port, + const uint16_t send_port) { + ConnectionParams config; + config.type = DeviceType::CUSTOM; + config.device_address = device_addr; + config.client_address = client_addr; + config.recv_port = recv_port; + config.send_port = send_port; + return config; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// DeviceSession::Impl - Minimal Socket-Only Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +struct DeviceSession::Impl { + ConnectionParams config; + SOCKET socket = INVALID_SOCKET_VALUE; + SOCKADDR_IN recv_addr{}; // Address we bind to (client side) + SOCKADDR_IN send_addr{}; // Address we send to (device side) + bool connected = false; + + // Device configuration (from REQCONFIGALL) + cbproto::DeviceConfig device_config{}; + + // Timestamp conversion for non-Gemini devices + // Gemini devices send timestamps in nanoseconds; non-Gemini (e.g. NPlay) send sample counts. + // Determined from PROCREP ident field: if ident contains "gemini", timestamps are nanoseconds. + bool timestamps_are_nanoseconds = true; // Default true until PROCREP says otherwise + uint64_t ts_convert_num = 1; // Numerator of reduced (1e9/sysfreq) fraction + uint64_t ts_convert_den = 1; // Denominator of reduced (1e9/sysfreq) fraction + + // Clock synchronization + ClockSync clock_sync; + std::chrono::steady_clock::time_point last_recv_timestamp{}; + struct PendingClockProbe { + std::chrono::steady_clock::time_point t1_local; + bool active = false; + }; + PendingClockProbe pending_clock_probe; + std::mutex clock_probe_mutex; + + // General response waiting mechanism + struct PendingResponse { + std::function matcher; + std::condition_variable cv; + std::mutex mutex; + size_t expected_count = 1; // How many packets we expect + size_t received_count = 0; // How many we've received so far + }; + std::vector> pending_responses; + std::mutex pending_mutex; + + // Callback registration + struct CallbackRegistration { + CallbackHandle handle; + ReceiveCallback callback; + }; + struct DatagramCallbackRegistration { + CallbackHandle handle; + DatagramCompleteCallback callback; + }; + std::vector receive_callbacks; + std::vector datagram_callbacks; + std::mutex callback_mutex; + std::atomic has_callbacks{false}; // Fast-path skip when no callbacks registered + CallbackHandle next_callback_handle = 1; // 0 is reserved for "invalid" + + // Protocol monitor — dropped packet detection + uint32_t pkts_since_monitor = 0; // Packets counted since last SYSPROTOCOLMONITOR + bool first_monitor_seen = false; // Skip comparison until first baseline is established + uint32_t dropped_accum = 0; // Drops accumulated since last log + uint32_t sent_accum = 0; // Sent accumulated since last log + std::chrono::steady_clock::time_point last_drop_log_time{}; + + // Receive thread state + std::thread receive_thread; + std::atomic receive_thread_running{false}; + std::atomic receive_thread_stop_requested{false}; + + // Platform-specific state + #ifdef _WIN32 + bool wsa_initialized = false; + #endif + + void stopReceiveThreadInternal() { + if (receive_thread_running.load()) { + receive_thread_stop_requested.store(true); + if (receive_thread.joinable()) { + receive_thread.join(); + } + receive_thread_running.store(false); + receive_thread_stop_requested.store(false); + } + } + + ~Impl() { + // Stop receive thread before closing socket + stopReceiveThreadInternal(); + + if (socket != INVALID_SOCKET_VALUE) { + closeSocket(socket); + } + +#ifdef _WIN32 + if (wsa_initialized) { + WSACleanup(); + } +#endif + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// ResponseWaiter::Impl - PImpl pattern to hide internal details +/////////////////////////////////////////////////////////////////////////////////////////////////// +struct DeviceSession::ResponseWaiter::Impl { + std::shared_ptr response; + DeviceSession* session; + + Impl(std::shared_ptr resp, DeviceSession* sess) + : response(std::move(resp)), session(sess) {} +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// DeviceSession Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +DeviceSession::DeviceSession(DeviceSession&&) noexcept = default; +DeviceSession& DeviceSession::operator=(DeviceSession&&) noexcept = default; + +DeviceSession::~DeviceSession() { + close(); +} + +Result DeviceSession::create(const ConnectionParams& config) { + DeviceSession session; + session.m_impl = std::make_unique(); + session.m_impl->config = config; + + // Auto-detect client address if not specified + if (session.m_impl->config.client_address.empty()) { + session.m_impl->config.client_address = detectClientAddress(session.m_impl->config.type); + } + +#ifdef _WIN32 + // Initialize Winsock 2.2 + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { + return Result::error("Failed to initialize Winsock"); + } + session.m_impl->wsa_initialized = true; +#endif + + // Create UDP socket +#ifdef _WIN32 + session.m_impl->socket = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, NULL, 0, 0); +#else + session.m_impl->socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); +#endif + + if (session.m_impl->socket == INVALID_SOCKET_VALUE) { + return Result::error("Failed to create socket"); + } + + // Set socket options + int opt_one = 1; + + if (config.broadcast) { + if (setsockopt(session.m_impl->socket, SOL_SOCKET, SO_BROADCAST, + (char*)&opt_one, sizeof(opt_one)) != 0) { + return Result::error("Failed to set SO_BROADCAST"); + } + } + + // Set SO_REUSEADDR to allow multiple binds to same port + if (setsockopt(session.m_impl->socket, SOL_SOCKET, SO_REUSEADDR, + (char*)&opt_one, sizeof(opt_one)) != 0) { + return Result::error("Failed to set SO_REUSEADDR"); + } + + // Set receive buffer size + if (config.recv_buffer_size > 0) { + int buffer_size = config.recv_buffer_size; + if (setsockopt(session.m_impl->socket, SOL_SOCKET, SO_RCVBUF, + (char*)&buffer_size, sizeof(buffer_size)) != 0) { + return Result::error("Failed to set receive buffer size"); + } + + // Verify buffer size was set + socklen_t opt_len = sizeof(int); + if (getsockopt(session.m_impl->socket, SOL_SOCKET, SO_RCVBUF, + (char*)&buffer_size, &opt_len) != 0) { + return Result::error("Failed to verify receive buffer size"); + } + +#ifdef __linux__ + // Linux returns double the requested size + buffer_size /= 2; +#endif + + if (buffer_size < config.recv_buffer_size) { + return Result::error( + "Receive buffer size too small (got " + std::to_string(buffer_size) + + ", requested " + std::to_string(config.recv_buffer_size) + ")"); + } + } + + // Set send buffer size (if specified) + if (config.send_buffer_size > 0) { + int buffer_size = config.send_buffer_size; + if (setsockopt(session.m_impl->socket, SOL_SOCKET, SO_SNDBUF, + (char*)&buffer_size, sizeof(buffer_size)) != 0) { + return Result::error("Failed to set send buffer size"); + } + } + + // Set non-blocking mode + if (config.non_blocking) { +#ifdef _WIN32 + u_long arg_val = 1; + if (ioctlsocket(session.m_impl->socket, FIONBIO, &arg_val) == SOCKET_ERROR_VALUE) { +#else + if (fcntl(session.m_impl->socket, F_SETFL, O_NONBLOCK) != 0) { +#endif + return Result::error("Failed to set non-blocking mode"); + } + } else { + // For blocking sockets, set a receive timeout so the receive thread + // can periodically check its stop flag and shut down cleanly. +#ifdef _WIN32 + DWORD timeout_ms = 250; + setsockopt(session.m_impl->socket, SOL_SOCKET, SO_RCVTIMEO, + (const char*)&timeout_ms, sizeof(timeout_ms)); +#else + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 250000; // 250ms + setsockopt(session.m_impl->socket, SOL_SOCKET, SO_RCVTIMEO, + (const char*)&tv, sizeof(tv)); +#endif + } + + // Bind to client address and receive port + std::memset(&session.m_impl->recv_addr, 0, sizeof(session.m_impl->recv_addr)); + session.m_impl->recv_addr.sin_family = AF_INET; + session.m_impl->recv_addr.sin_port = htons(config.recv_port); + + if (config.client_address == "0.0.0.0" || config.client_address.empty()) { + session.m_impl->recv_addr.sin_addr.s_addr = htonl(INADDR_ANY); + } else { + session.m_impl->recv_addr.sin_addr.s_addr = inet_addr(config.client_address.c_str()); + } + +#ifdef __APPLE__ + session.m_impl->recv_addr.sin_len = sizeof(session.m_impl->recv_addr); +#endif + + if (bind(session.m_impl->socket, reinterpret_cast(&session.m_impl->recv_addr), + sizeof(session.m_impl->recv_addr)) != 0) { + const int bind_error = errno; // Capture errno immediately + return Result::error("Failed to bind socket to " + + config.client_address + ":" + std::to_string(config.recv_port) + + " (errno: " + std::to_string(bind_error) + " - " + std::strerror(bind_error) + ")"); + } + + // Set up send address (device side) + std::memset(&session.m_impl->send_addr, 0, sizeof(session.m_impl->send_addr)); + session.m_impl->send_addr.sin_family = AF_INET; + session.m_impl->send_addr.sin_port = htons(config.send_port); + session.m_impl->send_addr.sin_addr.s_addr = inet_addr(config.device_address.c_str()); + +#ifdef __APPLE__ + session.m_impl->send_addr.sin_len = sizeof(session.m_impl->send_addr); + + // macOS multi-interface routing fix: Bind socket to specific interface if using + // a specific IP address (not INADDR_ANY) + if (!config.client_address.empty() && + config.client_address != "0.0.0.0" && + session.m_impl->recv_addr.sin_addr.s_addr != htonl(INADDR_ANY)) { + + const unsigned int if_index = GetInterfaceIndexForIP(config.client_address.c_str()); + if (if_index > 0) { + // Best effort - don't fail if this doesn't work + setsockopt(session.m_impl->socket, IPPROTO_IP, IP_BOUND_IF, + &if_index, sizeof(if_index)); + } + } +#endif + + session.m_impl->connected = true; + return Result::ok(std::move(session)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// IDeviceSession Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession::receivePacketsRaw(void* buffer, const size_t buffer_size) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + + // Receive UDP datagram into provided buffer (non-blocking) + int bytes_recv = recv(m_impl->socket, (char*)buffer, buffer_size, 0); + + // Capture host timestamp as early as possible after recv() returns data + if (bytes_recv > 0) { + m_impl->last_recv_timestamp = std::chrono::steady_clock::now(); + } + + if (bytes_recv == SOCKET_ERROR_VALUE) { + #ifdef _WIN32 + int err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK || err == WSAETIMEDOUT) { + return Result::ok(0); // No data available (non-blocking or timeout) + } + return Result::error("recv failed: " + std::to_string(err)); + #else + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return Result::ok(0); // No data available (non-blocking or timeout) + } + return Result::error("recv failed: " + std::string(strerror(errno))); + #endif + } + + return Result::ok(bytes_recv); +} + +Result DeviceSession::receivePackets(void* buffer, const size_t buffer_size) { + // Receive packets from device + auto result = receivePacketsRaw(buffer, buffer_size); + + if (result.isOk() && result.value() > 0) { + // TODO: Update statistics + + // Update configuration from received packets (if any) + updateConfigFromBuffer(buffer, result.value()); + + // Convert timestamps from sample counts to nanoseconds for non-Gemini devices. + // The flag is set when PROCREP is processed in updateConfigFromBuffer above. + if (!m_impl->timestamps_are_nanoseconds && m_impl->ts_convert_den > 0) { + auto* bytes_ptr = static_cast(buffer); + const size_t total_bytes = result.value(); + size_t offset = 0; + while (offset + cbPKT_HEADER_SIZE <= total_bytes) { + auto* header = reinterpret_cast(bytes_ptr + offset); + const size_t packet_size = cbPKT_HEADER_SIZE + (header->dlen * 4); + if (offset + packet_size > total_bytes) break; + + header->time = header->time * m_impl->ts_convert_num / m_impl->ts_convert_den; + + offset += packet_size; + } + } + } + + return result; +} + +Result DeviceSession::sendPacket(const cbPKT_GENERIC& pkt) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + + // Calculate actual packet size from header + // dlen is in quadlets (4-byte units), so packet size = header + (dlen * 4) + const size_t packet_size = cbPKT_HEADER_SIZE + (pkt.cbpkt_header.dlen * 4); + return sendRaw(&pkt, packet_size); +} + +Result DeviceSession::sendPackets(const std::vector& pkts) { + if (pkts.empty()) { + return Result::error("Empty packet vector"); + } + + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + + // Send each packet as its own datagram with a small delay between packets. + // Note: Coalescing multiple packets into one datagram was tried but the device + // expects one packet per UDP datagram. + for (const auto& pkt : pkts) { + if (auto result = sendPacket(pkt); result.isError()) { + return result; + } +#ifdef _WIN32 + hr_sleep_us(50); +#else + std::this_thread::sleep_for(std::chrono::microseconds(50)); +#endif + } + + return Result::ok(); +} + +Result DeviceSession::sendRaw(const void* buffer, const size_t size) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + + if (!buffer || size == 0) { + return Result::error("Invalid buffer or size"); + } + + const int bytes_sent = sendto( + m_impl->socket, + (const char*)buffer, + size, + 0, + reinterpret_cast(&m_impl->send_addr), + sizeof(m_impl->send_addr) + ); + + if (bytes_sent == SOCKET_ERROR_VALUE) { +#ifdef _WIN32 + int err = WSAGetLastError(); + return Result::error("Send failed with error: " + std::to_string(err)); +#else + return Result::error("Send failed with error: " + std::string(strerror(errno))); +#endif + } + + return Result::ok(); +} + +bool DeviceSession::isConnected() const { + return m_impl && m_impl->connected; +} + +const ConnectionParams& DeviceSession::getConnectionParams() const { + return m_impl->config; +} + +ProtocolVersion DeviceSession::getProtocolVersion() const { + return ProtocolVersion::PROTOCOL_CURRENT; +} + +const cbproto::DeviceConfig& DeviceSession::getDeviceConfig() const { + return m_impl->device_config; +} + +const cbPKT_SYSINFO& DeviceSession::getSysInfo() const { + return m_impl->device_config.sysinfo; +} + +const cbPKT_CHANINFO* DeviceSession::getChanInfo(const uint32_t chan_id) const { + if (chan_id < 1 || chan_id > cbMAXCHANS) { + return nullptr; + } + return &m_impl->device_config.chaninfo[chan_id - 1]; +} + +void DeviceSession::close() { + if (!m_impl) return; + + if (m_impl->socket != INVALID_SOCKET_VALUE) { + closeSocket(m_impl->socket); + m_impl->socket = INVALID_SOCKET_VALUE; + } + + m_impl->connected = false; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Utility Functions +/////////////////////////////////////////////////////////////////////////////////////////////////// + +std::string detectClientAddress(DeviceType type) { + // Loopback devices: INADDR_ANY on all platforms. + // nPlayServer may broadcast to various addresses (127.0.0.255, 255.255.255.0, etc.) + // and binding to a specific loopback address misses broadcast traffic on macOS. + // INADDR_ANY reliably receives both unicast and broadcast on loopback. + if (type == DeviceType::NPLAY) { + return "0.0.0.0"; + } + +#ifdef __APPLE__ + // On macOS with multiple interfaces, use 0.0.0.0 (bind to all interfaces) + // macOS routing will handle interface selection via IP_BOUND_IF + return "0.0.0.0"; +#else +#ifdef _WIN32 + // Find a Windows adapter with IPv4 in 192\.168\.137\.* (Cerebus default ICS subnet). + // If found, return its unicast IPv4 address; otherwise return 0\.0\.0\.0. + const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER; + const ULONG family = AF_UNSPEC; + + ULONG bufLen = 16 * 1024; + std::vector buffer(bufLen); + auto addrs = reinterpret_cast(buffer.data()); + + DWORD ret = GetAdaptersAddresses(family, flags, nullptr, addrs, &bufLen); + if (ret == ERROR_BUFFER_OVERFLOW) { + buffer.resize(bufLen); + addrs = reinterpret_cast(buffer.data()); + ret = GetAdaptersAddresses(family, flags, nullptr, addrs, &bufLen); + } + if (ret != NO_ERROR) { + return "0.0.0.0"; + } + + for (IP_ADAPTER_ADDRESSES* aa = addrs; aa != nullptr; aa = aa->Next) { + // Skip adapters that are down or loopback + if (aa->OperStatus != IfOperStatusUp) continue; + if (aa->IfType == IF_TYPE_SOFTWARE_LOOPBACK) continue; + + for (IP_ADAPTER_UNICAST_ADDRESS* ua = aa->FirstUnicastAddress; ua != nullptr; ua = ua->Next) { + if (!ua->Address.lpSockaddr) continue; + if (ua->Address.lpSockaddr->sa_family != AF_INET) continue; + + auto* sin = reinterpret_cast(ua->Address.lpSockaddr); + char ip[INET_ADDRSTRLEN] = {}; + if (!inet_ntop(AF_INET, &sin->sin_addr, ip, sizeof(ip))) continue; + + // Match 192\.168\.137\.* prefix + if (std::strncmp(ip, "192.168.137.", 12) == 0) { + return std::string(ip); + } + } + } + + // Fallback: bind to all interfaces + return "0.0.0.0"; +#else + // Linux: To receive UDP broadcast packets, we must bind to the broadcast address + // (not the interface's unicast IP). Linux does not deliver broadcast packets to + // sockets bound to a specific unicast address. + struct ifaddrs *ifaddr, *ifa; + std::string result = "0.0.0.0"; // Default fallback + + if (getifaddrs(&ifaddr) != -1) { + for (ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_INET) + continue; + + struct sockaddr_in* addr = (struct sockaddr_in*)ifa->ifa_addr; + char ip_str[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &addr->sin_addr, ip_str, INET_ADDRSTRLEN); + + // Check if this is a 192.168.137.x address + if (std::strncmp(ip_str, "192.168.137.", 12) == 0) { + // Found the Cerebus interface - use the broadcast address to receive broadcasts + result = cbNET_UDP_ADDR_BCAST; // "192.168.137.255" + break; + } + } + freeifaddrs(ifaddr); + } + + return result; +#endif +#endif +} + +///-------------------------------------------------------------------------------------------- +/// Protocol Commands +///-------------------------------------------------------------------------------------------- + +Result DeviceSession::setSystemRunLevel(const uint32_t runlevel, const uint32_t resetque, const uint32_t runflags) { + // Create runlevel command packet + cbPKT_SYSINFO sysinfo = {}; + + // Fill header + sysinfo.cbpkt_header.time = 1; + sysinfo.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + sysinfo.cbpkt_header.type = cbPKTTYPE_SYSSETRUNLEV; + sysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; + sysinfo.cbpkt_header.instrument = 0; + + // Fill payload + sysinfo.runlevel = runlevel; + sysinfo.resetque = resetque; + sysinfo.runflags = runflags; + + // Send the packet (caller handles waiting for response) + // Safe to reinterpret_cast since cbPKT_GENERIC is designed to hold any packet type + return sendPacket(*reinterpret_cast(&sysinfo)); +} + +Result DeviceSession::setSystemRunLevelSync(uint32_t runlevel, uint32_t resetque, + uint32_t runflags, + std::chrono::milliseconds timeout) { + // Determine expected runlevel in response: + // - HARDRESET returns STANDBY (after 2 packets) + // - RESET returns RUNNING (after 2 packets) + // - Others return the same runlevel + uint32_t expected_runlevel; + switch (runlevel) { + case cbRUNLEVEL_HARDRESET: + expected_runlevel = cbRUNLEVEL_STANDBY; + break; + case cbRUNLEVEL_RESET: + expected_runlevel = cbRUNLEVEL_RUNNING; + break; + default: + expected_runlevel = runlevel; + break; + } + + return sendAndWait( + [this, runlevel, resetque, runflags]() { + return setSystemRunLevel(runlevel, resetque, runflags); + }, + [expected_runlevel](const cbPKT_HEADER* hdr) { + if ((hdr->chid & cbPKTCHAN_CONFIGURATION) != cbPKTCHAN_CONFIGURATION || + hdr->type != cbPKTTYPE_SYSREPRUNLEV) { + return false; + } + // Cast to full packet to check runlevel field + auto* sysinfo = reinterpret_cast(hdr); + return sysinfo->runlevel == expected_runlevel; + }, + timeout + ); +} + +Result DeviceSession::requestConfiguration() { + if (!m_impl) { + return Result::error("Device not initialized"); + } + + // Create REQCONFIGALL packet + cbPKT_GENERIC pkt = {}; + pkt.cbpkt_header.time = 1; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_REQCONFIGALL; + pkt.cbpkt_header.dlen = 0; // No payload + pkt.cbpkt_header.instrument = 0; + + // Send the packet + return sendPacket(pkt); +} + +Result DeviceSession::requestConfigurationSync(std::chrono::milliseconds timeout) { + return sendAndWait( + [this]() { return requestConfiguration(); }, + [](const cbPKT_HEADER* hdr) { + return (hdr->chid & cbPKTCHAN_CONFIGURATION) == cbPKTCHAN_CONFIGURATION && + hdr->type == cbPKTTYPE_SYSREP; + }, + timeout + ); +} + +Result DeviceSession::performHandshakeSync(std::chrono::milliseconds timeout) { + if (!m_impl) { + return Result::error("Device not initialized"); + } + + // Step 1: Try to set runlevel to RUNNING + auto result = setSystemRunLevelSync(cbRUNLEVEL_RUNNING, 0, 0, std::chrono::milliseconds(10)); + if (result.isError()) { + // If this fails, it's not critical - device may already be in a different state + // Continue with the handshake process + } + + // Step 2: Check current runlevel + uint32_t current_runlevel = m_impl->device_config.sysinfo.runlevel; + + // Step 3: If not RUNNING, do HARDRESET + if (current_runlevel != cbRUNLEVEL_RUNNING) { + result = setSystemRunLevelSync(cbRUNLEVEL_HARDRESET, 0, 0, timeout); + if (result.isError()) { + return result; // HARDRESET failed + } + // After HARDRESET, we should be in STANDBY + } + + // Step 4: Request configuration. + result = requestConfigurationSync(timeout); + if (result.isError()) { + return result; + } + + // Step 5: Do (soft) RESET if not RUNNING + current_runlevel = m_impl->device_config.sysinfo.runlevel; + if (current_runlevel != cbRUNLEVEL_RUNNING) { + result = setSystemRunLevelSync(cbRUNLEVEL_RESET, 0, 0, timeout); + if (result.isError()) { + return result; + } + // After RESET, we should be in RUNNING + } + + // Verify we're in RUNNING state + current_runlevel = m_impl->device_config.sysinfo.runlevel; + if (current_runlevel != cbRUNLEVEL_RUNNING) { + return Result::error("Failed to reach RUNNING state after handshake"); + } + + return Result::ok(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Clock Synchronization +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession::sendClockProbe() { + if (!m_impl || !m_impl->connected) + return Result::error("Device not connected"); + + auto now = std::chrono::steady_clock::now(); + { + std::lock_guard lock(m_impl->clock_probe_mutex); + m_impl->pending_clock_probe.t1_local = now; + m_impl->pending_clock_probe.active = true; + } + + // Send nPlay packet as clock probe. The host send time goes in .stime; + // firmware writes a fresh clock_gettime(ptp_clkid) into .etime before echoing back. + // Use an undefined mode (0xFFFF) so the device/nPlay server won't act on it + // but will still echo the packet back for our ping-pong clock loop. + cbPKT_NPLAY pkt{}; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_NPLAYSET; + pkt.cbpkt_header.dlen = cbPKTDLEN_NPLAY; + pkt.mode = 0xFFFF; + pkt.stime = static_cast( + std::chrono::duration_cast( + now.time_since_epoch()).count()); + + return sendPacket(*reinterpret_cast(&pkt)); +} + +std::optional +DeviceSession::toLocalTime(uint64_t device_time_ns) const { + if (!m_impl) return std::nullopt; + return m_impl->clock_sync.toLocalTime(device_time_ns); +} + +std::optional +DeviceSession::toDeviceTime(std::chrono::steady_clock::time_point local_time) const { + if (!m_impl) return std::nullopt; + return m_impl->clock_sync.toDeviceTime(local_time); +} + +std::optional DeviceSession::getOffsetNs() const { + if (!m_impl) return std::nullopt; + return m_impl->clock_sync.getOffsetNs(); +} + +std::optional DeviceSession::getUncertaintyNs() const { + if (!m_impl) return std::nullopt; + return m_impl->clock_sync.getUncertaintyNs(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Configuration +/////////////////////////////////////////////////////////////////////////////////////////////////// + +bool DeviceSession::channelMatchesType(const cbPKT_CHANINFO& chaninfo, const ChannelType chanType) { + const uint32_t caps = chaninfo.chancaps; + const uint32_t ainpcaps = chaninfo.ainpcaps; + const uint32_t aoutcaps = chaninfo.aoutcaps; + const uint32_t dinpcaps = chaninfo.dinpcaps; + + // Channel must exist and be connected + if ((cbCHAN_EXISTS | cbCHAN_CONNECTED) != (caps & (cbCHAN_EXISTS | cbCHAN_CONNECTED))) { + return false; + } + + // Check type-specific capabilities + switch (chanType) { + case ChannelType::FRONTEND: + // Front-end: analog input + isolated + return (cbCHAN_AINP | cbCHAN_ISOLATED) == (caps & (cbCHAN_AINP | cbCHAN_ISOLATED)); + + case ChannelType::ANALOG_IN: + // Analog input but not isolated + return (cbCHAN_AINP) == (caps & (cbCHAN_AINP | cbCHAN_ISOLATED)); + + case ChannelType::ANALOG_OUT: + // Analog output but not audio + return (cbCHAN_AOUT == (caps & cbCHAN_AOUT)) && + (cbAOUT_AUDIO != (aoutcaps & cbAOUT_AUDIO)); + + case ChannelType::AUDIO: + // Analog output + audio flag + return (cbCHAN_AOUT == (caps & cbCHAN_AOUT)) && + (cbAOUT_AUDIO == (aoutcaps & cbAOUT_AUDIO)); + + case ChannelType::DIGITAL_IN: + // Digital input with mask (but not serial) + return (cbCHAN_DINP == (caps & cbCHAN_DINP)) && + (dinpcaps & cbDINP_MASK) && + !(dinpcaps & cbDINP_SERIALMASK); + + case ChannelType::SERIAL: + // Digital input with serial mask + return (cbCHAN_DINP == (caps & cbCHAN_DINP)) && + (dinpcaps & cbDINP_SERIALMASK); + + case ChannelType::DIGITAL_OUT: + // Digital output + return cbCHAN_DOUT == (caps & cbCHAN_DOUT); + + default: + return false; + } +} + +size_t DeviceSession::countChannelsOfType(const ChannelType chanType, const size_t maxCount) const { + if (!m_impl) return 0; + + size_t count = 0; + for (uint32_t chan = 1; chan <= cbMAXCHANS && count < maxCount; ++chan) { + const auto& chaninfo = m_impl->device_config.chaninfo[chan - 1]; + if (channelMatchesType(chaninfo, chanType)) { + count++; + } + } + return count; +} + +Result DeviceSession::setChannelsGroupByType(const size_t nChans, const ChannelType chanType, const DeviceRate group_id, const bool disableOthers) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + + // Build vector of packets for all matching channels + std::vector packets; + packets.reserve(nChans); + + size_t count = 0; + for (uint32_t chan = 1; chan <= cbMAXCHANS; ++chan) { + if (!disableOthers && count >= nChans) { + break; // Only configure beyond nChans if disabling others + } + + auto& chaninfo = m_impl->device_config.chaninfo[chan - 1]; + + // Check if this channel matches the requested type + if (!channelMatchesType(chaninfo, chanType)) { + continue; + } + + // Create channel config packet + cbPKT_CHANINFO pkt = chaninfo; // Copy current config + pkt.chan = chan; + + const auto grp = count < nChans ? group_id : DeviceRate::NONE; // Set to group_id or disable (0) + + // Apply group-specific logic + if (grp != DeviceRate::NONE && grp != DeviceRate::SR_RAW) { + pkt.cbpkt_header.type = cbPKTTYPE_CHANSETSMP; // only need SMP for this. + pkt.smpgroup = static_cast(grp); + + // Set filter based on group mapping: {1: 5, 2: 6, 3: 7, 4: 10} + constexpr uint32_t filter_map[] = {0, 5, 6, 7, 10, 0, 0}; + pkt.smpfilter = filter_map[static_cast(grp)]; + + if (grp == DeviceRate::SR_30000) { + // Further disable raw stream (group 6) when enabling group 5; requires cbPKTTYPE_CHANSET + pkt.cbpkt_header.type = cbPKTTYPE_CHANSET; + pkt.ainpopts &= ~cbAINP_RAWSTREAM; // Clear group 6 flag + } + } + else if (grp == DeviceRate::SR_RAW) { + // Group 6: Raw + pkt.cbpkt_header.type = cbPKTTYPE_CHANSETAINP; + pkt.ainpopts |= cbAINP_RAWSTREAM; // Set group 6 flag + } + else if (grp == DeviceRate::NONE) { + // Group 0: disable all groups including raw (group 6) + pkt.cbpkt_header.type = cbPKTTYPE_CHANSET; + pkt.smpgroup = 0; + pkt.ainpopts &= ~cbAINP_RAWSTREAM; // Clear group 6 flag + } + + // Add packet to vector + packets.push_back(*reinterpret_cast(&pkt)); + count++; + } + + if (packets.empty()) { + return Result::error("No channels found matching type"); + } + + // Send all packets coalesced into minimal datagrams + return sendPackets(packets); +} + +Result DeviceSession::setChannelsGroupSync(const size_t nChans, const ChannelType chanType, const DeviceRate group_id, const std::chrono::milliseconds timeout) { + // Count matching channels, capped by requested count + const size_t total_matching = std::min(nChans, countChannelsOfType(chanType, cbMAXCHANS)); + if (total_matching == 0) { + return Result::error("No channels found matching type"); + } + + return sendAndWait( + [this, nChans, chanType, group_id]() { + return setChannelsGroupByType(nChans, chanType, group_id, true); + }, + [](const cbPKT_HEADER* hdr) { + return (hdr->chid & cbPKTCHAN_CONFIGURATION) == cbPKTCHAN_CONFIGURATION && + (hdr->type == cbPKTTYPE_CHANREPAINP || hdr->type == cbPKTTYPE_CHANREPSMP || hdr->type == cbPKTTYPE_CHANREP); + }, + timeout, + total_matching + ); +} + +Result DeviceSession::setChannelsACInputCouplingByType(const size_t nChans, const ChannelType chanType, const bool enabled) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + + // Build vector of packets for all matching channels + std::vector packets; + packets.reserve(nChans); + + size_t count = 0; + for (uint32_t chan = 1; chan <= cbMAXCHANS && count < nChans; ++chan) { + const auto& chaninfo = m_impl->device_config.chaninfo[chan - 1]; + + // Check if this channel matches the requested type + if (!channelMatchesType(chaninfo, chanType)) { + continue; + } + + // Create channel config packet + cbPKT_CHANINFO pkt = chaninfo; // Start with current config + pkt.cbpkt_header.type = cbPKTTYPE_CHANSETAINP; // Use analog input set command + pkt.chan = chan; + + // Set or clear offset correction flag + if (enabled) { + pkt.ainpopts |= cbAINP_OFFSET_CORRECT; + } else { + pkt.ainpopts &= ~cbAINP_OFFSET_CORRECT; + } + + // Add packet to vector + packets.push_back(*reinterpret_cast(&pkt)); + count++; + } + + if (packets.empty()) { + return Result::error("No channels found matching type"); + } + + // Send all packets coalesced into minimal datagrams + return sendPackets(packets); +} + +Result DeviceSession::setChannelsACInputCouplingSync(const size_t nChans, const ChannelType chanType, const bool enabled, const std::chrono::milliseconds timeout) { + const size_t total_matching = std::min(nChans, countChannelsOfType(chanType, cbMAXCHANS)); + return sendAndWait( + [this, nChans, chanType, enabled]() { + return setChannelsACInputCouplingByType(nChans, chanType, enabled); + }, + [](const cbPKT_HEADER* hdr) { + return (hdr->chid & cbPKTCHAN_CONFIGURATION) == cbPKTCHAN_CONFIGURATION && + hdr->type == cbPKTTYPE_CHANREPAINP; + }, + timeout, + total_matching + ); +} + +Result DeviceSession::setChannelsSpikeSortingByType(const size_t nChans, const ChannelType chanType, const uint32_t sortOptions) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + + // Build vector of packets for all matching channels + std::vector packets; + packets.reserve(nChans); + + // Configure first nChans channels of type chanType + for (size_t i = 0; i < nChans && i < cbMAXCHANS; ++i) { + const uint32_t chan = i + 1; + const auto& chaninfo = m_impl->device_config.chaninfo[i]; + + // Check if this channel matches the requested type + if (!channelMatchesType(chaninfo, chanType)) { + continue; + } + + // Create channel config packet + cbPKT_CHANINFO pkt = chaninfo; // Start with current config + pkt.cbpkt_header.type = cbPKTTYPE_CHANSETSPKTHR; // Use spike threshold set command + pkt.chan = chan; + + // Clear all spike sorting flags and set new ones + pkt.spkopts &= ~cbAINPSPK_ALLSORT; + pkt.spkopts |= sortOptions; + + // Add packet to vector + packets.push_back(*reinterpret_cast(&pkt)); + } + + if (packets.empty()) { + return Result::ok(); // No matching channels, but not an error + } + + // Send all packets coalesced into minimal datagrams + return sendPackets(packets); +} + +Result DeviceSession::setChannelsSpikeSortingSync(const size_t nChans, const ChannelType chanType, const uint32_t sortOptions, const std::chrono::milliseconds timeout) { + const size_t total_matching = std::min(nChans, countChannelsOfType(chanType, cbMAXCHANS)); + + return sendAndWait( + [this, nChans, chanType, sortOptions]() { + return setChannelsSpikeSortingByType(nChans, chanType, sortOptions); + }, + [](const cbPKT_HEADER* hdr) { + return (hdr->chid & cbPKTCHAN_CONFIGURATION) == cbPKTCHAN_CONFIGURATION && + hdr->type == cbPKTTYPE_CHANREPSPKTHR; + }, + timeout, + total_matching + ); +} + +Result DeviceSession::setChannelConfig(const cbPKT_CHANINFO& chaninfo) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + const auto& pkt = reinterpret_cast(chaninfo); + return sendPacket(pkt); +} + +Result DeviceSession::setDigitalOutput(const uint32_t chan_id, const uint16_t value) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + cbPKT_SET_DOUT pkt = {}; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_SET_DOUTSET; + pkt.cbpkt_header.dlen = cbPKTDLEN_SET_DOUT; + pkt.chan = static_cast(chan_id); + pkt.value = value; + return sendPacket(reinterpret_cast(pkt)); +} + +Result DeviceSession::sendComment(const std::string& comment, const uint32_t rgba, const uint8_t charset) { + if (!m_impl || !m_impl->connected) { + return Result::error("Device not connected"); + } + cbPKT_COMMENT pkt = {}; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_COMMENTSET; + pkt.cbpkt_header.dlen = cbPKTDLEN_COMMENT; + pkt.info.charset = charset; + pkt.timeStarted = 0; + pkt.rgba = rgba; + + const size_t len = std::min(comment.size(), static_cast(cbMAX_COMMENT - 1)); + std::memcpy(pkt.comment, comment.c_str(), len); + pkt.comment[len] = '\0'; + + return sendPacket(reinterpret_cast(pkt)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Management +/////////////////////////////////////////////////////////////////////////////////////////////////// + +void DeviceSession::updateConfigFromBuffer(const void* buffer, const size_t bytes) { + if (!buffer || bytes == 0 || !m_impl) { + return; // Nothing to process + } + + // Parse packets in buffer and update device_config for configuration packets + const auto* buff_bytes = static_cast(buffer); + size_t offset = 0; + + while (offset + cbPKT_HEADER_SIZE <= bytes) { + // Read packet header + const auto* header = reinterpret_cast(buff_bytes + offset); + const size_t packet_size = cbPKT_HEADER_SIZE + (header->dlen * 4); + + // Verify complete packet + if (offset + packet_size > bytes) { + break; // Incomplete packet + } + + // Count every packet for protocol monitor drop detection + m_impl->pkts_since_monitor++; + + if ((header->chid & cbPKTCHAN_CONFIGURATION) == cbPKTCHAN_CONFIGURATION) { + // Configuration packet - process based on type + if (header->type == cbPKTTYPE_SYSHEARTBEAT) { + // TODO: Handle heartbeat if needed + // Central uses this to prevent the system from going idle. + } + else if (header->type == cbPKTTYPE_LOGREP) { + const auto* logrep = reinterpret_cast(buff_bytes + offset); + // TODO: if (logrep->mode == cbLOG_MODE_CRITICAL) {} + // NODO: cbsdk uses this to process comments OnPktLog + } + else if ((header->type & 0xF0) == cbPKTTYPE_CHANREP) { + // NODO: Optionally rename the channel label if this device has a prefix + // NSP{instrument}-{label} or Hub{instrument)-{label} + // Note: Even though CHANSET* packets sent to the device only have some fields that are valid, + // and the device indeed only reads those fields, the CHANREP packets sent back by the device + // contain the full channel info structure with all fields valid. + const auto* chaninfo = reinterpret_cast(buff_bytes + offset); + if (const uint32_t chan = chaninfo->chan; chan > 0 && chan <= cbMAXCHANS) { + // The device always returns the complete channel info, even if only a subset changed. + m_impl->device_config.chaninfo[chan-1] = *chaninfo; + // Note: If this is exactly type == cbPKTTYPE_CHANREP, then we could invalidate cached spikes. + // spk_buffer->cache[chan-1].valid = 0; + } + } + else if (header->type == cbPKTTYPE_REPCONFIGALL) { + // Config flood starting - no action needed + } + else if ((header->type & 0xF0) == cbPKTTYPE_SYSREP) { + const auto* sysinfo = reinterpret_cast(buff_bytes + offset); + m_impl->device_config.sysinfo = *sysinfo; + + // If SYSREP arrives after PROCREP established non-Gemini identity, + // update conversion factors now that sysfreq is available. + if (!m_impl->timestamps_are_nanoseconds && sysinfo->sysfreq > 0) { + uint64_t g = std::gcd(uint64_t(1000000000), uint64_t(sysinfo->sysfreq)); + m_impl->ts_convert_num = 1000000000 / g; + m_impl->ts_convert_den = sysinfo->sysfreq / g; + } + + // Note: Clock sync probes now use nPlay packets (NPLAYREP), not SYSREPRUNLEV. + // SYSREPRUNLEV is still processed here for config tracking (sysinfo update above). + } + else if (header->type == cbPKTTYPE_GROUPREP) { + auto const *groupinfo = reinterpret_cast(buff_bytes + offset); + m_impl->device_config.groupinfo[groupinfo->group-1] = *groupinfo; + } + // else if (header->type == cbPKTTYPE_COMMENTREP) { + // // cbsdk uses this to update comment shared memory: OnPktComment + // } + else if (header->type == cbPKTTYPE_FILTREP) { + auto const *filtinfo = reinterpret_cast(buff_bytes + offset); + m_impl->device_config.filtinfo[filtinfo->filt-1] = *filtinfo; + } + else if (header->type == cbPKTTYPE_PROCREP) { + m_impl->device_config.procinfo = *reinterpret_cast(buff_bytes + offset); + + // Determine timestamp units from processor identity. + // Gemini devices report ident containing "gemini" and send nanosecond timestamps. + // Non-Gemini devices (e.g. NPlay: "256-Channel player...") send sample counts. + const auto& ident = m_impl->device_config.procinfo.ident; + size_t ident_len = strnlen(ident, sizeof(m_impl->device_config.procinfo.ident)); + static constexpr char needle[] = "gemini"; + bool is_gemini = std::search( + ident, ident + ident_len, + std::begin(needle), std::end(needle) - 1, // exclude null terminator + [](char a, char b) { return std::tolower(static_cast(a)) == b; } + ) != ident + ident_len; + + m_impl->timestamps_are_nanoseconds = is_gemini; + + if (!is_gemini) { + uint32_t sysfreq = m_impl->device_config.sysinfo.sysfreq; + if (sysfreq > 0) { + uint64_t g = std::gcd(uint64_t(1000000000), uint64_t(sysfreq)); + m_impl->ts_convert_num = 1000000000 / g; + m_impl->ts_convert_den = sysfreq / g; + } + } + } + else if (header->type == cbPKTTYPE_BANKREP) { + auto const *bankinfo = reinterpret_cast(buff_bytes + offset); + if (bankinfo->bank < cbMAXBANKS) { + m_impl->device_config.bankinfo[bankinfo->bank] = *bankinfo; + } + } + else if (header->type == cbPKTTYPE_SYSPROTOCOLMONITOR) { + // Not used for clock sync — sent from firmware's 3rd thread after 2 queues, + // giving it an undefined timestamp delay. + const auto* mon = reinterpret_cast(buff_bytes + offset); + if (m_impl->first_monitor_seen && mon->sentpkts > 0) { + const uint32_t received = m_impl->pkts_since_monitor; + if (received < mon->sentpkts) { + m_impl->dropped_accum += mon->sentpkts - received; + m_impl->sent_accum += mon->sentpkts; + auto now = std::chrono::steady_clock::now(); + if (now - m_impl->last_drop_log_time >= std::chrono::seconds(1)) { + fprintf(stderr, "[cbdev] dropped %u of %u packets\n", + m_impl->dropped_accum, m_impl->sent_accum); + m_impl->dropped_accum = 0; + m_impl->sent_accum = 0; + m_impl->last_drop_log_time = now; + } + } + } + m_impl->first_monitor_seen = true; + m_impl->pkts_since_monitor = 0; + } + else if (header->type == cbPKTTYPE_ADAPTFILTREP) { + m_impl->device_config.adaptinfo = *reinterpret_cast(buff_bytes + offset); + } + else if (header->type == cbPKTTYPE_REFELECFILTREP) { + m_impl->device_config.refelecinfo = *reinterpret_cast(buff_bytes + offset); + } + else if (header->type == cbPKTTYPE_SS_MODELREP) { + auto const *ssmodelrep = reinterpret_cast(buff_bytes + offset); + uint32_t unit = ssmodelrep->unit_number; + if (unit == 255) { + // Noise. Put it into the last slot. + unit = cbMAXUNITS + 1; // +1 because we init the array with +2. + } + // Note: ssmodelrep->chan is 0-based, unlike most other channel fields. + // Note: ssmodelrep->unit_number is 0-based because unit==0 means unsorted + m_impl->device_config.spike_sorting.models[ssmodelrep->chan][unit] = *ssmodelrep; + } + else if (header->type == cbPKTTYPE_SS_STATUSREP) { + m_impl->device_config.spike_sorting.status = *reinterpret_cast(buff_bytes + offset); + } + else if (header->type == cbPKTTYPE_SS_DETECTREP) { + m_impl->device_config.spike_sorting.detect = *reinterpret_cast(buff_bytes + offset); + } + else if (header->type == cbPKTTYPE_SS_ARTIF_REJECTREP) { + m_impl->device_config.spike_sorting.artifact_reject = *reinterpret_cast(buff_bytes + offset); + } + else if (header->type == cbPKTTYPE_SS_NOISE_BOUNDARYREP) { + auto const* noise_boundary = reinterpret_cast(buff_bytes + offset); + m_impl->device_config.spike_sorting.noise_boundary[noise_boundary->chan-1] = *noise_boundary; + } + else if (header->type == cbPKTTYPE_SS_STATISTICSREP) { + m_impl->device_config.spike_sorting.statistics = *reinterpret_cast(buff_bytes + offset); + } + else if (header->type == cbPKTTYPE_FS_BASISREP) { + auto const* fs_basis = reinterpret_cast(buff_bytes + offset); + if (fs_basis->chan != 0) { // chan==0 is for a request packet only + m_impl->device_config.spike_sorting.basis[fs_basis->chan-1] = *fs_basis; + } + } + else if (header->type == cbPKTTYPE_LNCREP) { + m_impl->device_config.lnc = *reinterpret_cast(buff_bytes + offset); + } + else if (header->type == cbPKTTYPE_REPFILECFG) { + auto const* filecfg = reinterpret_cast(buff_bytes + offset); + if (filecfg->options == cbFILECFG_OPT_REC + || filecfg->options == cbFILECFG_OPT_STOP + || filecfg->options == cbFILECFG_OPT_TIMEOUT) { + m_impl->device_config.fileinfo = *filecfg; + } + } + // else if (header->type == cbPKTTYPE_REPINITIMPEDANCE) { + // + // } + // else if (header->type == cbPKTTYPE_REPPOLL) { + // + // } + // else if (header->type == cbPKTTYPE_SETUNITSELECTION) { + // + // } + else if (header->type == cbPKTTYPE_REPNTRODEINFO) { + auto const* ntrodeinfo = reinterpret_cast(buff_bytes + offset); + m_impl->device_config.ntrodeinfo[ntrodeinfo->ntrode-1] = *ntrodeinfo; + } + // else if (header->type == cbPKTTYPE_NMREP) { + // // NODO: Abandon NM support + // } + else if (header->type == cbPKTTYPE_WAVEFORMREP) { + const auto* waveformrep = reinterpret_cast(buff_bytes + offset); + uint16_t chan = waveformrep->chan; + // Analog out channels start after analog input channels + if (chan > cbNUM_ANALOG_CHANS && chan <= cbNUM_ANALOG_CHANS + AOUT_NUM_GAIN_CHANS) { + uint16_t aout_idx = chan - cbNUM_ANALOG_CHANS - 1; + if (waveformrep->trigNum < cbMAX_AOUT_TRIGGER) { + m_impl->device_config.waveform[aout_idx][waveformrep->trigNum] = *waveformrep; + } + } + } + else if (header->type == cbPKTTYPE_NPLAYREP) { + // Complete pending clock sync probe from nPlay echo. + const auto* nplay = reinterpret_cast(buff_bytes + offset); + std::lock_guard lock(m_impl->clock_probe_mutex); + if (m_impl->pending_clock_probe.active) { + // New firmware (>=7.8 with clock sync mod) writes fresh + // clock_gettime(ptp_clkid) into .etime — zero staleness. + // Old firmware echoes the packet unchanged, so .etime stays 0 + // (we zero-initialize it). Fall back to header->time which is + // the stale ptptime from the previous main loop iteration. + constexpr uint64_t STALENESS_CORRECTION_NS = 165000; + const uint64_t device_time_ns = (nplay->etime != 0) + ? nplay->etime + : header->time + STALENESS_CORRECTION_NS; + + m_impl->clock_sync.addProbeSample( + m_impl->pending_clock_probe.t1_local, + device_time_ns, + m_impl->last_recv_timestamp); + + m_impl->pending_clock_probe.active = false; + } + } + + std::lock_guard lock(m_impl->pending_mutex); + for (auto& pending : m_impl->pending_responses) { + if (pending->received_count < pending->expected_count && pending->matcher(header)) { + std::lock_guard resp_lock(pending->mutex); + pending->received_count++; + if (pending->received_count >= pending->expected_count) { + pending->cv.notify_all(); + } + } + } + } + + offset += packet_size; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Receive Thread and Callbacks +/////////////////////////////////////////////////////////////////////////////////////////////////// + +CallbackHandle DeviceSession::registerReceiveCallback(ReceiveCallback callback) { + if (!m_impl || !callback) { + return 0; // Invalid + } + + std::lock_guard lock(m_impl->callback_mutex); + CallbackHandle handle = m_impl->next_callback_handle++; + m_impl->receive_callbacks.push_back({handle, std::move(callback)}); + m_impl->has_callbacks.store(true, std::memory_order_release); + return handle; +} + +CallbackHandle DeviceSession::registerDatagramCompleteCallback(DatagramCompleteCallback callback) { + if (!m_impl || !callback) { + return 0; // Invalid + } + + std::lock_guard lock(m_impl->callback_mutex); + CallbackHandle handle = m_impl->next_callback_handle++; + m_impl->datagram_callbacks.push_back({handle, std::move(callback)}); + m_impl->has_callbacks.store(true, std::memory_order_release); + return handle; +} + +void DeviceSession::unregisterCallback(CallbackHandle handle) { + if (!m_impl || handle == 0) { + return; + } + + std::lock_guard lock(m_impl->callback_mutex); + + // Check receive callbacks + auto& recv_cbs = m_impl->receive_callbacks; + recv_cbs.erase( + std::remove_if(recv_cbs.begin(), recv_cbs.end(), + [handle](const Impl::CallbackRegistration& reg) { + return reg.handle == handle; + }), + recv_cbs.end()); + + // Check datagram callbacks + auto& dg_cbs = m_impl->datagram_callbacks; + dg_cbs.erase( + std::remove_if(dg_cbs.begin(), dg_cbs.end(), + [handle](const Impl::DatagramCallbackRegistration& reg) { + return reg.handle == handle; + }), + dg_cbs.end()); + + // Update fast-path flag + m_impl->has_callbacks.store( + !recv_cbs.empty() || !dg_cbs.empty(), std::memory_order_release); +} + +Result DeviceSession::startReceiveThread() { + if (!m_impl) { + return Result::error("Device not initialized"); + } + + if (m_impl->receive_thread_running.load()) { + return Result::error("Receive thread already running"); + } + + m_impl->receive_thread_stop_requested.store(false); + m_impl->receive_thread_running.store(true); + + m_impl->receive_thread = std::thread([this]() { + // Receive buffer with padding. The extra sizeof(cbPKT_GENERIC) bytes ensure + // that reinterpret_cast(&buffer[offset]) always has a full + // struct's worth of readable memory, even for packets near the end of a + // datagram. Without this, callbacks that copy the full 1024-byte struct + // (e.g., SPSCQueue::push) would read past the buffer into unmapped memory. + uint8_t buffer[cbCER_UDP_SIZE_MAX + sizeof(cbPKT_GENERIC)] = {}; + + while (!m_impl->receive_thread_stop_requested.load()) { + // Receive packets (only fill the actual datagram portion, not the padding) + auto result = receivePackets(buffer, cbCER_UDP_SIZE_MAX); + + if (result.isError()) { + // TODO: Could invoke an error callback here + continue; + } + + const int bytes_received = result.value(); + if (bytes_received == 0) { + // No data available - brief sleep to avoid busy-waiting + std::this_thread::sleep_for(std::chrono::microseconds(100)); + continue; + } + + // Parse packets and invoke callbacks + size_t offset = 0; + while (offset + cbPKT_HEADER_SIZE <= static_cast(bytes_received)) { + const auto* pkt = reinterpret_cast(&buffer[offset]); + const size_t packet_size = cbPKT_HEADER_SIZE + (pkt->cbpkt_header.dlen * 4); + + // Verify complete packet + if (offset + packet_size > static_cast(bytes_received)) { + break; // Incomplete packet + } + + // Invoke receive callbacks (skip mutex if no callbacks registered) + if (m_impl->has_callbacks.load(std::memory_order_acquire)) { + std::lock_guard lock(m_impl->callback_mutex); + for (const auto& reg : m_impl->receive_callbacks) { + reg.callback(*pkt); + } + } + + offset += packet_size; + } + + // Invoke datagram complete callbacks (skip mutex if no callbacks registered) + if (m_impl->has_callbacks.load(std::memory_order_acquire)) { + std::lock_guard lock(m_impl->callback_mutex); + for (const auto& reg : m_impl->datagram_callbacks) { + reg.callback(); + } + } + } + + m_impl->receive_thread_running.store(false); + }); + + return Result::ok(); +} + +void DeviceSession::stopReceiveThread() { + if (m_impl) { + m_impl->stopReceiveThreadInternal(); + } +} + +bool DeviceSession::isReceiveThreadRunning() const { + return m_impl && m_impl->receive_thread_running.load(); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Response Waiter (General Mechanism) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +DeviceSession::ResponseWaiter::ResponseWaiter(std::unique_ptr impl) + : m_impl(std::move(impl)) {} + +DeviceSession::ResponseWaiter::~ResponseWaiter() { + if (m_impl && m_impl->session && m_impl->session->m_impl) { + // Remove this waiter from the pending list + std::lock_guard lock(m_impl->session->m_impl->pending_mutex); + auto& vec = m_impl->session->m_impl->pending_responses; + vec.erase(std::remove(vec.begin(), vec.end(), m_impl->response), vec.end()); + } + // unique_ptr automatically cleans up m_impl +} + +DeviceSession::ResponseWaiter::ResponseWaiter(ResponseWaiter&&) noexcept = default; + +DeviceSession::ResponseWaiter& DeviceSession::ResponseWaiter::operator=(ResponseWaiter&&) noexcept = default; + +Result DeviceSession::ResponseWaiter::wait(const std::chrono::milliseconds timeout) { + if (!m_impl || !m_impl->response) { + return Result::error("Invalid response waiter"); + } + + auto& response = m_impl->response; + std::unique_lock lock(response->mutex); + + if (response->received_count >= response->expected_count) { + return Result::ok(); // Already received all expected packets + } + + if (response->cv.wait_for(lock, timeout, [&response] { + return response->received_count >= response->expected_count; + })) { + return Result::ok(); + } else { + return Result::error("Response timeout"); + } +} + +DeviceSession::ResponseWaiter DeviceSession::registerResponseWaiter( + std::function matcher, + const size_t count) { + + auto response = std::make_shared(); + response->matcher = std::move(matcher); + response->expected_count = count; + + { + std::lock_guard lock(m_impl->pending_mutex); + m_impl->pending_responses.push_back(response); + } + + // Create ResponseWaiter::Impl and wrap in unique_ptr + auto waiter_impl = std::make_unique(response, this); + return ResponseWaiter(std::move(waiter_impl)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Private Helper for Synchronous Operations +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession::sendAndWait( + const std::function()>& sender, + std::function matcher, + const std::chrono::milliseconds timeout, + const size_t count) { + + // Register waiter BEFORE sending packet (avoids race condition) + auto waiter = registerResponseWaiter(std::move(matcher), count); + + // Send the request + auto result = sender(); + if (result.isError()) { + return result; + } + + // Wait for response + return waiter.wait(timeout); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Utility Functions +/////////////////////////////////////////////////////////////////////////////////////////////////// + +const char* protocolVersionToString(ProtocolVersion version) { + switch (version) { + case ProtocolVersion::UNKNOWN: + return "Unknown"; + case ProtocolVersion::PROTOCOL_311: + return "Protocol 3.11 (32-bit timestamps, 8-bit packet types)"; + case ProtocolVersion::PROTOCOL_400: + return "Protocol 4.0 (64-bit timestamps, 8-bit packet types)"; + case ProtocolVersion::PROTOCOL_410: + return "Protocol 4.1 (64-bit timestamps, 16-bit packet types)"; + case ProtocolVersion::PROTOCOL_CURRENT: + return "Protocol 4.2+ (current)"; + default: + return "Invalid Protocol Version"; + } +} + +const char* deviceTypeToString(DeviceType type) { + switch (type) { + case DeviceType::LEGACY_NSP: + return "Legacy NSP"; + case DeviceType::NSP: + return "Gemini NSP"; + case DeviceType::HUB1: + return "Gemini Hub 1"; + case DeviceType::HUB2: + return "Gemini Hub 2"; + case DeviceType::HUB3: + return "Gemini Hub 3"; + case DeviceType::NPLAY: + return "nPlayServer"; + case DeviceType::CUSTOM: + return "Custom"; + default: + return "Invalid Device Type"; + } +} + +const char* channelTypeToString(ChannelType type) { + switch (type) { + case ChannelType::FRONTEND: + return "Front-End Analog Input"; + case ChannelType::ANALOG_IN: + return "Analog Input"; + case ChannelType::ANALOG_OUT: + return "Analog Output"; + case ChannelType::AUDIO: + return "Audio Output"; + case ChannelType::DIGITAL_IN: + return "Digital Input"; + case ChannelType::SERIAL: + return "Serial Input"; + case ChannelType::DIGITAL_OUT: + return "Digital Output"; + default: + return "Invalid Channel Type"; + } +} + +const char* deviceRateToString(DeviceRate rate) { + switch (rate) { + case DeviceRate::NONE: + return "None"; + case DeviceRate::SR_500: + return "500 S/s"; + case DeviceRate::SR_1000: + return "1000 S/s"; + case DeviceRate::SR_2000: + return "2000 S/s"; + case DeviceRate::SR_10000: + return "10000 S/s"; + case DeviceRate::SR_30000: + return "30000 S/s"; + case DeviceRate::SR_RAW: + return "Raw Stream"; + default: + return "Invalid Device Rate"; + } +} + +} // namespace cbdev \ No newline at end of file diff --git a/src/cbdev/src/device_session_311.cpp b/src/cbdev/src/device_session_311.cpp new file mode 100644 index 00000000..b5c71427 --- /dev/null +++ b/src/cbdev/src/device_session_311.cpp @@ -0,0 +1,175 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session_311.cpp +/// @author CereLink Development Team +/// @date 2025-01-17 +/// +/// @brief Protocol 3.11 wrapper implementation +/// +/// Implements packet translation between protocol 3.11 and current (4.1+) formats. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include "device_session_311.h" +#include +#include + +namespace cbdev { + +using cbproto::PacketTranslator; +using cbproto::cbPKT_HEADER_311; +using cbproto::HEADER_SIZE_311; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// Protocol 3.11 Packet Header Layout (8 bytes total) +/// +/// Byte offset layout (from protocol_detector.cpp): +/// 0-3: time (32-bit) +/// 4-5: chid (16-bit) +/// 6: type (8-bit) +/// 7: dlen (8-bit) +/// +/// Current Protocol (4.1+) Header Layout (16 bytes total): +/// 0-7: time (64-bit) +/// 8-9: chid (16-bit) +/// 10-11: type (16-bit) +/// 12-13: dlen (16-bit) +/// 14: instrument (8-bit) +/// 15: reserved (8-bit) +/////////////////////////////////////////////////////////////////////////////////////////////////// + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Factory +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession_311::create(const ConnectionParams& config) { + // Create underlying device session for actual socket I/O + auto result = DeviceSession::create(config); + if (result.isError()) { + return Result::error(result.error()); + } + + // Construct wrapper with the device session + DeviceSession_311 session(std::move(result.value())); + return Result::ok(std::move(session)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// IDeviceSession Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession_311::receivePackets(void* buffer, const size_t buffer_size) { + // Temporary buffer for receiving 3.11 formatted packets + uint8_t src_buffer[cbCER_UDP_SIZE_MAX]; + + // Receive from underlying device (in 3.11 format) + auto result = m_device.receivePacketsRaw(src_buffer, sizeof(src_buffer)); + if (result.isError()) { + return result; + } + + const int bytes_received = result.value(); + if (bytes_received == 0) { + return Result::ok(0); // No data available + } + + // Translate 3.11 format to current format + auto* dest_buffer = static_cast(buffer); + size_t dest_offset = 0; + size_t src_offset = 0; + while (src_offset < static_cast(bytes_received)) { + // -- Header -- + // Check if we have enough data for a 3.11 header + if (src_offset + HEADER_SIZE_311 > static_cast(bytes_received)) { + break; // Incomplete packet + } + // And enough room for the reformatted header in the dest buffer + if ((dest_offset + cbPKT_HEADER_SIZE) > buffer_size) { + return Result::error("Output buffer too small for packet header"); + } + // Copy-convert the header data + const auto src_header = *reinterpret_cast(&src_buffer[src_offset]); + auto& dest_header = *reinterpret_cast(&dest_buffer[dest_offset]); + // Read 3.11 header fields using byte offsets + dest_header.time = static_cast(src_header.time) * 1000000000/30000; + dest_header.chid = src_header.chid; + dest_header.type = static_cast(src_header.type); + dest_header.dlen = static_cast(src_header.dlen); + + // -- Payload -- + if (src_offset + HEADER_SIZE_311 + src_header.dlen * 4 > static_cast(bytes_received)) { + break; // Incomplete packet + } + // Verify destination buffer has space. + // quadlet diff -- NPLAY: +4, COMMENT: +2, SYSPROTOCOLMONITOR: +1, CHANINFO: +0.75, CHANRESET: +0.25 + size_t pad_quads = 0; + if (dest_header.type == cbPKTTYPE_NPLAYREP) { + pad_quads = 4; + } else if (dest_header.type == cbPKTTYPE_COMMENTREP) { + pad_quads = 2; + } else if ( + (dest_header.type == cbPKTTYPE_SYSPROTOCOLMONITOR) + || ((dest_header.type & 0xF0) == cbPKTTYPE_CHANREP) + || (dest_header.type == cbPKTTYPE_CHANRESETREP)){ + pad_quads = 1; + } + if ((dest_offset + cbPKT_HEADER_SIZE + (dest_header.dlen + pad_quads) * 4) > buffer_size) { + return Result::error("Output buffer too small for translated packets"); + } + // Translate payload + const size_t dest_dlen = PacketTranslator::translatePayload_311_to_current( + &src_buffer[src_offset], &dest_buffer[dest_offset]); + dest_header.dlen = dest_dlen; // This was likely modified in place, but just in case... + + // Advance offsets + src_offset += HEADER_SIZE_311 + src_header.dlen * 4; + dest_offset += cbPKT_HEADER_SIZE + dest_header.dlen * 4; + } + + // Update configuration from translated packets + if (dest_offset > 0) { + m_device.updateConfigFromBuffer(dest_buffer, dest_offset); + } + + return Result::ok(static_cast(dest_offset)); +} + +Result DeviceSession_311::sendPacket(const cbPKT_GENERIC& pkt) { + // Translate current format to 3.11 format + uint8_t dest[cbPKT_MAX_SIZE]; + + if (pkt.cbpkt_header.type > 0xFF) { + return Result::error("Packet type too large for protocol 3.11 (max 255)"); + } + if (pkt.cbpkt_header.dlen > 0xFF) { + return Result::error("Packet dlen too large for protocol 3.11 (max 255)"); + } + + // -- Header -- + auto& dest_header = *reinterpret_cast(&dest[0]); + dest_header.time = static_cast(pkt.cbpkt_header.time * 30000 / 1000000000); + dest_header.chid = pkt.cbpkt_header.chid; + dest_header.type = static_cast(pkt.cbpkt_header.type); // Narrowing! + dest_header.dlen = static_cast(pkt.cbpkt_header.dlen); // Narrowing! + + // -- Payload -- + const size_t dest_dlen = PacketTranslator::translatePayload_current_to_311(pkt, dest); + dest_header.dlen = dest_dlen; + + // Send raw bytes directly via the device's sendRaw method + return m_device.sendRaw(dest, HEADER_SIZE_311 + dest_header.dlen * 4); +} + +Result DeviceSession_311::sendRaw(const void* buffer, const size_t size) { + // Pass through to underlying device + return m_device.sendRaw(buffer, size); +} + +ProtocolVersion DeviceSession_311::getProtocolVersion() const { + return ProtocolVersion::PROTOCOL_311; +} + +} // namespace cbdev diff --git a/src/cbdev/src/device_session_311.h b/src/cbdev/src/device_session_311.h new file mode 100644 index 00000000..7fa16cd0 --- /dev/null +++ b/src/cbdev/src/device_session_311.h @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session_311.h +/// @author CereLink Development Team +/// @date 2025-01-17 +/// +/// @brief Protocol 3.11 wrapper for DeviceSession +/// +/// DeviceSession_311 wraps a standard DeviceSession and translates packets between protocol 3.11 +/// format and the current protocol format (4.1+). This allows older devices to work seamlessly +/// with the modern SDK. +/// +/// Protocol 3.11 header differences: +/// - 32-bit timestamp (vs 64-bit in 4.1+) +/// - 8-bit type field (vs 16-bit in 4.1+) +/// - 8-bit dlen field (vs 16-bit in 4.1+) +/// - No instrument/reserved fields +/// - Header size: 8 bytes (vs 16 bytes in 4.1+) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_DEVICE_SESSION_311_H +#define CBDEV_DEVICE_SESSION_311_H + +#include "device_session_wrapper.h" +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Protocol 3.11 wrapper for device communication +/// +/// Translates packets between protocol 3.11 format and current format (4.1+). +/// Inherits from DeviceSessionWrapper which handles all delegation automatically. +/// +class DeviceSession_311 : public DeviceSessionWrapper { +public: + /// Create protocol 3.11 wrapper around a device session + /// @param config Device configuration (IP addresses, ports, device type) + /// @return DeviceSession_311 on success, error on failure + static Result create(const ConnectionParams& config); + + /// Move constructor + DeviceSession_311(DeviceSession_311&&) noexcept = default; + + /// Move assignment + DeviceSession_311& operator=(DeviceSession_311&&) noexcept = default; + + /// Destructor + ~DeviceSession_311() override = default; + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Protocol-Specific Overrides + /// @{ + + /// Receive packets from device and translate from 3.11 to current format + Result receivePackets(void* buffer, size_t buffer_size) override; + + /// Send packet to device, translating from current to 3.11 format + Result sendPacket(const cbPKT_GENERIC& pkt) override; + + /// Send raw bytes (pass-through to underlying device) + Result sendRaw(const void* buffer, size_t size) override; + + /// Get protocol version + [[nodiscard]] ProtocolVersion getProtocolVersion() const override; + + /// @} + +private: + /// Private constructor taking a DeviceSession + explicit DeviceSession_311(DeviceSession&& device) + : DeviceSessionWrapper(std::move(device)) {} +}; + +} // namespace cbdev + +#endif // CBDEV_DEVICE_SESSION_311_H diff --git a/src/cbdev/src/device_session_400.cpp b/src/cbdev/src/device_session_400.cpp new file mode 100644 index 00000000..e372b66a --- /dev/null +++ b/src/cbdev/src/device_session_400.cpp @@ -0,0 +1,174 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session_400.cpp +/// @author CereLink Development Team +/// @date 2025-01-17 +/// +/// @brief Protocol 4.0 wrapper implementation +/// +/// Implements packet translation between protocol 4.0 and current (4.1+) formats. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include "device_session_400.h" +#include +#include + +namespace cbdev { + +using cbproto::PacketTranslator; +using cbproto::cbPKT_HEADER_400; +using cbproto::HEADER_SIZE_400; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// Protocol 4.0 Packet Header Layout (16 bytes total) +/// +/// Byte offset layout (from protocol_detector.cpp): +/// 0-7: time (64-bit) +/// 8-9: chid (16-bit) +/// 10: type (8-bit) <-- Changed to 16-bit in 4.1+ +/// 11-12: dlen (16-bit) <-- Different byte offset in 4.1+ +/// 13: instrument (8-bit) <-- Different byte offset in 4.1+ +/// 14-15: reserved (16-bit) <-- Changed to 8-bit in 4.1+ +/// Total size is the same. +/// + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Factory +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession_400::create(const ConnectionParams& config) { + // Create underlying device session for actual socket I/O + auto result = DeviceSession::create(config); + if (result.isError()) { + return Result::error(result.error()); + } + + // Construct wrapper with the device session + DeviceSession_400 session(std::move(result.value())); + return Result::ok(std::move(session)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// IDeviceSession Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession_400::receivePackets(void* buffer, const size_t buffer_size) { + // Even though the header size is the same, protocol 4.0 had the old CHANINFO structure, + // which was 3 bytes smaller than the new structure. Given that we typically receive many + // CHANINFO packets in a row, we cannot reasonably write into `buffer` then 'readjust' the + // buffer contents, as that would require a lot of moving memory around. Thus, we write into + // a temporary buffer and then translate into the hopefully-large-enough `buffer`. + uint8_t src_buffer[cbCER_UDP_SIZE_MAX]; + + auto result = m_device.receivePacketsRaw(src_buffer, sizeof(src_buffer)); + if (result.isError()) { + return result; + } + + const int bytes_received = result.value(); + if (bytes_received == 0) { + return Result::ok(0); // No data available + } + + // Translate 4.0 format to current format + auto* dest_buffer = static_cast(buffer); + size_t dest_offset = 0; + size_t src_offset = 0; + while (src_offset < static_cast(bytes_received)) { + if (src_offset + HEADER_SIZE_400 > static_cast(bytes_received)) { + break; // Incomplete packet + } + + // -- Header -- + auto src_header = *reinterpret_cast(&src_buffer[src_offset]); + auto& dest_header = *reinterpret_cast(&dest_buffer[dest_offset]); + // When going from 4.0 to current, we fix the header as follows: + // 1. Read reserved from bytes 15-16, truncate to 8-bit, write to byte 16. + // 2. Read instrument from byte 14, write to byte 15. + // 3. Read dlen from bytes 12-13, write to bytes 13-14. + // 4. Read 8-bit `type` from byte 11, write 16-bit to bytes 11-12. + // First 10 bytes are unchanged. + dest_header.reserved = static_cast(src_header.reserved); + dest_header.instrument = src_header.instrument; + dest_header.dlen = src_header.dlen; + dest_header.type = static_cast(src_header.type); + dest_header.chid = src_header.chid; + dest_header.time = src_header.time; + + // -- Payload -- + if (src_offset + HEADER_SIZE_400 + src_header.dlen * 4 > static_cast(bytes_received)) { + break; // Incomplete packet + } + // Verify destination buffer has space. Need enough extra room for max difference in payload size. + size_t pad_quads = 0; + if ( + (dest_header.type == cbPKTTYPE_SYSPROTOCOLMONITOR) + || ((dest_header.type & 0xF0) == cbPKTTYPE_CHANREP) + || (dest_header.type == cbPKTTYPE_CHANRESETREP)){ + pad_quads = 1; + } + if ((dest_offset + cbPKT_HEADER_SIZE + (dest_header.dlen + pad_quads) * 4) > buffer_size) { + return Result::error("Output buffer too small for translated packets"); + } + // Translate payload + const size_t dest_dlen = PacketTranslator::translatePayload_400_to_current( + &src_buffer[src_offset], &dest_buffer[dest_offset]); + dest_header.dlen = dest_dlen; // This was likely modified in place, but just in case... + + // Advance offsets + src_offset += HEADER_SIZE_400 + src_header.dlen * 4; + dest_offset += cbPKT_HEADER_SIZE + dest_header.dlen * 4; + } + + // Update configuration from translated packets + if (dest_offset > 0) { + m_device.updateConfigFromBuffer(dest_buffer, dest_offset); + } + + return Result::ok(static_cast(dest_offset)); +} + +Result DeviceSession_400::sendPacket(const cbPKT_GENERIC& pkt) { + // Translate current format to 4.0 format + uint8_t temp_buffer[cbPKT_MAX_SIZE]; + + // -- Header -- + // Read current format header fields + /// When going from current to 4.0, we fix the header as follows: + /// 1. Read 16-bit type from bytes 11-12, shift right 8 bits to get 8-bit type, set on byte 11. + /// 2. Read dlen from bytes 13-14, write to bytes 12-13. + /// 3. Read instrument from byte 15, write to byte 14. + /// 4. Read reserved from byte 16, write to bytes 15-16 as 16-bit. + auto& dest_header = *reinterpret_cast(temp_buffer); + dest_header.time = pkt.cbpkt_header.time; // TODO: What if we are using time ticks, not nanoseconds? + dest_header.chid = pkt.cbpkt_header.chid; + dest_header.type = static_cast(pkt.cbpkt_header.type); + dest_header.dlen = pkt.cbpkt_header.dlen; + dest_header.instrument = pkt.cbpkt_header.instrument; + dest_header.reserved = static_cast(pkt.cbpkt_header.reserved); + + // -- Payload -- + auto* dest_payload = &temp_buffer[HEADER_SIZE_400]; + const size_t dest_dlen = PacketTranslator::translatePayload_current_to_400(pkt, dest_payload); + dest_header.dlen = dest_dlen; + const size_t packet_size_400 = HEADER_SIZE_400 + dest_header.dlen * 4; + + // Send raw bytes directly via the device's sendRaw method + return m_device.sendRaw(temp_buffer, packet_size_400); +} + +Result DeviceSession_400::sendRaw(const void* buffer, const size_t size) { + // Pass through to underlying device + return m_device.sendRaw(buffer, size); +} + +ProtocolVersion DeviceSession_400::getProtocolVersion() const { + return ProtocolVersion::PROTOCOL_400; +} + +} // namespace cbdev diff --git a/src/cbdev/src/device_session_400.h b/src/cbdev/src/device_session_400.h new file mode 100644 index 00000000..01587f92 --- /dev/null +++ b/src/cbdev/src/device_session_400.h @@ -0,0 +1,84 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session_400.h +/// @author CereLink Development Team +/// @date 2025-01-17 +/// +/// @brief Protocol 4.0 wrapper for DeviceSession +/// +/// DeviceSession_400 wraps a standard DeviceSession and translates packets between protocol 4.0 +/// format and the current protocol format (4.1+). This allows 4.0 devices to work seamlessly +/// with the modern SDK. +/// +/// Protocol 4.0 header differences (compared to 4.1+): +/// - 64-bit timestamp (same as 4.1+) +/// - 8-bit type field (vs 16-bit in 4.1+) <-- KEY DIFFERENCE +/// - 16-bit dlen field (same as 4.1+) +/// - 8-bit instrument field (same as 4.1+) +/// - 16-bit reserved field (vs 8-bit in 4.1+) <-- Different size +/// - Header size: 16 bytes (same as 4.1+, but different internal layout) +/// +/// Byte offset differences: +/// - 4.0: time(0-7) chid(8-9) type(10) dlen(11-12) instrument(13) reserved(14-15) +/// - 4.1+: time(0-7) chid(8-9) type(10-11) dlen(12-13) instrument(14) reserved(15) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_DEVICE_SESSION_400_H +#define CBDEV_DEVICE_SESSION_400_H + +#include "device_session_wrapper.h" +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Protocol 4.0 wrapper for device communication +/// +/// Translates packets between protocol 4.0 format and current format (4.1+). +/// Inherits from DeviceSessionWrapper which handles all delegation automatically. +/// +class DeviceSession_400 : public DeviceSessionWrapper { +public: + /// Create protocol 4.0 wrapper around a device session + /// @param config Device configuration (IP addresses, ports, device type) + /// @return DeviceSession_400 on success, error on failure + static Result create(const ConnectionParams& config); + + /// Move constructor + DeviceSession_400(DeviceSession_400&&) noexcept = default; + + /// Move assignment + DeviceSession_400& operator=(DeviceSession_400&&) noexcept = default; + + /// Destructor + ~DeviceSession_400() override = default; + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Protocol-Specific Overrides + /// @{ + + /// Receive packets from device and translate from 4.0 to current format + Result receivePackets(void* buffer, size_t buffer_size) override; + + /// Send packet to device, translating from current to 4.0 format + Result sendPacket(const cbPKT_GENERIC& pkt) override; + + /// Send raw bytes (pass-through to underlying device) + Result sendRaw(const void* buffer, size_t size) override; + + /// Get protocol version + [[nodiscard]] ProtocolVersion getProtocolVersion() const override; + + /// @} + +private: + /// Private constructor taking a DeviceSession + explicit DeviceSession_400(DeviceSession&& device) + : DeviceSessionWrapper(std::move(device)) {} +}; + +} // namespace cbdev + +#endif // CBDEV_DEVICE_SESSION_400_H diff --git a/src/cbdev/src/device_session_410.cpp b/src/cbdev/src/device_session_410.cpp new file mode 100644 index 00000000..b4949544 --- /dev/null +++ b/src/cbdev/src/device_session_410.cpp @@ -0,0 +1,124 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session_410.cpp +/// @author CereLink Development Team +/// @date 2025-01-17 +/// +/// @brief Protocol 4.10 wrapper implementation +/// +/// Implements packet translation between protocol 4.10 and current (4.2+) formats. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include "device_session_410.h" +#include +#include + +namespace cbdev { + +using cbproto::PacketTranslator; +using cbproto::HEADER_SIZE_410; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// Protocol 4.10 Packet Header Layout (16 bytes total) +/// Identical to Protocol 4.2 +/// +/// Byte offset layout: +/// 0-7: time (64-bit) +/// 8-9: chid (16-bit) +/// 10-11: type (16-bit) +/// 12-13: dlen (16-bit) +/// 14: instrument (8-bit) +/// 15: reserved (8-bit) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Factory +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession_410::create(const ConnectionParams& config) { + // Create underlying device session for actual socket I/O + auto result = DeviceSession::create(config); + if (result.isError()) { + return Result::error(result.error()); + } + + // Construct wrapper with the device session + DeviceSession_410 session(std::move(result.value())); + return Result::ok(std::move(session)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// IDeviceSession Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result DeviceSession_410::receivePackets(void* buffer, const size_t buffer_size) { + // Receive from underlying device (in 4.10 format) + // Format is similar enough to current that we can receive directly into buffer. + // CHANRESET adds 1 byte to the payload but that packet is not actually sent by the device so we can ignore. + auto result = m_device.receivePacketsRaw(buffer, buffer_size); + if (result.isError()) { + return result; + } + + const int bytes_received = result.value(); + if (bytes_received == 0) { + return Result::ok(0); // No data available + } + + // Translate 4.10 format to current format + auto* buff_bytes = static_cast(buffer); + size_t offset = 0; + while (offset < static_cast(bytes_received)) { + if (offset + HEADER_SIZE_410 > static_cast(bytes_received)) { + break; // Incomplete packet + } + // -- Header -- unchanged + auto header = *reinterpret_cast(&buff_bytes[offset]); + // -- Payload -- + if (offset + HEADER_SIZE_410 + header.dlen * 4 > static_cast(bytes_received)) { + break; // Incomplete packet + } + // For packets that have different payload structures, additional translation may be needed + const size_t dest_dlen = PacketTranslator::translatePayload_410_to_current( + &buff_bytes[offset], &buff_bytes[offset]); + header.dlen = dest_dlen; // This was likely modified in place, but just in case... + + // Advance offsets + offset += HEADER_SIZE_410 + header.dlen * 4; + } + + // Update configuration from translated packets (now in current format) + if (offset > 0) { + m_device.updateConfigFromBuffer(buffer, offset); + } + + return Result::ok(static_cast(offset)); +} + +Result DeviceSession_410::sendPacket(const cbPKT_GENERIC& pkt) { + // Formats are nearly identical. + // Nevertheless, the src pkt is const so we make a copy to modify. + cbPKT_GENERIC new_pkt; + std::memcpy(&new_pkt, &pkt, sizeof(cbPKT_GENERIC)); + auto* pkt_bytes = reinterpret_cast(&new_pkt); + const size_t dest_dlen = PacketTranslator::translatePayload_current_to_410(pkt, pkt_bytes); + new_pkt.cbpkt_header.dlen = dest_dlen; + const size_t packet_size_410 = HEADER_SIZE_410 + new_pkt.cbpkt_header.dlen * 4; + + // Send raw bytes directly via the device's sendRaw method + return m_device.sendRaw(pkt_bytes, packet_size_410); +} + +Result DeviceSession_410::sendRaw(const void* buffer, const size_t size) { + // Pass through to underlying device + return m_device.sendRaw(buffer, size); +} + +ProtocolVersion DeviceSession_410::getProtocolVersion() const { + return ProtocolVersion::PROTOCOL_410; +} + +} // namespace cbdev diff --git a/src/cbdev/src/device_session_410.h b/src/cbdev/src/device_session_410.h new file mode 100644 index 00000000..d906ba8f --- /dev/null +++ b/src/cbdev/src/device_session_410.h @@ -0,0 +1,84 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session_410.h +/// @author CereLink Development Team +/// @date 2025-01-17 +/// +/// @brief Protocol 4.10 wrapper for DeviceSession +/// +/// DeviceSession_410 wraps a standard DeviceSession and translates packets between protocol 4.10 +/// format and the current protocol format (4.2+). This allows 4.10 devices to work seamlessly +/// with the modern SDK. +/// +/// Protocol 4.10 header differences (compared to 4.2+): +/// - 64-bit timestamp (same as 4.2+) +/// - 8-bit type field (vs 16-bit in 4.2+) <-- KEY DIFFERENCE +/// - 16-bit dlen field (same as 4.2+) +/// - 8-bit instrument field (same as 4.2+) +/// - 16-bit reserved field (vs 8-bit in 4.2+) <-- Different size +/// - Header size: 16 bytes (same as 4.2+, but different internal layout) +/// +/// Byte offset differences: +/// - 4.10: time(0-7) chid(8-9) type(10) dlen(11-12) instrument(13) reserved(14-15) +/// - 4.2+: time(0-7) chid(8-9) type(10-11) dlen(12-13) instrument(14) reserved(15) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_DEVICE_SESSION_410_H +#define CBDEV_DEVICE_SESSION_410_H + +#include "device_session_wrapper.h" +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Protocol 4.10 wrapper for device communication +/// +/// Translates packets between protocol 4.10 format and current format (4.2+). +/// Inherits from DeviceSessionWrapper which handles all delegation automatically. +/// +class DeviceSession_410 : public DeviceSessionWrapper { +public: + /// Create protocol 4.10 wrapper around a device session + /// @param config Device configuration (IP addresses, ports, device type) + /// @return DeviceSession_410 on success, error on failure + static Result create(const ConnectionParams& config); + + /// Move constructor + DeviceSession_410(DeviceSession_410&&) noexcept = default; + + /// Move assignment + DeviceSession_410& operator=(DeviceSession_410&&) noexcept = default; + + /// Destructor + ~DeviceSession_410() override = default; + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Protocol-Specific Overrides + /// @{ + + /// Receive packets from device and translate from 4.10 to current format + Result receivePackets(void* buffer, size_t buffer_size) override; + + /// Send packet to device, translating from current to 4.10 format + Result sendPacket(const cbPKT_GENERIC& pkt) override; + + /// Send raw bytes (pass-through to underlying device) + Result sendRaw(const void* buffer, size_t size) override; + + /// Get protocol version + [[nodiscard]] ProtocolVersion getProtocolVersion() const override; + + /// @} + +private: + /// Private constructor taking a DeviceSession + explicit DeviceSession_410(DeviceSession&& device) + : DeviceSessionWrapper(std::move(device)) {} +}; + +} // namespace cbdev + +#endif // CBDEV_DEVICE_SESSION_410_H diff --git a/src/cbdev/src/device_session_impl.h b/src/cbdev/src/device_session_impl.h new file mode 100644 index 00000000..c8eade11 --- /dev/null +++ b/src/cbdev/src/device_session_impl.h @@ -0,0 +1,343 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session_impl.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Minimal UDP socket wrapper for Cerebus device communication +/// +/// DeviceSession is a thin wrapper around UDP sockets for communicating with Cerebus devices. +/// It provides only socket operations - no threads, no callbacks, no statistics, no parsing. +/// All orchestration logic (threads, statistics, callbacks, parsing) is handled by SdkSession. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_DEVICE_SESSION_IMPL_H +#define CBDEV_DEVICE_SESSION_IMPL_H + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#ifdef _WIN32 + typedef SOCKET SocketHandle; +#else + typedef int SocketHandle; +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Minimal UDP socket wrapper for device communication (current protocol) +/// +/// Provides synchronous send/receive operations, with optional receive thread for +/// callback-based packet handling. Implements IDeviceSession for protocol abstraction. +/// +class DeviceSession : public IDeviceSession { +public: + /// Move constructor + DeviceSession(DeviceSession&&) noexcept; + + /// Move assignment + DeviceSession& operator=(DeviceSession&&) noexcept; + + /// Destructor - closes socket + ~DeviceSession() override; + + /// Create and initialize device session + /// @param config Device configuration (IP addresses, ports, device type) + /// @return DeviceSession on success, error on failure + static Result create(const ConnectionParams& config); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name IDeviceSession Implementation + /// @{ + + /// Receive packets from device and update configuration + /// High-level receive that automatically updates device config from received packets. + /// @param buffer Destination buffer for received data + /// @param buffer_size Maximum bytes to receive + /// @return Number of bytes received, or error + /// @note Returns 0 if no data available (EWOULDBLOCK) + Result receivePackets(void* buffer, size_t buffer_size) override; + + /// Receive packets without updating configuration (used by protocol wrappers) + /// Low-level socket receive that does not parse or update config. + /// Protocol wrappers use this to receive untranslated data. + /// @param buffer Destination buffer for received data + /// @param buffer_size Maximum bytes to receive + /// @return Number of bytes received, or error + /// @note Returns 0 if no data available (EWOULDBLOCK) + Result receivePacketsRaw(void* buffer, size_t buffer_size); + + /// Send single packet to device + /// @param pkt Packet to send + /// @return Success or error + Result sendPacket(const cbPKT_GENERIC& pkt) override; + + /// Send multiple packets to device, coalesced into minimal UDP datagrams + /// @param pkts Vector of packets to send + /// @return Success or error + /// @note Packets are batched into datagrams up to MTU size to reduce packet loss + Result sendPackets(const std::vector& pkts) override; + + /// Send raw bytes to device + /// @param buffer Buffer containing raw bytes + /// @param size Number of bytes to send + /// @return Success or error + Result sendRaw(const void* buffer, size_t size) override; + + /// Check if socket is open + /// @return true if connected + [[nodiscard]] bool isConnected() const override; + + /// Get device configuration + /// @return Configuration reference + [[nodiscard]] const ConnectionParams& getConnectionParams() const override; + + /// Get protocol version + /// @return Protocol version (PROTOCOL_CURRENT for this session) + [[nodiscard]] ProtocolVersion getProtocolVersion() const override; + + /// Get full device configuration + [[nodiscard]] const cbproto::DeviceConfig& getDeviceConfig() const override; + + /// Get system information + [[nodiscard]] const cbPKT_SYSINFO& getSysInfo() const override; + + /// Get channel information for specific channel + [[nodiscard]] const cbPKT_CHANINFO* getChanInfo(uint32_t chan_id) const override; + + /// @} + + /// Close socket (also called by destructor) + void close(); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Protocol Commands + /// @{ + + /// Set device system runlevel (asynchronous) + /// Sends cbPKTYPE_SETRUNLEVEL to change device operating state. + /// Does NOT wait for response - caller must handle SYSREP monitoring. + /// @param runlevel Desired runlevel (cbRUNLEVEL_*) + /// @param resetque Channel for reset to queue on + /// @param runflags Lock recording after reset + /// @return Success or error + /// @note For synchronous version, use setSystemRunLevelSync() + Result setSystemRunLevel(uint32_t runlevel, uint32_t resetque, uint32_t runflags) override; + + /// Request all configuration from the device (asynchronous) + /// Sends cbPKTTYPE_REQCONFIGALL which triggers the device to send all config packets. + /// Does NOT wait for response - caller must handle config flood and final SYSREP. + /// @return Success or error + /// @note For synchronous version, use requestConfigurationSync() + Result requestConfiguration() override; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Channel Configuration + /// @{ + + /// Count channels matching a specific type + [[nodiscard]] size_t countChannelsOfType(ChannelType chanType, size_t maxCount) const override; + + /// Set sampling group for first N channels of a specific type + Result setChannelsGroupByType(size_t nChans, ChannelType chanType, DeviceRate group_id, bool disableOthers) override; + + Result setChannelsGroupSync(size_t nChans, ChannelType chanType, DeviceRate group_id, std::chrono::milliseconds timeout) override; + + /// Set AC input coupling for first N channels of a specific type + Result setChannelsACInputCouplingByType(size_t nChans, ChannelType chanType, bool enabled) override; + + /// Set spike sorting options for first N channels + Result setChannelsSpikeSortingByType(size_t nChans, ChannelType chanType, uint32_t sortOptions) override; + + Result setChannelConfig(const cbPKT_CHANINFO& chaninfo) override; + Result setDigitalOutput(uint32_t chan_id, uint16_t value) override; + Result sendComment(const std::string& comment, uint32_t rgba = 0, uint8_t charset = 0) override; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Response Waiting (General Mechanism) + /// @{ + + /// RAII helper for waiting on packet responses + /// Automatically registers/unregisters matcher with device session + class ResponseWaiter { + public: + /// Wait for the response packet to arrive + /// @param timeout Maximum time to wait + /// @return Success if packet received, error on timeout + Result wait(std::chrono::milliseconds timeout); + + /// Destructor - automatically unregisters from device session + ~ResponseWaiter(); + + // Non-copyable, movable + ResponseWaiter(const ResponseWaiter&) = delete; + ResponseWaiter& operator=(const ResponseWaiter&) = delete; + ResponseWaiter(ResponseWaiter&&) noexcept; + ResponseWaiter& operator=(ResponseWaiter&&) noexcept; + + private: + friend class DeviceSession; + + struct Impl; // Forward declaration - defined in .cpp + ResponseWaiter(std::unique_ptr impl); + + std::unique_ptr m_impl; + }; + + /// Register a response waiter that will be notified when a matching packet arrives + /// @param matcher Function that returns true when the desired packet is received + /// @param count Number of matching packets to wait for (default: 1) + /// @return ResponseWaiter object - call wait() to block until packet arrives + /// @note The matcher is checked against all configuration packets in updateConfigFromBuffer + ResponseWaiter registerResponseWaiter(std::function matcher, size_t count = 1); + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Synchronous Protocol Commands + /// @{ + + /// Request configuration synchronously (blocks until SYSREP received) + /// @param timeout Maximum time to wait + /// @return Success if config received, error on timeout or send failure + Result requestConfigurationSync(std::chrono::milliseconds timeout) override; + + /// Set system runlevel synchronously (blocks until SYSREPRUNLEV received) + /// @param runlevel Desired runlevel + /// @param resetque Channel for reset to queue on + /// @param runflags Lock recording after reset + /// @param timeout Maximum time to wait + /// @return Success if response received, error on timeout or send failure + Result setSystemRunLevelSync(uint32_t runlevel, uint32_t resetque, uint32_t runflags, + std::chrono::milliseconds timeout) override; + + /// Set AC input coupling synchronously (blocks until CHANREP received) + /// @param nChans Number of channels to configure + /// @param chanType Channel type filter + /// @param enabled true to enable AC coupling, false to disable + /// @param timeout Maximum time to wait + /// @return Success if response received, error on timeout or send failure + Result setChannelsACInputCouplingSync(size_t nChans, ChannelType chanType, bool enabled, + std::chrono::milliseconds timeout) override; + + Result setChannelsSpikeSortingSync(size_t nChans, ChannelType chanType, uint32_t sortOptions, std::chrono::milliseconds timeout) override; + + /// Perform complete device handshake sequence (synchronous) + /// @param timeout Maximum total time for entire handshake sequence + /// @return Success if device reaches RUNNING state, error otherwise + Result performHandshakeSync(std::chrono::milliseconds timeout) override; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Configuration Management + /// @{ + + /// Update device configuration from received packet buffer + /// Parses the buffer for configuration packets and updates internal config accordingly. + /// This should be called after receiving packets (and after protocol translation for wrappers). + /// @param buffer Buffer containing packets in current protocol format + /// @param bytes Number of bytes in buffer + void updateConfigFromBuffer(const void* buffer, size_t bytes); + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Receive Thread and Callbacks + /// @{ + + /// Register a callback to be invoked for each received packet + /// @param callback Function to call for each packet + /// @return Handle for unregistration, or 0 on failure + /// @note Callbacks run on the receive thread - keep them fast to avoid packet loss! + /// @note Multiple callbacks can be registered and will be called in registration order + CallbackHandle registerReceiveCallback(ReceiveCallback callback) override; + + /// Register a callback to be invoked after all packets in a datagram are processed + /// @param callback Function to call after datagram processing + /// @return Handle for unregistration, or 0 on failure + /// @note Use this for batch operations like signaling shared memory + CallbackHandle registerDatagramCompleteCallback(DatagramCompleteCallback callback) override; + + /// Unregister a previously registered callback + /// @param handle Handle returned by registerReceiveCallback or registerDatagramCompleteCallback + void unregisterCallback(CallbackHandle handle) override; + + /// Start the receive thread + /// @return Success or error if thread cannot be started + /// @note Thread calls receivePackets() in a loop and invokes registered callbacks + Result startReceiveThread() override; + + /// Stop the receive thread + /// @note Blocks until thread terminates + void stopReceiveThread() override; + + /// Check if receive thread is running + /// @return true if thread is active + [[nodiscard]] bool isReceiveThreadRunning() const override; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Clock Synchronization + /// @{ + + [[nodiscard]] std::optional + toLocalTime(uint64_t device_time_ns) const override; + + [[nodiscard]] std::optional + toDeviceTime(std::chrono::steady_clock::time_point local_time) const override; + + Result sendClockProbe() override; + + [[nodiscard]] std::optional getOffsetNs() const override; + [[nodiscard]] std::optional getUncertaintyNs() const override; + + /// @} + +private: + /// Private constructor (use create() factory) + DeviceSession() = default; + + /// Helper method to check if a channel matches a specific type based on capabilities + /// @param chaninfo Channel information packet + /// @param chanType Channel type to check + /// @return true if channel matches the type + static bool channelMatchesType(const cbPKT_CHANINFO& chaninfo, ChannelType chanType); + + /// Helper for synchronous send-and-wait pattern + /// @param sender Function that sends the request packet + /// @param matcher Function that identifies the response packet + /// @param timeout Maximum time to wait for response + /// @param count Number of matching packets to wait for (default: 1) + /// @return Success if response received, error on timeout or send failure + Result sendAndWait( + const std::function()>& sender, + std::function matcher, + std::chrono::milliseconds timeout, + size_t count = 1 + ); + + /// Implementation details (pImpl pattern) + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace cbdev + +#endif // CBDEV_DEVICE_SESSION_IMPL_H diff --git a/src/cbdev/src/device_session_wrapper.h b/src/cbdev/src/device_session_wrapper.h new file mode 100644 index 00000000..66c3d160 --- /dev/null +++ b/src/cbdev/src/device_session_wrapper.h @@ -0,0 +1,404 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file device_session_wrapper.h +/// @author CereLink Development Team +/// @date 2025-01-24 +/// +/// @brief Base class for protocol wrappers - eliminates boilerplate delegation +/// +/// DeviceSessionWrapper provides a base class for protocol-specific wrappers that handles +/// all delegation to the wrapped DeviceSession. Subclasses only need to override: +/// - receivePackets() - for protocol → current translation +/// - sendPacket() - for current → protocol translation +/// - getProtocolVersion() - to return the protocol version +/// +/// All other IDeviceSession methods are automatically delegated to the wrapped device. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_DEVICE_SESSION_WRAPPER_H +#define CBDEV_DEVICE_SESSION_WRAPPER_H + +// IMPORTANT: device_session_impl.h includes Windows headers FIRST (before cbproto), +// so it must be included before any other headers that might include cbproto. +#include "device_session_impl.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Base class for protocol wrappers - handles delegation to wrapped DeviceSession +/// +/// Protocol wrappers only need to override receivePackets() and sendPacket() for translation. +/// All other methods are automatically delegated to the wrapped device. +/// +class DeviceSessionWrapper : public IDeviceSession { +protected: + /// Wrapped device session for actual I/O and logic + DeviceSession m_device; + + /// Protected constructor - only subclasses can create + explicit DeviceSessionWrapper(DeviceSession&& device) + : m_device(std::move(device)) {} + +public: + virtual ~DeviceSessionWrapper() { + // Stop receive thread before destruction + stopReceiveThread(); + } + + // Non-copyable, movable (uses pImpl for thread state) + DeviceSessionWrapper(const DeviceSessionWrapper&) = delete; + DeviceSessionWrapper& operator=(const DeviceSessionWrapper&) = delete; + DeviceSessionWrapper(DeviceSessionWrapper&&) noexcept = default; + DeviceSessionWrapper& operator=(DeviceSessionWrapper&&) noexcept = default; + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Methods That Must Be Overridden (Protocol-Specific) + /// @{ + + /// Receive packets with protocol translation + /// Subclasses MUST override to translate from protocol format → current format + Result receivePackets(void* buffer, size_t buffer_size) override = 0; + + /// Send packet with protocol translation + /// Subclasses MUST override to translate from current format → protocol format + Result sendPacket(const cbPKT_GENERIC& pkt) override = 0; + + /// Get protocol version + /// Subclasses MUST override to return their specific protocol version + [[nodiscard]] ProtocolVersion getProtocolVersion() const override = 0; + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Auto-Delegated Methods (Same for All Protocols) + /// @{ + + /// Send multiple packets with translation via virtual sendPacket() + Result sendPackets(const std::vector& pkts) override { + if (pkts.empty()) { + return Result::error("Empty packet vector"); + } + + // Send each packet as its own datagram with a small delay between packets. + // Uses virtual sendPacket() so derived classes handle protocol translation. + for (const auto& pkt : pkts) { + if (auto result = sendPacket(pkt); result.isError()) { + return result; + } + std::this_thread::sleep_for(std::chrono::microseconds(50)); + } + + return Result::ok(); + } + + /// Send raw bytes (delegated to wrapped device) + Result sendRaw(const void* buffer, const size_t size) override { + return m_device.sendRaw(buffer, size); + } + + /// Check if connected (delegated to wrapped device) + [[nodiscard]] bool isConnected() const override { + return m_device.isConnected(); + } + + /// Get connection parameters (delegated to wrapped device) + [[nodiscard]] const ConnectionParams& getConnectionParams() const override { + return m_device.getConnectionParams(); + } + + /// Get device configuration (delegated to wrapped device) + [[nodiscard]] const cbproto::DeviceConfig& getDeviceConfig() const override { + return m_device.getDeviceConfig(); + } + + /// Get system information (delegated to wrapped device) + [[nodiscard]] const cbPKT_SYSINFO& getSysInfo() const override { + return m_device.getSysInfo(); + } + + /// Get channel information (delegated to wrapped device) + [[nodiscard]] const cbPKT_CHANINFO* getChanInfo(const uint32_t chan_id) const override { + return m_device.getChanInfo(chan_id); + } + + /// Set system runlevel - async (delegated to wrapped device) + Result setSystemRunLevel(const uint32_t runlevel, const uint32_t resetque, const uint32_t runflags) override { + return m_device.setSystemRunLevel(runlevel, resetque, runflags); + } + + /// Request configuration - async (delegated to wrapped device) + Result requestConfiguration() override { + return m_device.requestConfiguration(); + } + + /// Request configuration - sync (delegated to wrapped device) + Result requestConfigurationSync(const std::chrono::milliseconds timeout) override { + return m_device.requestConfigurationSync(timeout); + } + + /// Set system runlevel - sync (delegated to wrapped device) + Result setSystemRunLevelSync(const uint32_t runlevel, const uint32_t resetque, const uint32_t runflags, + const std::chrono::milliseconds timeout) override { + return m_device.setSystemRunLevelSync(runlevel, resetque, runflags, timeout); + } + + /// Perform complete device handshake sequence - sync (delegated to wrapped device) + Result performHandshakeSync(const std::chrono::milliseconds timeout) override { + return m_device.performHandshakeSync(timeout); + } + + /// Count channels matching type (delegated to wrapped device) + [[nodiscard]] size_t countChannelsOfType(const ChannelType chanType, const size_t maxCount) const override { + return m_device.countChannelsOfType(chanType, maxCount); + } + + /// Set sampling group for channels by type (delegated to wrapped device) + Result setChannelsGroupByType(const size_t nChans, const ChannelType chanType, const DeviceRate group_id, const bool disableOthers) override { + return m_device.setChannelsGroupByType(nChans, chanType, group_id, disableOthers); + } + + Result setChannelsGroupSync(const size_t nChans, const ChannelType chanType, const DeviceRate group_id, const std::chrono::milliseconds timeout) override { + return m_device.setChannelsGroupSync(nChans, chanType, group_id, timeout); + } + + /// Set AC input coupling for channels by type (delegated to wrapped device) + Result setChannelsACInputCouplingByType(const size_t nChans, const ChannelType chanType, const bool enabled) override { + return m_device.setChannelsACInputCouplingByType(nChans, chanType, enabled); + } + + Result setChannelsACInputCouplingSync(const size_t nChans, const ChannelType chanType, const bool enabled, + const std::chrono::milliseconds timeout) override { + return m_device.setChannelsACInputCouplingSync(nChans, chanType, enabled, timeout); + } + + /// Set spike sorting options (delegated to wrapped device) + Result setChannelsSpikeSortingByType(const size_t nChans, const ChannelType chanType, const uint32_t sortOptions) override { + return m_device.setChannelsSpikeSortingByType(nChans, chanType, sortOptions); + } + + Result setChannelsSpikeSortingSync(const size_t nChans, const ChannelType chanType, const uint32_t sortOptions, const std::chrono::milliseconds timeout) override { + return m_device.setChannelsSpikeSortingSync(nChans, chanType, sortOptions, timeout); + } + + Result setChannelConfig(const cbPKT_CHANINFO& chaninfo) override { + return m_device.setChannelConfig(chaninfo); + } + + Result setDigitalOutput(const uint32_t chan_id, const uint16_t value) override { + return m_device.setDigitalOutput(chan_id, value); + } + + Result sendComment(const std::string& comment, const uint32_t rgba, const uint8_t charset) override { + return m_device.sendComment(comment, rgba, charset); + } + + /// Clock sync delegation (uses m_device's ClockSync which is fed by + /// receivePacketsRaw and updateConfigFromBuffer on the same call path) + std::optional + toLocalTime(uint64_t device_time_ns) const override { + return m_device.toLocalTime(device_time_ns); + } + + std::optional + toDeviceTime(std::chrono::steady_clock::time_point local_time) const override { + return m_device.toDeviceTime(local_time); + } + + Result sendClockProbe() override { + return m_device.sendClockProbe(); + } + + [[nodiscard]] std::optional getOffsetNs() const override { + return m_device.getOffsetNs(); + } + + [[nodiscard]] std::optional getUncertaintyNs() const override { + return m_device.getUncertaintyNs(); + } + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Receive Thread and Callbacks (Wrapper-Specific Implementation) + /// @{ + + /// Register a receive callback + /// @note Uses wrapper's own callback storage (not underlying device's) + CallbackHandle registerReceiveCallback(ReceiveCallback callback) override { + if (!callback || !m_thread_state) { + return 0; + } + std::lock_guard lock(m_thread_state->callback_mutex); + CallbackHandle handle = m_thread_state->next_callback_handle++; + m_thread_state->receive_callbacks.push_back({handle, std::move(callback)}); + return handle; + } + + /// Register a datagram complete callback + CallbackHandle registerDatagramCompleteCallback(DatagramCompleteCallback callback) override { + if (!callback || !m_thread_state) { + return 0; + } + std::lock_guard lock(m_thread_state->callback_mutex); + CallbackHandle handle = m_thread_state->next_callback_handle++; + m_thread_state->datagram_callbacks.push_back({handle, std::move(callback)}); + return handle; + } + + /// Unregister a callback + void unregisterCallback(CallbackHandle handle) override { + if (handle == 0 || !m_thread_state) { + return; + } + std::lock_guard lock(m_thread_state->callback_mutex); + + // Check receive callbacks + auto& recv_cbs = m_thread_state->receive_callbacks; + recv_cbs.erase( + std::remove_if(recv_cbs.begin(), recv_cbs.end(), + [handle](const CallbackRegistration& reg) { return reg.handle == handle; }), + recv_cbs.end()); + + // Check datagram callbacks + auto& dg_cbs = m_thread_state->datagram_callbacks; + dg_cbs.erase( + std::remove_if(dg_cbs.begin(), dg_cbs.end(), + [handle](const DatagramCallbackRegistration& reg) { return reg.handle == handle; }), + dg_cbs.end()); + } + + /// Start the receive thread + /// @note Thread calls this wrapper's receivePackets() (with translation) + Result startReceiveThread() override { + if (!m_thread_state) { + return Result::error("Thread state not initialized"); + } + + if (m_thread_state->receive_thread_running.load()) { + return Result::error("Receive thread already running"); + } + + m_thread_state->receive_thread_stop_requested.store(false); + m_thread_state->receive_thread_running.store(true); + + m_thread_state->receive_thread = std::thread([this]() { + uint8_t buffer[cbCER_UDP_SIZE_MAX]; + + while (!m_thread_state->receive_thread_stop_requested.load()) { + // Call virtual receivePackets() - handles protocol translation + auto result = this->receivePackets(buffer, sizeof(buffer)); + + if (result.isError()) { + continue; + } + + const int bytes_received = result.value(); + if (bytes_received == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + continue; + } + + // Parse packets and invoke callbacks + size_t offset = 0; + while (offset + cbPKT_HEADER_SIZE <= static_cast(bytes_received)) { + const auto* pkt = reinterpret_cast(&buffer[offset]); + const size_t packet_size = cbPKT_HEADER_SIZE + (pkt->cbpkt_header.dlen * 4); + + if (offset + packet_size > static_cast(bytes_received)) { + break; + } + + // Invoke receive callbacks + { + std::lock_guard lock(m_thread_state->callback_mutex); + for (const auto& reg : m_thread_state->receive_callbacks) { + reg.callback(*pkt); + } + } + + offset += packet_size; + } + + // Invoke datagram complete callbacks + { + std::lock_guard lock(m_thread_state->callback_mutex); + for (const auto& reg : m_thread_state->datagram_callbacks) { + reg.callback(); + } + } + } + + m_thread_state->receive_thread_running.store(false); + }); + + return Result::ok(); + } + + /// Stop the receive thread + void stopReceiveThread() override { + if (m_thread_state) { + m_thread_state->stop(); + } + } + + /// Check if receive thread is running + [[nodiscard]] bool isReceiveThreadRunning() const override { + return m_thread_state && m_thread_state->receive_thread_running.load(); + } + + /// @} + +private: + // Callback storage (in pImpl for move semantics) + struct CallbackRegistration { + CallbackHandle handle; + ReceiveCallback callback; + }; + struct DatagramCallbackRegistration { + CallbackHandle handle; + DatagramCompleteCallback callback; + }; + + // Thread state pImpl - allows DeviceSessionWrapper to be movable + struct ThreadState { + std::vector receive_callbacks; + std::vector datagram_callbacks; + std::mutex callback_mutex; + CallbackHandle next_callback_handle = 1; + + std::thread receive_thread; + std::atomic receive_thread_running{false}; + std::atomic receive_thread_stop_requested{false}; + + void stop() { + if (receive_thread_running.load()) { + receive_thread_stop_requested.store(true); + if (receive_thread.joinable()) { + receive_thread.join(); + } + receive_thread_running.store(false); + receive_thread_stop_requested.store(false); + } + } + + ~ThreadState() { + stop(); + } + }; + + std::unique_ptr m_thread_state = std::make_unique(); +}; + +} // namespace cbdev + +#endif // CBDEV_DEVICE_SESSION_WRAPPER_H diff --git a/src/cbdev/src/platform_first.h b/src/cbdev/src/platform_first.h new file mode 100644 index 00000000..828f0dd6 --- /dev/null +++ b/src/cbdev/src/platform_first.h @@ -0,0 +1,35 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file platform_first.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Platform headers that MUST be included before cbproto headers +/// +/// IMPORTANT: This header must be included FIRST in any source file that uses both +/// Windows headers and cbproto headers. +/// +/// Reason: cbproto uses #pragma pack(1) for network structures. Windows SDK headers +/// (specifically winnt.h) have a static_assert that fails if packing is not at the +/// default setting when they are included. This header ensures Windows headers are +/// processed before any pack directives are active. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_PLATFORM_FIRST_H +#define CBDEV_PLATFORM_FIRST_H + +#ifdef _WIN32 + // Windows headers must be included before cbproto headers due to packing requirements + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include + #include + #include + #include +#endif + +#endif // CBDEV_PLATFORM_FIRST_H diff --git a/src/cbdev/src/protocol_detector.cpp b/src/cbdev/src/protocol_detector.cpp new file mode 100644 index 00000000..57d2e05f --- /dev/null +++ b/src/cbdev/src/protocol_detector.cpp @@ -0,0 +1,525 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file protocol_detector.cpp +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Protocol version detection implementation +/// +/// Header layout (values in bits): +/// +/// | version | time | chid | type | dlen | instrument | reserved | +/// |---------|------|------|------|------|------------|----------| +/// | 3.11 | 32b | 16b | 8b | 8b | 0b | 0b | +/// | 4.0 | 64b | 16b | 8b | 16b | 8b | 16b | +/// | 4.1+ | 64b | 16b | 16b | 16b | 8b | 8b | +/// +/// Then, if it's a SYSREPRUNLEV packet, the payload contains: +/// +/// | version | sysfreq | spikelen | spikepre | resetque | runlevel | runflags | total | +/// |---------|---------|----------|----------|----------|----------|----------|-------| +/// | 3.11 | 32b | 32b | 32b | 32b | 32b | 32b | 256b | +/// | 4.0 | 32b | 32b | 32b | 32b | 32b | 32b | 320b | +/// | 4.1+ | 32b | 32b | 32b | 32b | 32b | 32b | 320b | +/// Expected values: +/// chid: cbPKTCHAN_CONFIGURATION +/// type: cbPKTTYPE_SYSREPRUNLEV +/// dlen: 6 (for SYSREPRUNLEV packets) +/// sysfreq: 30000 (0x7530) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#ifdef _WIN32 + #pragma comment(lib, "ws2_32.lib") + #define SOCKET_ERROR_VALUE SOCKET_ERROR + typedef int socklen_t; +#else + #include + #include + #include + #include + #include + #include + #include + #define SOCKET_ERROR_VALUE -1 + #define INVALID_SOCKET -1 + #define closesocket close + typedef int SOCKET; +#endif + +#include "protocol_detector.h" +#include +#include +#include +#include +#include +#include + +namespace cbdev { + +/// State shared between main thread and receive thread +struct DetectionState { + std::atomic send_config{false}; + std::atomic done{false}; + // TODO: State machine to create REQCONFIGALL and capture the first packet in the response + std::atomic detected_version{ProtocolVersion::PROTOCOL_CURRENT}; + SOCKET sock = INVALID_SOCKET; + // Debug counters + std::atomic packets_seen{0}; + std::atomic bytes_received{0}; + std::atomic config_packets_seen{0}; + std::atomic procrep_seen{0}; +}; + +/// @brief Guess packet size when protocol version is ambiguous +/// @param buffer Pointer to start of packet (at least 16 bytes available) +/// @param buffer_size Remaining bytes in buffer +/// @return Number of bytes to advance, or 0 if unable to determine +/// +/// Reads all three possible dlen locations (3.11, 4.0, 4.1+) and uses heuristics +/// to determine the most likely packet size. +static size_t guessPacketSize(const uint8_t* buffer, size_t buffer_size) { + if (buffer_size < 16) { + return 0; // Not enough data to analyze + } + + // Read all three possible dlen interpretations + constexpr size_t HEADER_311 = 8; + constexpr size_t HEADER_400 = 16; + constexpr size_t HEADER_410 = 16; + + uint8_t dlen_311 = buffer[7]; // 3.11: byte 7 + uint16_t dlen_400 = *reinterpret_cast(&buffer[11]); // 4.0: bytes 11-12 + uint16_t dlen_410 = *reinterpret_cast(&buffer[12]); // 4.1+: bytes 12-13 + + // Calculate potential packet sizes + size_t size_311 = HEADER_311 + dlen_311 * 4; + size_t size_400 = HEADER_400 + dlen_400 * 4; + size_t size_410 = HEADER_410 + dlen_410 * 4; + + // Validate against maximum packet size + constexpr size_t MAX_SIZE = 1024; + bool valid_311 = (size_311 <= MAX_SIZE); + bool valid_400 = (size_400 <= MAX_SIZE); + bool valid_410 = (size_410 <= MAX_SIZE); + + // Count valid interpretations + int valid_count = valid_311 + valid_400 + valid_410; + + if (valid_count == 0) { + return 0; // All invalid - corrupted data + } + + if (valid_count == 1) { + // Only one valid interpretation - use it + if (valid_311) return size_311; + if (valid_400) return size_400; + if (valid_410) return size_410; + } + + // Multiple valid interpretations - use heuristics to disambiguate + + // Heuristic 1: Timestamp magnitude + // 4.0/4.1+ use 64-bit timestamps (often nanoseconds, which are huge numbers) + // 3.11 first 8 bytes are: time(32) | chid(16) | type(8) | dlen(8) + uint64_t first_8_bytes = *reinterpret_cast(&buffer[0]); + if (first_8_bytes > (1ULL << 40)) { // > ~1 trillion + // Almost certainly a real 64-bit timestamp (4.0 or 4.1+) + // Prefer 4.1+ over 4.0 (newer protocol) + if (valid_410) return size_410; + if (valid_400) return size_400; + } + + // Heuristic 2: Channel validity + // Channels have known valid ranges + uint16_t chid_311 = *reinterpret_cast(&buffer[4]); + uint16_t chid_400 = *reinterpret_cast(&buffer[8]); + + auto is_valid_chid = [](uint16_t chid) { + return (chid == 0) || // Special channel + (chid >= 1 && chid <= 768) || // Front-end channels (Gemini max) + (chid >= 0x8000 && chid <= 0x8FFF); // Configuration channels + }; + + bool valid_chid_311 = is_valid_chid(chid_311); + bool valid_chid_400 = is_valid_chid(chid_400); // Same for 4.0 and 4.1+ + + if (valid_chid_400 && !valid_chid_311) { + // 4.0 or 4.1+ more likely + if (valid_410) return size_410; + if (valid_400) return size_400; + } else if (valid_chid_311 && !valid_chid_400) { + if (valid_311) return size_311; + } + + // Heuristic 3: Type validity + // Check if packet type is in known ranges + uint8_t type_311 = buffer[6]; + uint8_t type_400 = buffer[10]; + uint16_t type_410 = *reinterpret_cast(&buffer[10]); + + auto is_valid_type = [](uint16_t type) { + // Known packet type ranges (see cbproto/types.h) + return (type >= 0x01 && type <= 0x10) || // System types + (type >= 0x20 && type <= 0x30) || // Channel info types + (type >= 0x40 && type <= 0x5F) || // Data/preview types + (type >= 0x81 && type <= 0x90) || // SET types + (type >= 0xA1 && type <= 0xCF) || // REP types + (type >= 0xD0 && type <= 0xEF); // Config/file types + }; + + int valid_type_count = is_valid_type(type_311) + + is_valid_type(type_400) + + is_valid_type(type_410); + + if (valid_type_count == 1) { + if (is_valid_type(type_311) && valid_311) return size_311; + if (is_valid_type(type_400) && valid_400) return size_400; + if (is_valid_type(type_410) && valid_410) return size_410; + } + + // Heuristic 4: Default to most recent protocol if still ambiguous + // This is a reasonable assumption for newer devices + if (valid_410) return size_410; + if (valid_400) return size_400; + if (valid_311) return size_311; + + return 0; // Should never reach here +} + +/// Receive thread function - listens for packets and analyzes protocol version +void receiveThread(DetectionState* state) { + std::vector recv_buffer(cbPKT_MAX_SIZE * 1024); + + while (!state->done) { + const int bytes_received = recv(state->sock, reinterpret_cast(recv_buffer.data()), recv_buffer.size(), 0); + + if (bytes_received == SOCKET_ERROR_VALUE) { + // Timeout or error - thread will be stopped by main thread + continue; + } + state->bytes_received += bytes_received; + uint8_t* buffer = recv_buffer.data(); + + size_t offset = 0; + while (offset < bytes_received && bytes_received - offset >= 32) { + ++state->packets_seen; + + // Remaining bytes in buffer are at least as large as 3.11 SYSINFO packet. + // This filters out some smaller packets we might want to ignore. + // Try 3.11 SYSREPRUNLEV first + if ( + (*reinterpret_cast(buffer + offset + 4) == cbPKTCHAN_CONFIGURATION) + && (buffer[offset + 6] == cbPKTTYPE_SYSREPRUNLEV) + && (buffer[offset + 8] == cbPKTDLEN_SYSINFO) + ) { + state->detected_version = ProtocolVersion::PROTOCOL_311; + state->done = true; + return; + } + + // If remaining bytes aren't big enough for a 4.0+ SYSINFO packet, break. + if (bytes_received - offset < cbPKTDLEN_SYSINFO * 4 + cbPKT_HEADER_SIZE) { + break; + } + + // Try 4.0 SYSREPRUNLEV-type SYSINFO packet. + if ( + (*reinterpret_cast(buffer + offset + 8) == cbPKTCHAN_CONFIGURATION) + && (buffer[offset + 10] == cbPKTTYPE_SYSREPRUNLEV) + && (*reinterpret_cast(buffer + offset + 11) == cbPKTDLEN_SYSINFO) + ) { + state->detected_version = ProtocolVersion::PROTOCOL_400; + state->done = true; + return; + } + + // Try 4.1+ packets. + const auto* pkt_header = reinterpret_cast(buffer + offset); + if ((pkt_header->chid == cbPKTCHAN_CONFIGURATION)) { + ++state->config_packets_seen; + if (pkt_header->type == cbPKTTYPE_SYSREPRUNLEV) { + // We cannot distinguish between 4.1 and 4.2 based solely on SYSREPRUNLEV + // Send a REQCONFIGALL and await the response. + state->send_config = true; + } + else if (pkt_header->type == cbPKTTYPE_PROCREP) { + ++state->procrep_seen; + + // If we received PROCREP then we can inspect it to distinguish between 4.1 and 4.2+. + const auto* pkt_procinfo = reinterpret_cast(buffer + offset); + + // Extract major and minor version from packed uint32_t + // Undo MAKELONG(cbVERSION_MINOR, cbVERSION_MAJOR) + // Where #define MAKELONG(a, b) ((a & 0xffff) | ((b & 0xffff) << 16)) + uint32_t version = pkt_procinfo->version; + uint16_t minor_version = version & 0xFFFF; // Lower 16 bits + uint16_t major_version = (version >> 16) & 0xFFFF; // Upper 16 bits + + // Distinguish between 4.1 and 4.2+ + if (major_version == 4) { + if (minor_version == 1) { + state->detected_version = ProtocolVersion::PROTOCOL_410; + } else if (minor_version >= 2) { + state->detected_version = ProtocolVersion::PROTOCOL_CURRENT; + } else { + // 4.0 or earlier - shouldn't happen if we got PROCREP + state->detected_version = ProtocolVersion::PROTOCOL_400; + } + } else if (major_version > 4) { + // Future protocol version - treat as current + state->detected_version = ProtocolVersion::PROTOCOL_CURRENT; + } else { + // Older major version + state->detected_version = ProtocolVersion::PROTOCOL_400; + } + + state->done = true; + return; + } + } + + // If we didn't match any specific protocol check above, we need to guess + // the packet size to properly advance the offset + size_t pkt_size = guessPacketSize(buffer + offset, bytes_received - offset); + if (pkt_size == 0) { + // Unable to determine packet size - skip to next datagram + break; + } + offset += pkt_size; + } + } +} +Result detectProtocol(const char* device_addr, uint16_t send_port, + const char* client_addr, uint16_t recv_port, + const uint32_t timeout_ms) { +#ifdef _WIN32 + // Initialize Winsock on Windows before using socket APIs + WSADATA wsaData; + int wsaInit = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (wsaInit != 0) { + return Result::error("WSAStartup failed"); + } +#endif + // Create temporary UDP socket for probing + SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock == INVALID_SOCKET) { +#ifdef _WIN32 + WSACleanup(); +#endif + return Result::error("Failed to create probe socket"); + } + + // Set socket options for broadcast + int opt_one = 1; + setsockopt(sock, SOL_SOCKET, SO_BROADCAST, reinterpret_cast(&opt_one), sizeof(opt_one)); + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&opt_one), sizeof(opt_one)); + + // Bind to client address/port + sockaddr_in client_sockaddr = {}; + client_sockaddr.sin_family = AF_INET; + client_sockaddr.sin_port = htons(recv_port); + + if (client_addr && strlen(client_addr) > 0 && strcmp(client_addr, "0.0.0.0") != 0) { + inet_pton(AF_INET, client_addr, &client_sockaddr.sin_addr); + } else { +#if defined(__linux__) + // Linux: To receive UDP broadcast packets, we must bind to the broadcast address. + // First check if we have a 192.168.137.x interface, if so use its broadcast address. + bool found_cerebus_interface = false; + struct ifaddrs *ifaddr, *ifa; + if (getifaddrs(&ifaddr) != -1) { + for (ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_INET) + continue; + struct sockaddr_in* addr = reinterpret_cast(ifa->ifa_addr); + uint32_t ip = ntohl(addr->sin_addr.s_addr); + // Check if this is a 192.168.137.x address (0xC0A889xx) + if ((ip & 0xFFFFFF00) == 0xC0A88900) { + // Use the broadcast address for this subnet + inet_pton(AF_INET, cbNET_UDP_ADDR_BCAST, &client_sockaddr.sin_addr); + found_cerebus_interface = true; + break; + } + } + freeifaddrs(ifaddr); + } + if (!found_cerebus_interface) { + client_sockaddr.sin_addr.s_addr = INADDR_ANY; + } +#else + client_sockaddr.sin_addr.s_addr = INADDR_ANY; +#endif + } + + if (bind(sock, reinterpret_cast(&client_sockaddr), sizeof(client_sockaddr)) == SOCKET_ERROR_VALUE) { + closesocket(sock); +#ifdef _WIN32 + WSACleanup(); +#endif + return Result::error("Failed to bind probe socket"); + } + + // Set socket timeout for receive (for the thread) + // Use a short timeout so the thread can check 'done' flag frequently +#ifdef _WIN32 + DWORD recv_timeout = 500; // 500ms + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&recv_timeout), sizeof(recv_timeout)); + // Increase socket buffers to handle bursts + int bufSize = 8 * 1024 * 1024; + setsockopt(sock, SOL_SOCKET, SO_RCVBUF, reinterpret_cast(&bufSize), sizeof(bufSize)); + setsockopt(sock, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&bufSize), sizeof(bufSize)); +#else + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 500000; // 500ms + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + // Increase socket buffers to handle bursts + int bufSize = 8 * 1024 * 1024; // 8 MB + setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufSize, sizeof(bufSize)); + setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufSize, sizeof(bufSize)); +#endif + + // Prepare device address for sending + sockaddr_in device_sockaddr = {}; + device_sockaddr.sin_family = AF_INET; + device_sockaddr.sin_port = htons(send_port); + inet_pton(AF_INET, device_addr, &device_sockaddr.sin_addr); + + // Create detection state and start receive thread BEFORE sending probes + DetectionState state; + state.sock = sock; + state.done = false; + state.send_config = false; + state.detected_version = ProtocolVersion::PROTOCOL_CURRENT; + + // Prepare probe packet in current protocol (cbPKTTYPE_SYSSETRUNLEV with cbRUNLEVEL_RUNNING) + // Note: We'd rather send REQCONFIGALL because the first packet in the response has explicit protocol version info, + // that packet is not replied to by devices in standby mode, and we can't take it out of standby mode without + // first establishing the protocol! + cbPKT_SYSINFO runlev = {}; + runlev.cbpkt_header.time = 1; + runlev.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + runlev.cbpkt_header.type = cbPKTTYPE_SYSSETRUNLEV; + runlev.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; + runlev.cbpkt_header.instrument = 0; + runlev.runlevel = cbRUNLEVEL_RUNNING; + runlev.resetque = 0; + runlev.runflags = 0; + + // cbPKT_GENERIC pktgeneric; + // pktgeneric.cbpkt_header.time = 1; + // pktgeneric.cbpkt_header.chid = 0x8000; + // pktgeneric.cbpkt_header.type = cbPKTTYPE_REQCONFIGALL; + // pktgeneric.cbpkt_header.dlen = 0; + // pktgeneric.cbpkt_header.instrument = 0; + + // Prepare runlev packet in protocol 4.0 format (64-bit timestamp, 8-bit type) + // See table in receiveThread for detailed differences. + // Protocol 4.0 layout: time(64b) chid(16b) type(8b) dlen(16b) instrument(8b) reserved(16b) payload... + constexpr int HEADER_SIZE_400 = 16; + constexpr int PAYLOAD_SIZE = 24; + uint8_t runlev_400[HEADER_SIZE_400 + PAYLOAD_SIZE] = {}; + *reinterpret_cast(&runlev_400[0]) = 1; // time (64-bit) at byte 0 + *reinterpret_cast(&runlev_400[8]) = cbPKTCHAN_CONFIGURATION; // chid (16-bit) at byte 8 + runlev_400[10] = cbPKTTYPE_SYSSETRUNLEV; // type (8-bit) at byte 10 + *reinterpret_cast(&runlev_400[11]) = PAYLOAD_SIZE / 4; // dlen (16-bit) at byte 11 + // Add SYSINFO payload (all zeros: sysfreq, spikelen, spikepre, resetque) + *reinterpret_cast(&runlev_400[36]) = cbRUNLEVEL_RUNNING; // runlevel (32-bit) at byte 36 + + // Prepare runlev packet in protocol 3.11 format (32-bit timestamp, 8-bit type) + // See table in receiveThread for detailed differences. + // Protocol 3.11 layout: time(32b) chid(16b) type(8b) dlen(8b) payload... + constexpr int HEADER_SIZE_311 = 8; + uint8_t runlev_311[HEADER_SIZE_311 + PAYLOAD_SIZE] = {}; + *reinterpret_cast(&runlev_311[0]) = 1; // time (32-bit) at byte 0 + *reinterpret_cast(&runlev_311[4]) = cbPKTCHAN_CONFIGURATION; // chid (16-bit) at byte 4 + runlev_311[6] = cbPKTTYPE_SYSSETRUNLEV; // type (8-bit) at byte 6 + runlev_311[7] = PAYLOAD_SIZE / 4; // dlen (8-bit) at byte 7 + // Add SYSINFO payload (all zeros: sysfreq, spikelen, spikepre, resetque) + *reinterpret_cast(&runlev_311[24]) = cbRUNLEVEL_RUNNING; // runlevel (32-bit) at byte 24 + + // Start the receive thread before sending the runlev packets. + std::thread recv_thread(receiveThread, &state); + + // Send the runlev packets + if (sendto(sock, reinterpret_cast(&runlev), sizeof(cbPKT_SYSINFO), 0, + reinterpret_cast(&device_sockaddr), sizeof(device_sockaddr)) == SOCKET_ERROR_VALUE) { + state.done = true; + recv_thread.join(); + closesocket(sock); + return Result::error("Failed to send 4.1 runlev packet"); + } + + std::this_thread::sleep_for(std::chrono::microseconds(50)); + if (sendto(sock, reinterpret_cast(runlev_400), sizeof(runlev_400), 0, + reinterpret_cast(&device_sockaddr), sizeof(device_sockaddr)) == SOCKET_ERROR_VALUE) { + state.done = true; + recv_thread.join(); + closesocket(sock); + return Result::error("Failed to send 4.0 runlev packet"); + } + + std::this_thread::sleep_for(std::chrono::microseconds(50)); + if (sendto(sock, reinterpret_cast(runlev_311), sizeof(runlev_311), 0, + reinterpret_cast(&device_sockaddr), sizeof(device_sockaddr)) == SOCKET_ERROR_VALUE) { + state.done = true; + recv_thread.join(); + closesocket(sock); + return Result::error("Failed to send 3.11 runlev packet"); + } + + // Wait for receive thread to detect protocol or timeout + const auto start_time = std::chrono::steady_clock::now(); + bool timed_out = false; + while (!state.done) { + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time).count(); + + if (elapsed >= timeout_ms) { + // Timeout - stop thread and return default (current protocol) + timed_out = true; + break; + } + if (state.send_config.load()) { + // Send REQCONFIGALL packet to elicit PROCINFO packet. + cbPKT_GENERIC pkt; + pkt.cbpkt_header.time = 1; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_REQCONFIGALL; + pkt.cbpkt_header.dlen = 0; + pkt.cbpkt_header.instrument = 0; + + sendto(sock, reinterpret_cast(&pkt), cbPKT_HEADER_SIZE, 0, + reinterpret_cast(&device_sockaddr), sizeof(device_sockaddr)); + + state.send_config = false; + } + + // Sleep briefly to avoid busy-waiting + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // Stop receive thread + state.done = true; + recv_thread.join(); + closesocket(sock); +#ifdef _WIN32 + WSACleanup(); +#endif + if (timed_out) { + // Provide debug info on what we observed + char msg[256]; + snprintf(msg, sizeof(msg), "Timed out: packets=%llu, config=%llu, procrep=%llu, bytes=%llu", + (unsigned long long)state.packets_seen.load(), + (unsigned long long)state.config_packets_seen.load(), + (unsigned long long)state.procrep_seen.load(), + (unsigned long long)state.bytes_received.load()); + return Result::error(msg); + } + return Result::ok(state.detected_version.load()); +} + +} // namespace cbdev diff --git a/src/cbdev/src/protocol_detector.h b/src/cbdev/src/protocol_detector.h new file mode 100644 index 00000000..ef4f088b --- /dev/null +++ b/src/cbdev/src/protocol_detector.h @@ -0,0 +1,44 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file protocol_detector.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Protocol version detection for device communication +/// +/// Detects the protocol version used by a device by sending a probe packet and analyzing +/// the response format (e.g., 32-bit vs 64-bit timestamps). +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBDEV_PROTOCOL_DETECTOR_H +#define CBDEV_PROTOCOL_DETECTOR_H + +#include +#include +#include + +namespace cbdev { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Detect protocol version by probing the device +/// +/// Sends a probe packet to the device and analyzes the response to determine protocol version. +/// This is a blocking operation that may take up to a few hundred milliseconds. +/// +/// @param device_addr Device IP address +/// @param send_port Device receives config packets on this port +/// @param client_addr Client IP address (for binding) +/// @param recv_port Client UDP port (for binding) +/// @param timeout_ms Timeout in milliseconds (default: 500ms) +/// @return Detected protocol version, or error +/// +/// @note Creates temporary socket for probing, then closes it +/// @note Returns UNKNOWN if device doesn't respond within timeout +/// +Result detectProtocol(const char* device_addr, uint16_t send_port, + const char* client_addr, uint16_t recv_port, + uint32_t timeout_ms = 500); + +} // namespace cbdev + +#endif // CBDEV_PROTOCOL_DETECTOR_H diff --git a/src/cbhwlib/CkiVersion.rc b/src/cbhwlib/CkiVersion.rc deleted file mode 100755 index 3be92ccc..00000000 --- a/src/cbhwlib/CkiVersion.rc +++ /dev/null @@ -1,129 +0,0 @@ -// =STS=> CkiVersion.rc[2408].aa25 open SMID:27 -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" -#include "..\CentralCommon\BmiVersion.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "afxres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (U.S.) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -#ifdef _WIN32 -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) -#endif //_WIN32 - -///////////////////////////////////////////////////////////////////////////// -// -// About Dialog -// - -IDD_ABOUT DIALOG 0, 0, 207, 72 -STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "About Cerebus Suite" -FONT 8, "MS Sans Serif" -BEGIN - ICON IDR_MAINFRAME,IDC_STATIC,5,10,20,20,SS_SUNKEN - CTEXT "Cerebus Suite",IDC_STATIC_APP_VERSION,30,5,150,10,SS_NOPREFIX | SS_CENTERIMAGE - PUSHBUTTON "OK",IDOK,180,10,20,20 - CTEXT "built with Hardware Library",IDC_STATIC_LIB_VERSION,30,15,150,10,SS_NOPREFIX | SS_CENTERIMAGE - CTEXT BMI_COPYRIGHT_STR,IDC_STATIC,30,30,160,10,SS_NOPREFIX | SS_CENTERIMAGE - CTEXT "NSP Firmware",IDC_STATIC_NSP_VERSION,30,45,150,10,SS_NOPREFIX | SS_CENTERIMAGE - CTEXT "",IDC_STATIC_NSP_ID,30,60,150,10,SS_NOPREFIX | SS_CENTERIMAGE -END - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -VS_VERSION_INFO VERSIONINFO - FILEVERSION BMI_VERSION - PRODUCTVERSION BMI_VERSION - FILEFLAGSMASK 0x3fL -#ifndef DEBUG -#if BMI_VERSION_BETA - FILEFLAGS 0x2L -#else - FILEFLAGS 0x0L -#endif -#else - FILEFLAGS 0x1L -#endif - FILEOS 0x40004L -#ifdef _WINDLL - FILETYPE 0x2L -#else - FILETYPE 0x1L -#endif - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904b0" - BEGIN - VALUE "CompanyName", "Blackrock Microsystems" - VALUE "FileVersion", BMI_VERSION_STR - VALUE "LegalCopyright", "Copyright (C) 2008-2011 Blackrock Microsystems" - VALUE "ProductVersion", BMI_VERSION_STR - VALUE "FileDescription", VERSION_DESCRIPTION - VALUE "InternalName", VERSION_DESCRIPTION - VALUE "OriginalFilename", VERSION_FILENAME - VALUE "ProductName", VERSION_DESCRIPTION - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1200 - END -END - - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" -END - -2 TEXTINCLUDE -BEGIN - "#include ""afxres.h""\r\n" -END - -#endif // APSTUDIO_INVOKED - -#endif // English (U.S.) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - diff --git a/src/cbhwlib/DataVector.h b/src/cbhwlib/DataVector.h deleted file mode 100644 index 6d787231..00000000 --- a/src/cbhwlib/DataVector.h +++ /dev/null @@ -1,731 +0,0 @@ -/* =STS=> DataVector.h[5034].aa00 submit SMID:1 */ -///////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003-2008 Cyberkinetics, Inc -// (c) Copyright 2008-2012 Blackrock Microsystems -// -// $Workfile: DataVector.h $ -// $Archive: $ -// $Revision: 1 $ -// $Date: 11/6/08 12:30p $ -// $Author: jscott $ -// -// $NoKeywords: $ -// -// This is a highly templated Vector class. It can make Vectors of -// arbitrary size and arbitrary type. However, some of the functions -// only have implementations up to a small number, such as -// constructors which initialize the Vector. -// This Vector class is intended for use with numeric types, as most -// of the operations are mathematical. -////////////////////////////////////////////////////////////////////// - -#ifndef DATA_VECTOR_H_INCLUDED -#define DATA_VECTOR_H_INCLUDED - -#if _MSC_VER > 1000 -#pragma once -#endif // _MSC_VER > 1000 - -#include -#include - -/////////////////////////////////////////////////////////// -// This is the main structure. Gives all operator overloads -template -struct Vector -{ - T data[N]; - - Vector(); - Vector(const T vec[N]); - Vector(T t); - Vector(T t0, T t1); - Vector(T t0, T t1, T t2); - Vector(T t0, T t1, T t2, T t3); - - void Set(const T vec[N]); - void Set(T t); - void Set(T t0, T t1); - void Set(T t0, T t1, T t2); - void Set(T t0, T t1, T t2, T t3); - - T& operator [] (int i); - T operator [] (int i)const; - - template - operator Vector()const; - - Vector& operator = (const T vec[N]); - - Vector operator - ()const; - - Vector operator + (Vector vec)const; - Vector operator - (Vector vec)const; - Vector operator * (Vector vec)const; - Vector operator / (Vector vec)const; - - Vector operator += (Vector vec); - Vector operator -= (Vector vec); - Vector operator *= (Vector vec); - Vector operator /= (Vector vec); - - template - Vector operator + (U u)const; - template - Vector operator - (U u)const; - template - Vector operator * (U u)const; - template - Vector operator / (U u)const; - - template - Vector operator += (U u); - template - Vector operator -= (U u); - template - Vector operator *= (U u); - template - Vector operator /= (U u); - - T LengthSqrd()const; - T Length()const; - void SetLength(T len); - void Normalize(); - Vector Norm()const; - - bool IsSimilar(const Vector &v, T maxDifference)const; - -}; - -/////////////////////////////////////////////////////////// -// Implementation of constructors - -template -Vector::Vector(){} - -template -Vector::Vector(const T vec[N]){Set(vec);} - -template -Vector::Vector(T t){Set(t);} - -template -Vector::Vector(T t0, T t1){Set(t0,t1);} - -template -Vector::Vector(T t0, T t1, T t2){Set(t0,t1,t2);} - -template -Vector::Vector(T t0, T t1, T t2, T t3){Set(t0,t1,t2,t3);} - -/////////////////////////////////////////////////////////// -// Implementation of setters - -template -void Vector::Set(T t) -{ - for(int i = 0; i < N; i++) - data[i] = t; -} - -template -void Vector::Set(const T vec[N]) -{ - for(int i = 0; i < N; i++) - data[i] = vec[i]; -} - -template -void Vector::Set(T t0, T t1) -{ - data[0] = t0; - data[1] = t1; -} - -template -void Vector::Set(T t0, T t1, T t2) -{ - data[0] = t0; - data[1] = t1; - data[2] = t2; -} - -template -void Vector::Set(T t0, T t1, T t2, T t3) -{ - data[0] = t0; - data[1] = t1; - data[2] = t2; - data[3] = t3; -} - -/////////////////////////////////////////////////////////// -// Implementation of non-arithmetic member operator overloads - -template -inline T& Vector::operator [] (int i) -{ - return data[i]; -} - -template -inline T Vector::operator [] (int i)const -{ - return data[i]; -} - -templatetemplate -inline Vector::operator Vector()const -{ - Vector v; - for(int i = 0; i < N; ++i) - { - v[i] = static_cast(data[i]); - } - - return v; -} - -template -inline Vector& Vector::operator = (const T vec[N]) -{ - for(int i = 0; i < N; i++) - data[i] = vec[i]; - return *this; -} - -#include -template -std::ostream& operator<<(std::ostream &s, const Vector &v) -{ - s << "["; - for(int i = 0; i < N; i++) - { - s << " " << v.data[i] << " "; - } - s << "]" << std::endl; - return s; -} - -/////////////////////////////////////////////////////////// -// Implementation of arithmetic operator overloads which -// take a Vector as a parameter - -template -inline Vector Vector::operator - ()const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = -1*data[i]; - return retVal; - -} - -template -inline Vector Vector::operator + (Vector vec)const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = data[i] + vec[i]; - return retVal; -} - -template -inline Vector Vector::operator - (Vector vec)const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = data[i] - vec[i]; - return retVal; -} - -template -inline Vector Vector::operator * (Vector vec)const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = data[i] * vec[i]; - return retVal; -} - -template -inline Vector Vector::operator / (Vector vec)const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = data[i] / vec[i]; - return retVal; -} - -template -inline Vector Vector::operator += (Vector vec) -{ - for(int i = 0; i < N; i++) - data[i] += vec[i]; - return *this; -} - -template -inline Vector Vector::operator -= (Vector vec) -{ - for(int i = 0; i < N; i++) - data[i] -= vec[i]; - return *this; -} - -template -inline Vector Vector::operator *= (Vector vec) -{ - for(int i = 0; i < N; i++) - data[i] *= vec[i]; - return *this; -} - -template -inline Vector Vector::operator /= (Vector vec) -{ - for(int i = 0; i < N; i++) - data[i] /= vec[i]; - return *this; -} - -/////////////////////////////////////////////////////////// -// Implementation of arithmetic operator overloads which -// take a scalar as a parameter - -template -template -Vector Vector::operator + (U u)const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = data[i] + u; - return retVal; -} - -template -template -Vector Vector::operator - (U u)const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = data[i] - u; - return retVal; -} - -template -template -Vector Vector::operator * (U u)const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = static_cast(data[i] * u); - return retVal; -} - -template -template -Vector Vector::operator / (U u)const -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = data[i] / u; - return retVal; -} - -template -template -Vector Vector::operator += (U u) -{ - for(int i = 0; i < N; i++) - data[i] += u; - return *this; -} - -template -template -Vector Vector::operator -= (U u) -{ - for(int i = 0; i < N; i++) - data[i] -= u; - return *this; -} - -template -template -Vector Vector::operator *= (U u) -{ - for(int i = 0; i < N; i++) - data[i] = static_cast(data[i]*u); - return *this; -} - -template -template -Vector Vector::operator /= (U u) -{ - for(int i = 0; i < N; i++) - data[i] /= u; - return *this; -} - -/////////////////////////////////////////////////////////// -// Implementation of global arithmetic operator overloads -// (for when the scalar precedes the Vector) - -template -inline Vector operator+(T t,Vector v) -{ - return v+t; -} - -template -inline Vector operator-(T t,Vector v) -{ - return v-t; -} - -template -inline Vector operator*(T t,Vector v) -{ - return v*t; -} - -template -inline Vector operator/(T t,Vector v) -{ - return v/t; -} - -/////////////////////////////////////////////////////////// -// Member Vector function implementations - -template -inline T Vector::LengthSqrd()const -{ - T lengthSqrd = 0; - for(int i = 0; i < N; i++) - lengthSqrd += data[i]*data[i]; - return lengthSqrd; -} - -template -inline T Vector::Length()const -{ - return sqrt(LengthSqrd()); -} - -template -inline void Vector::SetLength(T len) -{ - len /= Length(); - for(int i = 0; i < N; i++) - data[i] *= len; -} - -template -inline void Vector::Normalize() -{ - T len = Length(); - for(int i = 0; i < N; i++) - data[i] /= len; -} - -template -inline Vector Vector::Norm()const -{ - Vector v = *this; - v.Normalize(); - return v; -} - - -template -inline bool Vector::IsSimilar(const Vector &v, T maxDifference)const -{ - for(int i = 0; i < N; i++) - if(abs(data[i] - v[i]) > maxDifference) - return false; - return true; -} -/////////////////////////////////////////////////////////// -// Global Vector function implementations - -template -inline T DotProduct(Vector v1, Vector v2) -{ - T DotProduct = 0; - for(int i = 0; i < N; i++) - DotProduct += v1[i]*v2[i]; - return DotProduct; -} - -template -inline Vector CrossProduct(const Vector &v1, const Vector &v2) -{ - Vector res; - res[0] = (v1[1] * v2[2] - v1[2] * v2[1]); - res[1] = (v1[2] * v2[0] - v1[0] * v2[2]); - res[2] = (v1[0] * v2[1] - v1[1] * v2[0]); - return res; -} - -template -inline Vector MaxVector(const Vector &v1, const Vector &v2) -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = max(v1[i],v2[i]); - return retVal; -} - -template -inline Vector MinVector(const Vector &v1, const Vector &v2) -{ - Vector retVal; - for(int i = 0; i < N; i++) - retVal[i] = min(v1[i],v2[i]); - return retVal; -} - -// sets the Vector to a Vector which is orthogonal to the original -template -inline void Orthogonal(Vector &v) -{ - if(v[1] == 0 && v[2] == 0)// we're dealing with the x axis - {// just set it to the y-axis - v[0] = 0; - v[1] = 1; - } - else - { - v[0] = 0; - T temp = v[1]; - v[1] = v[2]; - v[2] = -temp; - } -} - -/////////////////////////////////////////////////////////// -// Rotation fuctions for Vectors in 3-space -// These functions rotate a Vector around an axis by the -// given angle in the clockwise direction (when looking -// along the positive axis). All angles are in radians -template -inline void RotateXAxis(double cosTheta, double sinTheta, Vector &v) -{ - T temp = static_cast(cosTheta*v[1]-sinTheta*v[2]); - v[2] = static_cast(sinTheta*v[1]+cosTheta*v[2]); - v[1] = temp; -} - -template -inline void RotateXAxis(double theta, Vector &v) -{ - RotateXAxis(cos(theta),sin(theta),v); -} - -template -inline void RotateYAxis(double cosTheta, double sinTheta, Vector &v) -{ - T temp = static_cast(cosTheta*v[0]+sinTheta*v[2]); - v[2] = static_cast(-sinTheta*v[0]+cosTheta*v[2]); - v[0] = temp; -} - -template -inline void RotateYAxis(double theta, Vector &v) -{ - RotateYAxis(cos(theta),sin(theta),v); -} - -template -inline void RotateZAxis(double cosTheta, double sinTheta, Vector &v) -{ - T temp = static_cast(cosTheta*v[0]-sinTheta*v[1]); - v[1] = static_cast(sinTheta*v[0]+cosTheta*v[1]); - v[0] = temp; -} - -template -inline void RotateZAxis(double theta, Vector &v) -{ - RotateZAxis(cos(theta),sin(theta),v); -} - -template -inline void RotateAroundAxis(double cosTheta, double sinTheta, Vector &v, const Vector &axis) -{ - Vector temp; - temp[0] = static_cast( - (axis[0]*axis[0]*(1 - cosTheta) + cosTheta)*v[0] + - (axis[0]*axis[1]*(1 - cosTheta) - axis[2]*sinTheta)*v[1] + - (axis[0]*axis[2]*(1 - cosTheta) + axis[1]*sinTheta)*v[2]); - - temp[1] = static_cast( - (axis[0]*axis[1]*(1 - cosTheta) + axis[2]*sinTheta)*v[0] + - (axis[1]*axis[1]*(1 - cosTheta) + cosTheta)*v[1] + - (axis[1]*axis[2]*(1 - cosTheta) - axis[0]*sinTheta)*v[2]); - - temp[2] = static_cast( - (axis[0]*axis[2]*(1 - cosTheta) - axis[1]*sinTheta)*v[0] + - (axis[1]*axis[2]*(1 - cosTheta) + axis[0]*sinTheta)*v[1] + - (axis[2]*axis[2]*(1 - cosTheta) + cosTheta)*v[2]); - - v = temp; -} - -template -inline void RotateAroundAxis(double theta, Vector &v, const Vector &axis) -{ - RotateAroundAxis(cos(theta),sin(theta),v,axis); -} - - -// Author & Date: Jason Scott 2008 -// Purpose: find the angle of rotation from one Vector to another, given the axis of rotation -// Input: v1 - original Vector -// v2 - Vector after rotation -// axis - the axis of rotation -// Returns: the angle of rotation, in radians, from v1 to v2, -// in a clockwise direction (when looking along the axis) -// This value will always be in the range [-PI, PI] -// Notes: The angle of rotation from one Vector to another is NOT the same as the angle between -// them. The angle between two Vectors is necessarily positive (in range [0,PI)), while -// the angle of rotation can account for negative rotations. -// This function finds the angle between v1 and v2, theta, using the dot product. Then -// the axis is used to perform test rotations using +theta and -theta. -// The closest rotation indicates the proper angle of rotation. -template -inline double AngleOfRotation(const Vector &v1, const Vector &v2, const Vector &axis) -{ - Vector v1Norm = v1.Norm(); - Vector v2Norm = v2.Norm(); - Vector rotTest = v1Norm; - // calculate cos(theta) - double theta = DotProduct(v1Norm,v2Norm); - // because of numerical error, ensure cos(theta) is in [-1,1] - theta = std::min(1.0, theta); - theta = std::max(theta, -1.0); - theta = acos(theta); - - // perform test rotation using +theta - RotateAroundAxis(theta,rotTest,axis); - double error = (rotTest - v2Norm).LengthSqrd(); - rotTest = v1Norm; - // perform test rotation using -theta - RotateAroundAxis(-theta,rotTest,axis); - - // use whichever rotation comes closer to v2 (has smallest error) - if(error < (rotTest-v2Norm).LengthSqrd()) - return theta; - else return -theta; -} - -// Author & Date: Jason Scott August 7 2009 -// Purpose: find the angles of rotation to move from the standard coordinate frame to -// the input coordinate frame. -// In other words, applying the resulting rotations -// (RotateXAxis(thetaX); RotateYAxis(thetaY); RotateZAxis(thetaZ); (IN THAT ORDER!)) -// to vectors along the coordinate axes will yield vectors along the new coordinate -// frame. - -// Input: -// x - x-axis of the input coordinate frame -// y - y-axis of the input coordinate frame -// z - z-axis of the input coordinate frame -// (x,y,z) should form a right-hand coordinate frame (ie. CrossProduct(|x|,|y|) = |z|) -// Output: -// thetaX - x rotation angle. Must be applied first -// thetaY - y rotation angle. Must be applied second -// thetaZ - z rotation angle. Must be applied third -template -void GetRotationAngles(Vector x, Vector y, Vector z, float &thetaX, float &thetaY, float &thetaZ) -{ - Vector xAxis(1,0,0); - Vector yAxis(0,1,0); - Vector zAxis(0,0,1); - - // find thetaX - // Project the z vector onto the yz plane and find the rotation needed to align it with the z-axis - Vector yzProj = z - xAxis*DotProduct(z, xAxis); - thetaX = static_cast(AngleOfRotation(yzProj, zAxis, xAxis)); - - // Rotate the remaining vectors accordingly - RotateXAxis(thetaX, x); - RotateXAxis(thetaX, z); - - // find thetaY - // Project the z vector onto the xz plane and find the rotation needed to align it with the z-axis - Vector xzProj = z - yAxis*DotProduct(z, yAxis); - thetaY = static_cast(AngleOfRotation(xzProj, zAxis, yAxis)); - - // Rotate the remaining vectors accordingly - RotateYAxis(thetaY, x); - - // find thetaZ - // Project the x vector onto the xy plane and find the rotation needed to align it with the x-axis - Vector xyProj = x - zAxis*DotProduct(x, zAxis); - thetaZ = static_cast(AngleOfRotation(xyProj, xAxis, zAxis)); - - thetaX = -thetaX; - thetaY = -thetaY; - thetaZ = -thetaZ; -} - -template -inline void GetRotationAngles(const Vector &x, const Vector &y, const Vector &z, float afTheta[3]) -{ - GetRotationAngles(x, y, z, afTheta[0], afTheta[1], afTheta[2]); -} - -// Author & Date: Jason Scott August 7 2009 -// Purpose: Apply three angles of rotation to a vector. -// Works in conjunction with GetRotationAngles -// Input: -// v - the vector to be rotated -// thetaX - x rotation angle. Will be applied first -// thetaY - y rotation angle. Will be applied second -// thetaZ - z rotation angle. Will be applied third -template -inline void ApplyRotationAngles(Vector &v, float thetaX, float thetaY, float thetaZ) -{ - RotateXAxis(thetaX, v); - RotateYAxis(thetaY, v); - RotateZAxis(thetaZ, v); -} - -template -inline void ApplyRotationAngles(Vector &v, const float afTheta[3]) -{ - ApplyRotationAngles(v, afTheta[0], afTheta[1], afTheta[2]); -} - - -/////////////////////////////////////////////////////////// -// Some typedefs for common type low degree Vectors -typedef Vector Vector4d; -typedef Vector Vector3d; -typedef Vector Vector2d; -typedef Vector Vector4f; -typedef Vector Vector3f; -typedef Vector Vector2f; -typedef Vector Vector4i; -typedef Vector Vector3i; -typedef Vector Vector2i; - -// an NxN matrix -template -struct matrix -{ - T data[N*N]; -}; -// Data point from the NSP. Combination of unit and position -struct data_point -{ - unsigned int unit; - Vector3f point; -}; - -#endif // include guard diff --git a/src/cbhwlib/InstNetwork.cpp b/src/cbhwlib/InstNetwork.cpp deleted file mode 100755 index d519d2ea..00000000 --- a/src/cbhwlib/InstNetwork.cpp +++ /dev/null @@ -1,907 +0,0 @@ -// =STS=> InstNetwork.cpp[2732].aa08 open SMID:8 -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2010 - 2020 Blackrock Microsystems -// -// $Workfile: InstNetwork.cpp $ -// $Archive: /common/InstNetwork.cpp $ -// $Revision: 1 $ -// $Date: 3/15/10 12:21a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// -// -// PURPOSE: -// -// Common xPlatform instrument network -// -#include "../cbproto/StdAfx.h" -#include // Use C++ default min and max implementation. -#include // For std::thread -#include // For std::chrono timing -#include "InstNetwork.h" -#ifndef WIN32 - #include -#endif - -const uint32_t InstNetwork::MAX_NUM_OF_PACKETS_TO_PROCESS_PER_PASS = 5000; // This is not exported correctly unless redefined here. - -// Author & Date: Ehsan Azar 15 March 2010 -// Purpose: Constructor for instrument networking thread -InstNetwork::InstNetwork(STARTUP_OPTIONS startupOption) : - m_enLOC(LOC_LOW), - m_nStartupOptionsFlags(startupOption), - m_timerTicks(0), - m_bDone(false), - m_nRecentPacketCount(0), - m_dataCounter(0), - m_nLastNumberOfPacketsReceived(0), - m_runlevel(cbRUNLEVEL_SHUTDOWN), - m_bInitChanCount(false), - m_bStandAlone(true), - m_instInfo(0), - m_nInstance(0), - m_nIdx(0), - m_nInPort(NSP_IN_PORT), - m_nOutPort(NSP_OUT_PORT), - m_bBroadcast(false), - m_bDontRoute(true), - m_bNonBlocking(true), - m_nRecBufSize(NSP_REC_BUF_SIZE), - m_strInIP(NSP_IN_ADDRESS), - m_strOutIP(NSP_OUT_ADDRESS), - m_nRange(0) -{ - - // Object initialization complete -} - -// Author & Date: Ehsan Azar 15 March 2010 -// Purpose: Open the instrument network -// Inputs: -// listener - the instrument listener of packets and events -void InstNetwork::Open(Listener * listener) -{ - if (listener) - m_listener.push_back(listener); -} - -// Author & Date: Ehsan Azar 23 Sept 2010 -// Purpose: Shut down the instrument network -void InstNetwork::ShutDown() -{ - if (m_bStandAlone) - m_icInstrument.Shutdown(); - else - cbSetSystemRunLevel(cbRUNLEVEL_SHUTDOWN, 0, 0, cbNSP1 - 1, m_nInstance); -} - -// Author & Date: Ehsan Azar 23 Sept 2010 -// Purpose: Standby the instrument network -void InstNetwork::StandBy() -{ - if (m_bStandAlone) - m_icInstrument.Standby(); - else - cbSetSystemRunLevel(cbRUNLEVEL_HARDRESET, 0, 0, cbNSP1 - 1, m_nInstance); -} - -// Author & Date: Ehsan Azar 15 March 2010 -// Purpose: Close the instrument network -void InstNetwork::Close() -{ - // Signal thread to finish - m_bDone = true; - // Wait for thread to finish - if (m_thread && m_thread->joinable()) - m_thread->join(); -} - -// Author & Date: Ehsan Azar 15 March 2010 -// Purpose: Start the network thread -void InstNetwork::Start() -{ - m_thread = std::make_unique(&InstNetwork::run, this); - - // Set thread priority (platform-specific) -#ifdef WIN32 -#if defined(__MINGW32__) || defined(__MINGW64__) - // For MinGW with pthreads, native_handle() returns a pthread_t, but pthread_getw32threadhandle_np is not available. - // Disabling thread priority setting for MinGW for now. -#else - // For MSVC, native_handle() returns a HANDLE. - SetThreadPriority(m_thread->native_handle(), THREAD_PRIORITY_HIGHEST); -#endif -#else - // On Unix/Linux, setting thread priority may require privileges - // For now, we'll skip priority setting on non-Windows platforms - // If needed, you can use pthread_setschedparam with appropriate permissions -#endif -} - -// Author & Date: Ehsan Azar 14 Feb 2012 -// Purpose: Specific network commands to handle as Qt slots -// Inputs: -// cmd - network command to perform -// code - additional argument for the command -void InstNetwork::OnNetCommand(NetCommandType cmd, unsigned int /*code*/) -{ - switch (cmd) - { - case NET_COMMAND_NONE: - // Do nothing - break; - case NET_COMMAND_OPEN: - Start(); - break; - case NET_COMMAND_CLOSE: - Close(); - break; - case NET_COMMAND_STANDBY: - StandBy(); - break; - case NET_COMMAND_SHUTDOWN: - ShutDown(); - break; - } -} - - -void InstNetwork::SetNumChans() -{ - uint32_t nCaps; - uint32_t nAoutCaps; - uint32_t nDinpCaps; - uint32_t nNumFEChans = 0; - uint32_t nNumAnainChans = 0; - uint32_t nNumAoutChans = 0; - uint32_t nNumAudioChans = 0; - uint32_t nNumDiginChans = 0; - uint32_t nNumSerialChans = 0; - uint32_t nNumDigoutChans = 0; - - if (!m_bInitChanCount) - { - cbPROCINFO isProcInfo = {}; - ::cbGetProcInfo(cbNSP1, &isProcInfo); - if (0 != isProcInfo.chancount) - m_bInitChanCount = true; - for (uint32_t nChan = 1; nChan <= isProcInfo.chancount; ++nChan) - { - if (cbRESULT_OK == (::cbGetChanCaps(nChan, &nCaps) + - ::cbGetAoutCaps(nChan, &nAoutCaps, nullptr, nullptr) + - ::cbGetDinpCaps(nChan, &nDinpCaps))) - { - if ((cbCHAN_EXISTS | cbCHAN_CONNECTED) == (nCaps & (cbCHAN_EXISTS | cbCHAN_CONNECTED))) - { - if ((cbCHAN_AINP | cbCHAN_ISOLATED) == (nCaps & (cbCHAN_AINP | cbCHAN_ISOLATED))) - nNumFEChans++; - if ((cbCHAN_AINP) == (nCaps & (cbCHAN_AINP | cbCHAN_ISOLATED))) - nNumAnainChans++; - if (cbCHAN_AOUT == (nCaps & cbCHAN_AOUT) && (cbAOUT_AUDIO != (nAoutCaps & cbAOUT_AUDIO))) - nNumAoutChans++; - if (cbCHAN_AOUT == (nCaps & cbCHAN_AOUT) && (cbAOUT_AUDIO == (nAoutCaps & cbAOUT_AUDIO))) - nNumAudioChans++; - if (cbCHAN_DINP == (nCaps & cbCHAN_DINP) && (nDinpCaps & cbDINP_MASK)) - nNumDiginChans++; - if (cbCHAN_DINP == (nCaps & cbCHAN_DINP) && (nDinpCaps & cbDINP_SERIALMASK)) - nNumSerialChans++; - else if (cbCHAN_DOUT == (nCaps & cbCHAN_DOUT)) - nNumDigoutChans++; - } - } - } - cb_pc_status_buffer_ptr[0]->cbSetNumFEChans(nNumFEChans); - cb_pc_status_buffer_ptr[0]->cbSetNumAnainChans(nNumAnainChans); - cb_pc_status_buffer_ptr[0]->cbSetNumAnalogChans(nNumFEChans + nNumAnainChans); - cb_pc_status_buffer_ptr[0]->cbSetNumAoutChans(nNumAoutChans); - cb_pc_status_buffer_ptr[0]->cbSetNumAudioChans(nNumAudioChans); - cb_pc_status_buffer_ptr[0]->cbSetNumAnalogoutChans(nNumAoutChans + nNumAudioChans); - cb_pc_status_buffer_ptr[0]->cbSetNumDiginChans(nNumDiginChans); - cb_pc_status_buffer_ptr[0]->cbSetNumSerialChans(nNumSerialChans); - cb_pc_status_buffer_ptr[0]->cbSetNumDigoutChans(nNumDigoutChans); - cb_pc_status_buffer_ptr[0]->cbSetNumTotalChans(nNumFEChans + nNumAnainChans + nNumAoutChans + nNumAudioChans + nNumDiginChans + nNumSerialChans + nNumDigoutChans); - } -} - - -// Author & Date: Ehsan Azar 24 June 2010 -// Purpose: Some packets coming from the stand-alone instrument network need -// to be processed for any listener application. -// then ask the network listener for more process. -// Inputs: -// pPkt - pointer to the packet -void InstNetwork::ProcessIncomingPacket(const cbPKT_GENERIC * const pPkt) -{ -// if (!(pPkt->cbpkt_header.chid & cbPKTCHAN_CONFIGURATION) || (pPkt->cbpkt_header.type != cbPKTTYPE_SYSHEARTBEAT)) -// TRACE("ProcessIncomingPacket of type 0x%2X\n", pPkt->cbpkt_header.type); - // -------- Process some incoming packet here ----------- - // check for configuration class packets - if (pPkt->cbpkt_header.chid & 0x8000) - { - // Check for configuration packets - if (pPkt->cbpkt_header.chid == 0x8000) - { - if ((pPkt->cbpkt_header.type & 0xF0) == cbPKTTYPE_CHANREP) - { - if (m_bStandAlone) - { - const auto * pNew = reinterpret_cast(pPkt); - uint32_t chan = pNew->chan; - if (chan > 0 && chan <= cbMAXCHANS) - { - memcpy(&(cb_cfg_buffer_ptr[m_nIdx]->chaninfo[chan - 1]), pPkt, sizeof(cbPKT_CHANINFO)); - if (pPkt->cbpkt_header.type == cbPKTTYPE_CHANREP) - { - // Invalidate the cache - if (chan <= cb_pc_status_buffer_ptr[0]->cbGetNumAnalogChans()) - cb_spk_buffer_ptr[m_nIdx]->cache[chan - 1].valid = 0; - } - } - } - } - else if ((pPkt->cbpkt_header.type & 0xF0) == cbPKTTYPE_SYSREP) - { - const auto * pNew = reinterpret_cast(pPkt); - if (m_bStandAlone) - { - cbPKT_SYSINFO & rOld = cb_cfg_buffer_ptr[m_nIdx]->sysinfo; - // replace our copy with this one - rOld = *pNew; - SetNumChans(); - } - // Rely on the fact that sysrep must be the last config packet sent via NSP6.04 and upwards - if (pPkt->cbpkt_header.type == cbPKTTYPE_SYSREP) - { - // Any change to the instrument will be reported here, including initial connection as stand-alone - uint32_t instInfo; - cbGetInstInfo(pPkt->cbpkt_header.instrument + 1, &instInfo, m_nInstance); - // If instrument connection state has changed - if (instInfo != m_instInfo) - { - m_instInfo = instInfo; - InstNetworkEvent(NET_EVENT_INSTINFO, instInfo); - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_SYSREPRUNLEV) - { - if (pNew->runlevel == cbRUNLEVEL_HARDRESET) - { - // If any app did a hard reset which is not the initial reset - // Application should decide what to do on reset - if (!m_bStandAlone || (m_bStandAlone && pPkt->cbpkt_header.time > 500)) - InstNetworkEvent(NET_EVENT_RESET); - } - else if (pNew->runlevel == cbRUNLEVEL_RUNNING) - { - if (pNew->runflags & cbRUNFLAGS_LOCK) - { - InstNetworkEvent(NET_EVENT_LOCKEDRESET); - } - } - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_GROUPREP) - { - if (m_bStandAlone) - memcpy(&(cb_cfg_buffer_ptr[m_nIdx]->groupinfo[0][((cbPKT_GROUPINFO*)pPkt)->group-1]), pPkt, sizeof(cbPKT_GROUPINFO)); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_FILTREP) - { - if (m_bStandAlone) - memcpy(&(cb_cfg_buffer_ptr[m_nIdx]->filtinfo[0][((cbPKT_FILTINFO*)pPkt)->filt-1]), pPkt, sizeof(cbPKT_FILTINFO)); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_PROCREP) - { - if (m_bStandAlone) - memcpy(&(cb_cfg_buffer_ptr[m_nIdx]->procinfo[0]), pPkt, sizeof(cbPKT_PROCINFO)); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_BANKREP) - { - if (m_bStandAlone && (((cbPKT_BANKINFO*)pPkt)->bank < cbMAXBANKS)) - memcpy(&(cb_cfg_buffer_ptr[m_nIdx]->bankinfo[0][((cbPKT_BANKINFO*)pPkt)->bank-1]), pPkt, sizeof(cbPKT_BANKINFO)); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_ADAPTFILTREP) - { - if (m_bStandAlone) - cb_cfg_buffer_ptr[m_nIdx]->adaptinfo[0] = *reinterpret_cast(pPkt); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_REFELECFILTREP) - { - if (m_bStandAlone) - cb_cfg_buffer_ptr[m_nIdx]->refelecinfo[0] = *reinterpret_cast(pPkt); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_SS_MODELREP) - { - if (m_bStandAlone) - { - cbPKT_SS_MODELSET rNew = *reinterpret_cast(pPkt); - UpdateSortModel(rNew); - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_SS_STATUSREP) - { - if (m_bStandAlone) - { - cbPKT_SS_STATUS rNew = *reinterpret_cast(pPkt); - cbPKT_SS_STATUS & rOld = cb_cfg_buffer_ptr[m_nIdx]->isSortingOptions.pktStatus; - rOld = rNew; - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_SS_DETECTREP) - { - if (m_bStandAlone) - { - // replace our copy with this one - cbPKT_SS_DETECT rNew = *reinterpret_cast(pPkt); - cbPKT_SS_DETECT & rOld = cb_cfg_buffer_ptr[m_nIdx]->isSortingOptions.pktDetect; - rOld = rNew; - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_SS_ARTIF_REJECTREP) - { - if (m_bStandAlone) - { - // replace our copy with this one - cbPKT_SS_ARTIF_REJECT rNew = *reinterpret_cast(pPkt); - cbPKT_SS_ARTIF_REJECT & rOld = cb_cfg_buffer_ptr[m_nIdx]->isSortingOptions.pktArtifReject; - rOld = rNew; - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_SS_NOISE_BOUNDARYREP) - { - if (m_bStandAlone) - { - // replace our copy with this one - cbPKT_SS_NOISE_BOUNDARY rNew = *reinterpret_cast(pPkt); - cbPKT_SS_NOISE_BOUNDARY & rOld = cb_cfg_buffer_ptr[m_nIdx]->isSortingOptions.pktNoiseBoundary[rNew.chan - 1]; - rOld = rNew; - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_SS_STATISTICSREP) - { - if (m_bStandAlone) - { - // replace our copy with this one - cbPKT_SS_STATISTICS rNew = *reinterpret_cast(pPkt); - cbPKT_SS_STATISTICS & rOld = cb_cfg_buffer_ptr[m_nIdx]->isSortingOptions.pktStatistics; - rOld = rNew; - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_FS_BASISREP) - { - if (m_bStandAlone) - { - cbPKT_FS_BASIS rPkt = *reinterpret_cast(pPkt); - UpdateBasisModel(rPkt); - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_LNCREP) - { - if (m_bStandAlone) - memcpy(&(cb_cfg_buffer_ptr[m_nIdx]->isLnc), pPkt, sizeof(cbPKT_LNC)); - // For 6.03 and before, use this packet instead of sysrep for instinfo event - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_REPFILECFG) - { - if (m_bStandAlone) - { - const auto * pPktFileCfg = reinterpret_cast(pPkt); - if (pPktFileCfg->options == cbFILECFG_OPT_REC || pPktFileCfg->options == cbFILECFG_OPT_STOP || (pPktFileCfg->options == cbFILECFG_OPT_TIMEOUT)) - { - cb_cfg_buffer_ptr[m_nIdx]->fileinfo = * reinterpret_cast(pPkt); - } - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_REPNTRODEINFO) - { - if (m_bStandAlone) - memcpy(&(cb_cfg_buffer_ptr[m_nIdx]->isLnc), pPkt, sizeof(cbPKT_LNC)); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_NMREP) - { - if (m_bStandAlone) - { - const auto * pPktNm = reinterpret_cast(pPkt); - // video source to go to the file header - if (pPktNm->mode == cbNM_MODE_SETVIDEOSOURCE) - { - if (pPktNm->flags > 0 && pPktNm->flags <= cbMAXVIDEOSOURCE) - { - memcpy(cb_cfg_buffer_ptr[m_nIdx]->isVideoSource[pPktNm->flags - 1].name, pPktNm->name, cbLEN_STR_LABEL); - cb_cfg_buffer_ptr[m_nIdx]->isVideoSource[pPktNm->flags - 1].fps = ((float)pPktNm->value) / 1000; - // fps>0 means valid video source - } - } - // trackable object to go to the file header - else if (pPktNm->mode == cbNM_MODE_SETTRACKABLE) - { - if (pPktNm->flags > 0 && pPktNm->flags <= cbMAXTRACKOBJ) - { - memcpy(cb_cfg_buffer_ptr[m_nIdx]->isTrackObj[pPktNm->flags - 1].name, pPktNm->name, cbLEN_STR_LABEL); - cb_cfg_buffer_ptr[m_nIdx]->isTrackObj[pPktNm->flags - 1].type = (uint16_t)(pPktNm->value & 0xff); - cb_cfg_buffer_ptr[m_nIdx]->isTrackObj[pPktNm->flags - 1].pointCount = (uint16_t)((pPktNm->value >> 16) & 0xff); - // type>0 means valid trackable - } - } - // nullify all tracking upon NM exit - else if (pPktNm->mode == cbNM_MODE_STATUS && pPktNm->value == cbNM_STATUS_EXIT) - { - memset(cb_cfg_buffer_ptr[m_nIdx]->isTrackObj, 0, sizeof(cb_cfg_buffer_ptr[m_nIdx]->isTrackObj)); - memset(cb_cfg_buffer_ptr[m_nIdx]->isVideoSource, 0, sizeof(cb_cfg_buffer_ptr[m_nIdx]->isVideoSource)); - } - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_WAVEFORMREP) - { - if (m_bStandAlone) - { - SetNumChans(); - const auto * pPktAoutWave = reinterpret_cast(pPkt); - uint16_t nChan = pPktAoutWave->chan; - if (IsChanAnalogOut(nChan)) - { - nChan -= (cb_pc_status_buffer_ptr[0]->cbGetNumAnalogChans() + 1); - if (nChan < AOUT_NUM_GAIN_CHANS) - { - uint8_t trigNum = pPktAoutWave->trigNum; - if (trigNum < cbMAX_AOUT_TRIGGER) - cb_cfg_buffer_ptr[m_nIdx]->isWaveform[nChan][trigNum] = *pPktAoutWave; - } - } - } - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_NPLAYREP) - { - if (m_bStandAlone) - { - const auto * pNew = reinterpret_cast(pPkt); - // Only store the main config packet in stand-alone mode - if (pNew->flags == cbNPLAY_FLAG_MAIN) - { - cbPKT_NPLAY & rOld = cb_cfg_buffer_ptr[m_nIdx]->isNPlay; - // replace our copy with this one - rOld = *pNew; - } - } - } - } // end if (pPkt->chid==0x8000 - } // end if (pPkt->chid & 0x8000 - else if ( (pPkt->cbpkt_header.chid > 0) && (pPkt->cbpkt_header.chid < cbPKT_SPKCACHELINECNT) ) - { - if (m_bStandAlone) - { - // post the packet to the cache buffer - memcpy( &(cb_spk_buffer_ptr[m_nIdx]->cache[pPkt->cbpkt_header.chid - 1].spkpkt[cb_spk_buffer_ptr[m_nIdx]->cache[pPkt->cbpkt_header.chid - 1].head]), - pPkt, (pPkt->cbpkt_header.dlen + cbPKT_HEADER_32SIZE) * 4); - - // increment the valid pointer - cb_spk_buffer_ptr[m_nIdx]->cache[pPkt->cbpkt_header.chid - 1].valid++; - - // increment the head pointer of the packet and check for wraparound - uint32_t head = cb_spk_buffer_ptr[m_nIdx]->cache[(pPkt->cbpkt_header.chid)-1].head + 1; - if (head >= cbPKT_SPKCACHEPKTCNT) - head = 0; - cb_spk_buffer_ptr[m_nIdx]->cache[pPkt->cbpkt_header.chid - 1].head = head; - } - } - - // -- Process the incoming packet inside the listeners -- - for (auto & i : m_listener) - i->ProcessIncomingPacket(pPkt); -} - -// Author & Date: Kirk Korver 25 Apr 2005 -// Purpose: update our sorting model -// Inputs: -// rUnitModel - the unit model packet of interest -inline void InstNetwork::UpdateSortModel(const cbPKT_SS_MODELSET & rUnitModel) const -{ - const uint32_t nChan = rUnitModel.chan; - uint32_t nUnit = rUnitModel.unit_number; - - // Unit 255 == noise, put it into the last slot - if (nUnit == 255) - nUnit = std::size(cb_cfg_buffer_ptr[m_nIdx]->isSortingOptions.asSortModel[0]) - 1; - - if (cb_library_initialized[m_nIdx] && cb_cfg_buffer_ptr[m_nIdx]) - cb_cfg_buffer_ptr[m_nIdx]->isSortingOptions.asSortModel[nChan][nUnit] = rUnitModel; -} - -// Author & Date: Hyrum L. Sessions 21 Apr 2009 -// Purpose: update our PCA basis model -// Inputs: -// rBasisModel - the basis model packet of interest -inline void InstNetwork::UpdateBasisModel(const cbPKT_FS_BASIS & rBasisModel) const -{ - if (0 == rBasisModel.chan) - return; // special packet request to get all basis, don't save it - - const uint32_t nChan = rBasisModel.chan - 1; - - if (cb_library_initialized[m_nIdx] && cb_cfg_buffer_ptr[m_nIdx]) - cb_cfg_buffer_ptr[m_nIdx]->isSortingOptions.asBasis[nChan] = rBasisModel; -} - -///////////////////////////////////////////////////////////////////////////// -// Author & Date: Kirk Korver 07 Jan 2003 -// Purpose: do any processing necessary to test for link failure (i.e. wire disconnected) -// Inputs: -// nTicks - the ever growing number of times that the mmtimer has been called -// rParent - the parent window -// nCurrentPacketCount - the number of packets that we have ever received -inline void InstNetwork::CheckForLinkFailure(const uint32_t nTicks, const uint32_t nCurrentPacketCount) -{ - if ((nTicks % 250) == 0) // Check every 2.5 seconds - { - if (m_nLastNumberOfPacketsReceived != nCurrentPacketCount) - { - m_nLastNumberOfPacketsReceived = nCurrentPacketCount; - } - else - { - InstNetworkEvent(NET_EVENT_LINKFAILURE); - } - } -} - -// Author & Date: Ehsan Azar 15 March 2010 -// Purpose: Process one timer tick (replaced Qt timerEvent) -void InstNetwork::processTimerTick() -{ - m_timerTicks++; // number of intervals - int burstcount = 0; - int recv_returned = 0; -// TRACE("m_timerTicks: %d\n", m_timerTicks); - if (m_bDone) - { - return; // Exit timer loop - } - ///////////////////////////////////////// - // below 5 seconds, call startup routines - if (m_timerTicks < 500) - { - // at time 0 request sysinfo - if (m_timerTicks == 1) - { - InstNetworkEvent(NET_EVENT_INSTCONNECTING); - cbSetSystemRunLevel(cbRUNLEVEL_RUNNING, 0, 0, cbNSP1 - 1, m_nInstance); - } - // at 0.5 seconds, if not already running, reset the hardware - else if (m_timerTicks == 50) - { - // get runlevel - cbGetSystemRunLevel(&m_runlevel, nullptr, nullptr, m_nInstance); - // if not running reset - if (cbRUNLEVEL_RUNNING != m_runlevel) - { - InstNetworkEvent(NET_EVENT_INSTHARDRESET); - cbSetSystemRunLevel(cbRUNLEVEL_HARDRESET, 0, 0, cbNSP1 - 1, m_nInstance); - } - } - // at 1.0 seconds, retrieve the hardware config - else if (m_timerTicks == 100) - { - InstNetworkEvent(NET_EVENT_INSTCONFIG); - cbPKT_GENERIC pktgeneric; - pktgeneric.cbpkt_header.time = 1; - pktgeneric.cbpkt_header.chid = 0x8000; - pktgeneric.cbpkt_header.type = cbPKTTYPE_REQCONFIGALL; - pktgeneric.cbpkt_header.dlen = 0; - cbSendPacketToInstrument(&pktgeneric, m_nInstance, 0); - } - // at 2.0 seconds, if not already running, do soft reset, which will lead to running state. - else if (m_timerTicks == 200) - { - InstNetworkEvent(NET_EVENT_INSTRUN); // going to soft reset and run - // if already running do not reset, otherwise reset - if (cbRUNLEVEL_RUNNING != m_runlevel) - { - cbSetSystemRunLevel(cbRUNLEVEL_RESET, 0, 0, cbNSP1 - 1, m_nInstance); - } - } - } // end if (m_timerTicks < 500 - if (m_icInstrument.Tick()) - { - InstNetworkEvent(NET_EVENT_PCTONSPLOST); - m_bDone = true; - } - - // Check for link failure because we always have heartbeat packets - if (!(m_instInfo & cbINSTINFO_NPLAY)) - CheckForLinkFailure(m_timerTicks, cb_rec_buffer_ptr[m_nIdx]->received); - - // Process 1024 remaining packets - while (burstcount < 1024) - { - burstcount++; - recv_returned = m_icInstrument.Recv(&(cb_rec_buffer_ptr[m_nIdx]->buffer[cb_rec_buffer_ptr[m_nIdx]->headindex])); - if (recv_returned <= 0) - break; - - // get pointer to the first packet in received data block - auto *pktptr = reinterpret_cast(&(cb_rec_buffer_ptr[m_nIdx]->buffer[cb_rec_buffer_ptr[m_nIdx]->headindex])); - - uint32_t bytes_to_process = recv_returned; - do { - ++m_nRecentPacketCount; // only count the "real" packets, not loopback ones - m_icInstrument.TestForReply(pktptr); // loopbacks won't need a "reply"...they are never sent - - // make sure that the next packet in the data block that we are processing fits. - const uint32_t quadlettotal = (pktptr->cbpkt_header.dlen) + cbPKT_HEADER_32SIZE; - const uint32_t packetsize = quadlettotal << 2; - if (packetsize > bytes_to_process) - { - // TODO: complain about bad packet - break; - } - // update time index - cb_rec_buffer_ptr[m_nIdx]->lasttime = pktptr->cbpkt_header.time; - // Do incoming packet process - ProcessIncomingPacket(pktptr); - - // increment packet pointer and subract out the packetsize from the processing counter - pktptr = reinterpret_cast(reinterpret_cast(pktptr) + packetsize); - bytes_to_process -= packetsize; - - // Increment head index and check for buffer wraparound. - // If the currently processed packet extends at all within the last 1k of the circular - // recording buffer, wrap the head pointer around to zero. This is the same mechanism - // that the client applications that read the buffer use to update their tail pointers. - cb_rec_buffer_ptr[m_nIdx]->headindex += quadlettotal; - if ((cb_rec_buffer_ptr[m_nIdx]->headindex) > (cbRECBUFFLEN - (cbCER_UDP_SIZE_MAX / 4))) - { - // rewind the circular buffer head pointer and increment the headwrap count - cb_rec_buffer_ptr[m_nIdx]->headwrap++; - cb_rec_buffer_ptr[m_nIdx]->headindex = 0; - - // Since multiple Cerebus packets can be contained within a single UDP packet, - // a few Cerebus packets may be extended beyond the bound of the currently - // processed packet. These need to be copied (wrapped) around to the beginning - // of the circular buffer so that they are not dropped. - if (bytes_to_process > 0) - { - // copy the remaining packet bytes - memcpy(&(cb_rec_buffer_ptr[m_nIdx]->buffer[0]), pktptr, - bytes_to_process); - - // wrap the internal packet pointer - pktptr = (cbPKT_GENERIC*) &(cb_rec_buffer_ptr[m_nIdx]->buffer[0]); - } - } - - // increment the packets received and data exchanged counters - cb_rec_buffer_ptr[m_nIdx]->received++; - m_dataCounter += quadlettotal; - - } while (bytes_to_process); // end do - } // end while (burstcount - // check for receive errors - if (recv_returned < 0) - { - // Complain - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - // Check for and process outgoing packets - // - // This routine roughly executes every 10ms. In order to prevent the NSP from being overloaded by - // floods of configuration packets, throttle the configuration packets bursts per 10ms. - { - // UINT nPacketsLeftInBurst = 16; - // - // The line above was in use for a while. It appears that sometimes a packet from the PC->NSP - // will be dropped. By reducing the possible number of packets that can be sent at a time, we - // appear to not have this problem any more. The real solution is to ensure that packets are sent - UINT nPacketsLeftInBurst = 4; - - auto *xmtpacket = (cbPKT_GENERIC*) &(cb_xmt_global_buffer_ptr[m_nIdx]->buffer[cb_xmt_global_buffer_ptr[m_nIdx]->tailindex]); - - while ((xmtpacket->cbpkt_header.time) && (nPacketsLeftInBurst--)) - { - // find the length of the packet - uint32_t quadlettotal = (xmtpacket->cbpkt_header.dlen) + cbPKT_HEADER_32SIZE; - - if (m_icInstrument.OkToSend() == false) - continue; - - // transmit the packet - m_icInstrument.Send(xmtpacket); - - // complete the packet processing by clearing the packet from the xmt buffer - memset(xmtpacket, 0, quadlettotal << 2); - cb_xmt_global_buffer_ptr[m_nIdx]->transmitted++; - cb_xmt_global_buffer_ptr[m_nIdx]->tailindex += quadlettotal; - m_dataCounter += quadlettotal; - - if (cb_xmt_global_buffer_ptr[m_nIdx]->tailindex - > cb_xmt_global_buffer_ptr[m_nIdx]->last_valid_index) - { - cb_xmt_global_buffer_ptr[m_nIdx]->tailindex = 0; - } - - // update the local reference pointer - xmtpacket = (cbPKT_GENERIC*) &(cb_xmt_global_buffer_ptr[m_nIdx]->buffer[cb_xmt_global_buffer_ptr[m_nIdx]->tailindex]); - } - } - // Signal the other apps that new data is available -#ifdef WIN32 - PulseEvent(cb_sig_event_hnd[m_nIdx]); -#else - sem_post((sem_t *)cb_sig_event_hnd[m_nIdx]); -#endif -} - -// Author & Date: Ehsan Azar 15 March 2010 -// Purpose: The thread function -void InstNetwork::run() -{ - // No instrument yet - m_instInfo = 0; - // Start initializing instrument network - InstNetworkEvent(NET_EVENT_INIT); - - if (m_listener.empty()) - { - // If listener not set - InstNetworkEvent(NET_EVENT_LISTENERERR); - return; - } - - m_nIdx = cb_library_index[m_nInstance]; - - // Open the cbhwlib library and create the shared objects - cbRESULT cbRet = cbOpen(false, m_nInstance); - if (cbRet == cbRESULT_OK) - { - m_nIdx = cb_library_index[m_nInstance]; - m_bStandAlone = false; - InstNetworkEvent(NET_EVENT_NETCLIENT); // Client to the Central application - } else if (cbRet == cbRESULT_NOCENTRALAPP) { // If Central is not running then run as stand alone - m_bStandAlone = true; // Run stand alone without Central - // Run as stand-alone application - cbRet = cbOpen(true, m_nInstance); - if (cbRet) - { - InstNetworkEvent(NET_EVENT_CBERR, cbRet); // report cbRESULT - return; - } - m_nIdx = cb_library_index[m_nInstance]; - InstNetworkEvent(NET_EVENT_NETSTANDALONE); // Stand-alone application - } else { - // Report error and quit - InstNetworkEvent(NET_EVENT_CBERR, cbRet); - return; - } - - STARTUP_OPTIONS startupOption = m_nStartupOptionsFlags; - // If non-stand-alone just being local can be detected at this stage - // because the network is not yet running. - cbGetInstInfo(cbNSP1, &m_instInfo, m_nInstance); - if (m_instInfo & cbINSTINFO_LOCAL) - { - // if local instrument detected - startupOption = OPT_LOCAL; // Override startup option - } - // If stand-alone network - if (m_bStandAlone) - { - // Give nPlay and Cereplex more time - bool bHighLatency = (m_instInfo & (cbINSTINFO_NPLAY | cbINSTINFO_CEREPLEX)); - m_icInstrument.Reset(bHighLatency ? (int)INST_TICK_COUNT : (int)Instrument::TICK_COUNT); - // Set network connection details - m_icInstrument.SetNetwork(PROTOCOL_UDP, m_nInPort, m_nOutPort, - m_strInIP.c_str(), m_strOutIP.c_str(), m_nRange); - // Open UDP - cbRESULT cbres = m_icInstrument.Open(startupOption, m_bBroadcast, m_bDontRoute, m_bNonBlocking, m_nRecBufSize); - if (cbres) - { - // if we can't open NSP, say so - InstNetworkEvent(NET_EVENT_NETOPENERR, cbres); // report cbRESULT - cbClose(m_bStandAlone, m_nInstance); // Close library - return; - } - } - - // Reset counters and initial state - m_timerTicks = 0; - m_nRecentPacketCount = 0; - m_dataCounter = 0; - m_nLastNumberOfPacketsReceived = 0; - m_runlevel = cbRUNLEVEL_SHUTDOWN; - m_bDone = false; - - // If stand-alone setup network packet handling timer - if (m_bStandAlone) - { - // Start network packet processing using custom timer loop (10ms ticks) -#ifdef WIN32 - timeBeginPeriod(1); -#endif - // Custom timer loop replacing Qt event loop - using namespace std::chrono; - auto nextTick = steady_clock::now(); - const auto tickInterval = milliseconds(10); - - while (!m_bDone) - { - nextTick += tickInterval; - processTimerTick(); - std::this_thread::sleep_until(nextTick); - } - -#ifdef WIN32 - timeEndPeriod(1); -#endif - } else { // else wait for central application data - // Instrument info for non-stand-alone - InstNetworkEvent(NET_EVENT_INSTINFO, m_instInfo); // Wake's main thread's wait - bool bMonitorThreadMessageWaiting = false; // If message is waiting in non stand-alone mode - UINT missed_messages = 0; - // Start the network loop - while (!m_bDone) - { - cbRESULT waitresult = cbWaitforData(m_nInstance); //hls ); - if (waitresult == cbRESULT_NOCENTRALAPP) - { - // No instrument anymore - m_instInfo = 0; - InstNetworkEvent(NET_EVENT_NETOPENERR, cbRESULT_NOCENTRALAPP); - m_bDone = true; - } - else if ((waitresult == cbRESULT_OK) || (bMonitorThreadMessageWaiting == false)) - { - missed_messages = 0; - bMonitorThreadMessageWaiting = true; - OnWaitEvent(); // Handle incoming packets - } - else if ((missed_messages++) > 25) - bMonitorThreadMessageWaiting = false; - } - } - // No instrument anymore - m_instInfo = 0; - InstNetworkEvent(NET_EVENT_CLOSE); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Give apps some time to flush their work - if (m_bStandAlone) - { - // Close the Data Socket and Winsock Subsystem - m_icInstrument.Close(); - } - // Close the library - cbClose(m_bStandAlone, m_nInstance); -} - -// Author & Date: Ehsan Azar 1 June 2010 -// Purpose: Network incoming packet handling in non-stand-alone mode -void InstNetwork::OnWaitEvent() -{ - // Look to see how much there is to process - uint32_t pktstogo; - cbCheckforData(m_enLOC, &pktstogo, m_nInstance); - - if (m_enLOC == LOC_CRITICAL) - { - cbMakePacketReadingBeginNow(m_nInstance); - InstNetworkEvent(NET_EVENT_CRITICAL); - return; - } - // Limit how many we can look at - pktstogo = std::min(pktstogo, MAX_NUM_OF_PACKETS_TO_PROCESS_PER_PASS); - - // process any available packets - for(unsigned int p = 0; p < pktstogo; ++p) - { - cbPKT_GENERIC *pktptr = cbGetNextPacketPtr(m_nInstance); - if (pktptr == nullptr) - { - cbMakePacketReadingBeginNow(m_nInstance); - InstNetworkEvent(NET_EVENT_CRITICAL); - break; - } else { - ProcessIncomingPacket(pktptr); - } - } -} diff --git a/src/cbhwlib/InstNetwork.h b/src/cbhwlib/InstNetwork.h deleted file mode 100755 index 5528eac8..00000000 --- a/src/cbhwlib/InstNetwork.h +++ /dev/null @@ -1,154 +0,0 @@ -/* =STS=> InstNetwork.h[2733].aa08 open SMID:8 */ -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2010 - 2017 Blackrock Microsystems -// -// $Workfile: InstNetwork.h $ -// $Archive: /common/InstNetwork.h $ -// $Revision: 1 $ -// $Date: 3/15/10 12:21a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// -// -// PURPOSE: -// -// Common xPlatform instrument network -// - -#ifndef INSTNETWORK_H_INCLUDED -#define INSTNETWORK_H_INCLUDED - -#include "../cbproto/debugmacs.h" -#include "../include/cerelink/cbhwlib.h" -#include "cbHwlibHi.h" -#include "../central/Instrument.h" -#include "cki_common.h" -#include -#include -#include -#include -#include - -enum NetCommandType -{ - NET_COMMAND_NONE = 0, // invalid command - NET_COMMAND_OPEN, // open network - NET_COMMAND_CLOSE, // close network - NET_COMMAND_STANDBY, // instrument standby - NET_COMMAND_SHUTDOWN, // instrument shutdown -}; -// Events by network -enum NetEventType -{ - NET_EVENT_NONE = 0, // Invalid event - NET_EVENT_INIT, // Instrument initialization started - NET_EVENT_LISTENERERR, // Listener is not set - NET_EVENT_CBERR, // cbRESULT error happened - NET_EVENT_NETOPENERR, // Error opening the instrument network - NET_EVENT_NETCLIENT, // Client network connection - NET_EVENT_NETSTANDALONE, // Stand-alone network connection - NET_EVENT_INSTINFO, // Instrument information (sent when network is established) - NET_EVENT_INSTCONNECTING, // Instrument connecting - NET_EVENT_INSTHARDRESET, // Instrument reset hardware - NET_EVENT_INSTCONFIG, // Instrument retrieving configuration - NET_EVENT_INSTRUN, // Instrument reset software to run - NET_EVENT_PCTONSPLOST, // Connection lost - NET_EVENT_LINKFAILURE, // Link failure - NET_EVENT_CRITICAL, // Critical data catchup - NET_EVENT_CLOSE, // Instrument closed - NET_EVENT_RESET, // Instrument got reset - NET_EVENT_LOCKEDRESET, // Locked reset (for recording) -}; - -// Author & Date: Ehsan Azar 15 March 2010 -// Purpose: Instrument networking thread -class InstNetwork -{ -public: - // Instrument network listener - class Listener - { - public: - virtual ~Listener() = default; - // Callback function to process packets, must be implemented in target class - virtual void ProcessIncomingPacket(const cbPKT_GENERIC * pPkt) = 0; - }; - -public: - InstNetwork(STARTUP_OPTIONS startupOption = OPT_NONE); - virtual ~InstNetwork() = default; - void Open(Listener * listener); // Open the network - void ShutDown(); // Instrument shutdown - void StandBy(); // Instrument standby - bool IsStandAlone() const {return m_bStandAlone;} // If running in stand-alone - uint32_t getPacketCounter() const {return m_nRecentPacketCount;} - uint32_t getDataCounter() const {return m_dataCounter;} - void SetNumChans(); - void Start(); // Start the network thread -protected: - enum { INST_TICK_COUNT = 10 }; - void run(); - void ProcessIncomingPacket(const cbPKT_GENERIC * pPkt); // Process incoming packets in stand-alone mode - void processTimerTick(); // Process one timer tick (was timerEvent for Qt) - void OnWaitEvent(); // Non-stand-alone networking - inline void CheckForLinkFailure(uint32_t nTicks, uint32_t nCurrentPacketCount); // Check link failure -private: - void UpdateSortModel(const cbPKT_SS_MODELSET & rUnitModel) const; - void UpdateBasisModel(const cbPKT_FS_BASIS & rBasisModel) const; -private: - static const uint32_t MAX_NUM_OF_PACKETS_TO_PROCESS_PER_PASS; - std::unique_ptr m_thread; // The network thread - cbLevelOfConcern m_enLOC; // level of concern - STARTUP_OPTIONS m_nStartupOptionsFlags; - std::vector m_listener; // instrument network listeners - uint32_t m_timerTicks; // network timer ticks - std::atomic m_bDone;// flag to finish networking thread - uint32_t m_nRecentPacketCount; // number of real packets - uint32_t m_dataCounter; // data counter - uint32_t m_nLastNumberOfPacketsReceived; - uint32_t m_runlevel; // Last runlevel - bool m_bInitChanCount; -protected: - bool m_bStandAlone; // If it is stand-alone - Instrument m_icInstrument; // The instrument - uint32_t m_instInfo; // Last instrument state - uint32_t m_nInstance; // library instance - uint32_t m_nIdx; // library instance index - int m_nInPort; // Client port number - int m_nOutPort; // Instrument port number - bool m_bBroadcast; - bool m_bDontRoute; - bool m_bNonBlocking; - int m_nRecBufSize; - std::string m_strInIP; // Client IPv4 address - std::string m_strOutIP; // Instrument IPv4 address - int m_nRange; // number of IP addresses to try (default is 16) - -public: - void Close(); // stop timer and close the message loop - -private: - void OnNetCommand(NetCommandType cmd, unsigned int code = 0); - - // Helper functions to specialize this class for networking -protected: - virtual void OnInstNetworkEvent(NetEventType, unsigned int) {;} - virtual void OnExec() {;} - - // Called to notify about network events (was Qt signal, now virtual function) - // Note: No default arguments on virtual functions (linter warning) - virtual void InstNetworkEvent(NetEventType type, unsigned int code) { - // Default implementation calls the virtual handler - OnInstNetworkEvent(type, code); - } - - // Non-virtual overload to provide default argument (code = 0) - void InstNetworkEvent(NetEventType type) { - InstNetworkEvent(type, 0); - } -}; - -#endif // INSTNETWORK_H_INCLUDED diff --git a/src/cbhwlib/cbHwlibHi.cpp b/src/cbhwlib/cbHwlibHi.cpp deleted file mode 100755 index 99c2d263..00000000 --- a/src/cbhwlib/cbHwlibHi.cpp +++ /dev/null @@ -1,1196 +0,0 @@ -// =STS=> cbHwlibHi.cpp[1688].aa22 submit SMID:23 -////////////////////////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003 - 2008 Cyberkinetics, Inc. -// (c) Copyright 2008 - 2021 Blackrock Microsystems -// -// $Workfile: cbHwlibHi.cpp $ -// $Archive: /Cerebus/WindowsApps/cbhwlib/cbHwlibHi.cpp $ -// $Revision: 13 $ -// $Date: 4/05/04 11:29a $ -// $Author: Kkorver $ -// -// $NoKeywords: $ -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#include "StdAfx.h" // MFC core and standard components -#include "debugmacs.h" -#include "cbHwlibHi.h" -#include -#include "cki_common.h" - -#ifdef _DEBUG -#undef THIS_FILE -static char THIS_FILE[]=__FILE__; -#endif - - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -// Author & Date: Kirk Korver 30 Jan 2006 -// Purpose: tell me if CentralSim is running, or is it regular central -// Outputs: -// TRUE means CentralSIM is running; FALSE, Central -bool IsSimRunning(uint32_t nInstance) -{ -#ifdef WIN32 - const uint32_t nIdx = cb_library_index[nInstance]; - char szTitle[255] = ""; - ::GetWindowTextA(static_cast(cb_cfg_buffer_ptr[nIdx]->hwndCentral), szTitle, sizeof(szTitle)); - if (strstr(szTitle, "SIM")) - return true; - else - return false; -#else - return (cbCheckApp("SIM") == cbRESULT_OK); -#endif -} - - -// TRUE means yes; FALSE, no -bool IsSpikeProcessingEnabled(const uint32_t nChan, const uint32_t nInstance) -{ - uint32_t dwFlags; - if (cbRESULT_OK != ::cbGetAinpSpikeOptions(nChan, &dwFlags, nullptr, nInstance)) - return false; - - return (dwFlags & cbAINPSPK_EXTRACT) ? true : false; -} - - - -// TRUE means yes; FALSE, no -bool IsContinuousProcessingEnabled(const uint32_t nChan, const uint32_t nInstance) -{ - // In the case where the call fails, we will still return FALSE. - uint32_t nGroup = 0; - ::cbGetAinpSampling(nChan, nullptr, &nGroup, nInstance); - - return nGroup != 0; -} - -// TRUE means yes; FALSE, no -bool IsRawProcessingEnabled(const uint32_t nChan, const uint32_t nInstance) -{ - // In the case where the call fails, we will still return FALSE. - uint32_t nainpopts = 0; - ::cbGetAinpOpts( nChan, &nainpopts, nullptr, nullptr, nInstance); - - return ((nainpopts & cbAINP_RAWSTREAM_ENABLED) != 0); -} - - -// TRUE means yes; FALSE, no -bool IsChanAnyDigIn(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwChanCaps; - - bool result = dwChan >= MIN_CHANS; - result &= cbGetChanCaps(dwChan, &dwChanCaps, nInstance) == cbRESULT_OK; - result &= (dwChanCaps & cbCHAN_DINP) == cbCHAN_DINP; - return result; -} - - -// TRUE means yes; FALSE, no -bool IsChanSerial(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwDiginCaps; - bool result = IsChanAnyDigIn(dwChan, nInstance); - result &= cbGetDinpCaps(dwChan, &dwDiginCaps, nInstance) == cbRESULT_OK; - result &= (dwDiginCaps & cbDINP_SERIALMASK) == cbDINP_SERIALMASK; - return result; -} - - -// TRUE means yes; FALSE, no -bool IsChanDigin(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwDiginCaps; - bool result = IsChanAnyDigIn(dwChan, nInstance); - result &= cbGetDinpCaps(dwChan, &dwDiginCaps, nInstance) == cbRESULT_OK; - result &= (dwDiginCaps & cbDINP_SERIALMASK) != cbDINP_SERIALMASK; - return result; -} - -bool IsChanDigout(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwChanCaps; - - bool result = dwChan >= MIN_CHANS; - result &= cbGetChanCaps(dwChan, &dwChanCaps, nInstance) == cbRESULT_OK; - result &= (dwChanCaps & cbCHAN_DOUT) == cbCHAN_DOUT; - return result; -} - - -// Author & Date: Almut Branner 15 Jan 2004 -// Purpose: Find out whether an analog in channel -// Input: nChannel - the channel ID that we want to check -bool IsChanAnalogIn(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwChanCaps; - - bool result = dwChan >= MIN_CHANS; - result &= cbGetChanCaps(dwChan, &dwChanCaps, nInstance) == cbRESULT_OK; - result &= (dwChanCaps & cbCHAN_AINP) == cbCHAN_AINP; - return result; -} - - -// Author & Date: Almut Branner 15 Jan 2004 -// Purpose: Find out whether a channel is a Front-End analog in channel -// Input: nChannel - the channel ID that we want to check -bool IsChanFEAnalogIn(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwChanCaps; - - return ((dwChan >= MIN_CHANS) && - (cbGetChanCaps(dwChan, &dwChanCaps, nInstance) == cbRESULT_OK) && - ((cbCHAN_AINP | cbCHAN_ISOLATED) == (dwChanCaps & (cbCHAN_AINP | cbCHAN_ISOLATED)))); -} - - -// Author & Date: Almut Branner 15 Jan 2004 -// Purpose: Find out whether a channel is an Analog-In analog in channel -// Input: nChannel - the channel ID that we want to check -bool IsChanAIAnalogIn(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwChanCaps; - - return ((dwChan >= MIN_CHANS) && - (cbGetChanCaps(dwChan, &dwChanCaps, nInstance) == cbRESULT_OK) && - (dwChanCaps & cbCHAN_AINP) && - !(dwChanCaps & cbCHAN_ISOLATED)); -} - - -/// @author Hyrum L. Sessions -/// @date 14 Feb 2017 -/// @brief Find out whether a channel is an Analog Out channel, don't include audio out -// Input: nChannel - the channel ID that we want to check -bool IsChanAnalogOut(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwChanCaps; - uint32_t dwAnaOutCaps; - - return ((dwChan >= MIN_CHANS) && - (cbGetChanCaps(dwChan, &dwChanCaps, nInstance) == cbRESULT_OK) && - (cbGetAoutCaps(dwChan, &dwAnaOutCaps, nullptr, nullptr, nInstance) == cbRESULT_OK) && - (dwChanCaps & cbCHAN_AOUT) && - ((dwAnaOutCaps & cbAOUT_AUDIO) == 0)); -} - - -/// @author Hyrum L. Sessions -/// @date 20 Feb 2017 -/// @brief Find out whether a channel is an Audio Out channel -// Input: nChannel - the channel ID that we want to check -bool IsChanAudioOut(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t dwChanCaps; - uint32_t dwAnaOutCaps; - - return ((dwChan >= MIN_CHANS) && - (cbGetChanCaps(dwChan, &dwChanCaps, nInstance) == cbRESULT_OK) && - (cbGetAoutCaps(dwChan, &dwAnaOutCaps, nullptr, nullptr, nInstance) == cbRESULT_OK) && - (dwChanCaps & cbCHAN_AOUT) && - ((dwAnaOutCaps & cbAOUT_AUDIO) == cbAOUT_AUDIO)); -} - - -// Author & Date: Almut Branner 21 Nov 2003 -// Purpose: Find out whether a channel is continuously sampled -// Input: nChannel - the channel ID that we want to check -bool IsChanCont(const uint32_t dwChan, const uint32_t nInstance) -{ - uint32_t nGroup = 0; - - ::cbGetAinpSampling(dwChan, nullptr, &nGroup, nInstance); - - return (nGroup != 0); -} - - -// Author & Date: Kirk Korver 29 Oct 2003 -// Purpose: Tell me if this channel is enabled -// Inputs: -// nChannel - the channel of interest (1 based) -// Outputs: -// TRUE if the channel is enabled; FALSE, otherwise -bool IsChannelEnabled(const uint32_t nChannel, const uint32_t nInstance) -{ - ASSERT(nChannel >=1); - ASSERT(nChannel <= MAX_CHANS_DIGITAL_OUT); - - if (IsChanAnalogIn(nChannel)) - return IsAnalogInEnabled(nChannel); - - if (IsChanAnalogOut(nChannel)) - return IsAnalogOutEnabled(nChannel); - - if (!IsChanDigout(nChannel)) - return IsAudioEnabled(nChannel); - - if (IsChanDigin(nChannel)) - return IsDigitalInEnabled(nChannel); - - if (IsChanSerial(nChannel)) - return IsSerialEnabled(nChannel, nInstance); - - if (IsChanDigout(nChannel)) - return IsDigitalOutEnabled(nChannel, nInstance); - - return false; -} - - -// Author & Date: Kirk Korver 29 Oct 2003 -// Purpose: Tell me if this channel is enabled -// Inputs: -// nChannel - the channel of interest (1 based) -// Outputs: -// TRUE if the channel is enabled; FALSE, otherwise -bool IsAnalogInEnabled(uint32_t nChannel) -{ - TRACE("*** ::IsAnalogInEnabled *** not functional\n"); - return true; -} - -// Author & Date: Kirk Korver 29 Oct 2003 -// Purpose: Tell me if this channel is enabled -// Inputs: -// nChannel - the channel of interest (1 based) -// Outputs: -// TRUE if the channel is enabled; FALSE, otherwise -bool IsAnalogOutEnabled(uint32_t nChannel) -{ - TRACE("*** ::IsAnalogOutEnabled *** not functional\n"); - return true; -} - -// Author & Date: Kirk Korver 29 Oct 2003 -// Purpose: Tell me if this channel is enabled -// Inputs: -// nChannel - the channel of interest (1 based) -// Outputs: -// TRUE if the channel is enabled; FALSE, otherwise -bool IsAudioEnabled(uint32_t nChannel) -{ - TRACE("*** ::IsAudioEnabled *** not functional\n"); - return true; -} - - -// Author & Date: Kirk Korver 29 Oct 2003 -// Purpose: Tell me if this channel is enabled -// Inputs: -// nChannel - the channel of interest (1 based) -// Outputs: -// TRUE if the channel is enabled; FALSE, otherwise -bool IsDigitalInEnabled(uint32_t nChannel) -{ - TRACE("*** ::IsDigitalInEnabled *** not functional\n"); - return true; -} - -// Author & Date: Kirk Korver 29 Oct 2003 -// Purpose: Tell me if this channel is enabled -// Inputs: -// nChannel - the channel of interest (1 based) -// Outputs: -// TRUE if the channel is enabled; FALSE, otherwise -bool IsSerialEnabled(const uint32_t nChannel, const uint32_t nInstance) -{ - uint32_t nOptions; - ::cbGetDinpOptions(nChannel, &nOptions, nullptr, nInstance);//get EOPchar - - if (nOptions & cbDINP_SERIALMASK) - return true; - else - return false; -} - -// Author & Date: Kirk Korver 29 Oct 2003 -// Purpose: Tell me if this channel is enabled -// Inputs: -// nChannel - the channel of interest (1 based) -// Outputs: -// TRUE if the channel is enabled; FALSE, otherwise -bool IsDigitalOutEnabled(const uint32_t nChannel, const uint32_t nInstance) -{ - uint32_t nOptions; - cbGetDoutOptions(nChannel, &nOptions, nullptr, nullptr, nullptr, nullptr, nullptr, nInstance); - - return (nOptions & cbDOUT_1BIT) ? true : false; -} - -// Author & Date: Almut Branner 6 Nov 2003 -// Purpose: Tell me whether hoops are defined for a channel -// Inputs: nChannel - the channel of interest -// Outputs: TRUE if hoops are defined, FALSE, otherwise -bool AreHoopsDefined(const uint32_t nChannel, const uint32_t nInstance) -{ - cbHOOP hoops[cbMAXUNITS][cbMAXHOOPS]; - cbGetAinpSpikeHoops(nChannel, &hoops[0][0], nInstance); - - for (const auto & hoop : hoops) - if (hoop[0].valid) - return true; - - return false; -} - -// Author & Date: Kirk Korver 24 Mar 2004 -// Purpose: tell me if there are hoops for this channel and unit -// Inputs: -// nChannel - the 1 based channel number -// nUnit - the 0 based unit number -bool AreHoopsDefined(const uint32_t nChannel, const uint32_t nUnit, const uint32_t nInstance) -{ - cbHOOP hoops[cbMAXUNITS][cbMAXHOOPS]; - cbGetAinpSpikeHoops(nChannel, &hoops[0][0], nInstance); - - if (hoops[nUnit][0].valid) - return true; - else - return false; -} - -// Author & Date: Ehsan Azar June 3, 2009 -// Purpose: determine if a channel has valid sorting unit -// Input: nChannel = channel to track 1 - based -// dwUnit the unit number (1=< dwUnit <=cbMAXUNITS) -bool cbHasValidUnit(const uint32_t dwChan, const uint32_t dwUnit, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - const cbSPIKE_SORTING *pSortingOptions = &cb_cfg_buffer_ptr[nIdx]->isSortingOptions; - if (dwUnit > cbMAXUNITS) - return false; - return (pSortingOptions->asSortModel[dwChan - 1][dwUnit].valid != 0); -} - -// Author & Date: Ehsan Azar June 2, 2009 -// Purpose: Return if given channel has a sorting algorithm -// If cbAINPSPK_ALLSORT is passed it returns if there is any sorting -// If cbAINPSPK_NOSORT is passed it returns if there is no sorting -// Input: spkSrtOpt = Sorting methods: -// cbAINPSPK_HOOPSORT, cbAINPSPK_SPREADSORT, -// cbAINPSPK_CORRSORT, cbAINPSPK_PEAKMAJSORT, -// cbAINPSPK_PEAKFISHSORT, cbAINPSPK_PCAMANSORT, -// cbAINPSPK_NOSORT, cbAINPSPK_OLDAUTOSORT, -// cbAINPSPK_ALLSORT -bool cbHasSpikeSorting(const uint32_t dwChan, const uint32_t spkSrtOpt, const uint32_t nInstance) -{ - uint32_t dwFlags; - ::cbGetAinpSpikeOptions(dwChan, &dwFlags, nullptr, nInstance); - if (spkSrtOpt == cbAINPSPK_NOSORT) - return ((dwFlags & cbAINPSPK_ALLSORT) == cbAINPSPK_NOSORT); - else if (spkSrtOpt == cbAINPSPK_ALLSORT) - return ((dwFlags & cbAINPSPK_ALLSORT) != cbAINPSPK_NOSORT); - else - return ((dwFlags & spkSrtOpt) != cbAINPSPK_NOSORT); -} - -// Author & Date: Ehsan Azar June 2, 2009 -// Purpose: Set a sorting algorithm for a channel -// Input: spkSrtOpt = Sorting methods: -// cbAINPSPK_HOOPSORT, cbAINPSPK_SPREADSORT, -// cbAINPSPK_CORRSORT, cbAINPSPK_PEAKMAJSORT, -// cbAINPSPK_PEAKFISHSORT, cbAINPSPK_PCAMANSORT, -// cbAINPSPK_NOSORT -cbRESULT cbSetSpikeSorting(const uint32_t dwChan, const uint32_t spkSrtOpt, const uint32_t nInstance) -{ - uint32_t dwFlags, dwFilter; - ::cbGetAinpSpikeOptions(dwChan, &dwFlags, &dwFilter, nInstance); - - dwFlags &= ~(cbAINPSPK_ALLSORT) ; // Delete all sortings first - dwFlags |= spkSrtOpt; // Add requested sorting only - cbRESULT nRet = ::cbSetAinpSpikeOptions(dwChan, dwFlags, dwFilter, nInstance); - return nRet; -} - - -// Author & Date: Almut Branner Dec 9, 2003 -// Purpose: Set all output channels to track a particular channel -// Input: nChannel = channel to track 1 - based -void TrackChannel(const uint32_t nChannel, const uint32_t nInstance) -{ - int32_t smpdispmax = 0; - int32_t spkdispmax = 0; - cbPROCINFO isProcInfo; - - // Only do this if the channel is an analog input channel - if (IsChanAnalogIn(nChannel)) - { - uint32_t nChanProcMax = 0; - uint32_t nChanProcStart = 0; - // Find channel range for the NSP with the channel to be monitored - for (uint32_t nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if (NSP_FOUND == cbGetNspStatus(nProc)) - { - ::cbGetProcInfo(nProc, &isProcInfo); - nChanProcMax += isProcInfo.chancount; - if (cbGetChanInstrument(nChannel) == nProc) - break; - nChanProcStart = nChanProcMax; - } - } - - // scan through the analog output channels - // if channel exists and is set to track, update its source channel - for (uint32_t nOutCh = nChanProcStart + 1; nOutCh <= nChanProcMax; ++nOutCh) - { - if (IsChanAnalogOut(nOutCh) || IsChanAudioOut(nOutCh)) - { - uint32_t options, monchan; - int32_t value; - if (cbGetAoutOptions(nOutCh, &options, &monchan, &value, nInstance) == cbRESULT_OK) - { - if (options & cbAOUT_TRACK) - { - cbSetAoutOptions(nOutCh, options, nChannel, value, nInstance); - - ///// Now adjust any of the analog output monitoring ///// - // Get the input scaling - cbSCALING isInScaling; - ::cbGetAinpDisplay(nChannel, nullptr, &smpdispmax, &spkdispmax, nullptr); - ::cbGetAinpScaling(nChannel, &isInScaling, nInstance); - - if (options & cbAOUT_MONITORSMP) // Monitoring the continuous signal - { - const int32_t nAnaMax = smpdispmax * isInScaling.anamax / isInScaling.digmax; - cbSCALING isOutScaling; - - isOutScaling.digmin = static_cast(-smpdispmax); - isOutScaling.digmax = static_cast(smpdispmax); - isOutScaling.anamin = -nAnaMax; - isOutScaling.anamax = nAnaMax; - strncpy(isOutScaling.anaunit, isInScaling.anaunit, sizeof(isOutScaling.anaunit)); - ::cbSetAoutScaling(nOutCh, &isOutScaling, nInstance); - } - if (options & cbAOUT_MONITORSPK) // Monitoring the spikes - { - int32_t nAnaMax = spkdispmax * isInScaling.anamax / isInScaling.digmax; - cbSCALING isOutScaling; - - isOutScaling.digmin = static_cast(-spkdispmax); - isOutScaling.digmax = static_cast(spkdispmax); - isOutScaling.anamin = -nAnaMax; - isOutScaling.anamax = nAnaMax; - strncpy(isOutScaling.anaunit, isInScaling.anaunit, sizeof(isOutScaling.anaunit)); - ::cbSetAoutScaling(nOutCh, &isOutScaling, nInstance); - } - } - } - } - // Now scan through the Digital Out channels and set them - if (IsChanDigout(nOutCh)) - { - uint32_t nOptions; - int32_t nVal; - if (cbRESULT_OK == ::cbGetDoutOptions(nOutCh, &nOptions, nullptr, &nVal, nullptr, nullptr, nullptr, nInstance)) - { - // Now if we are set to track, then change the monitored channel - if (nOptions & cbDOUT_TRACK) - { - ::cbSetDoutOptions(nOutCh, nOptions, nChannel, nVal, cbDOUT_TRIGGER_NONE, 0, 0, nInstance); - } - } - } - } - } -} - - -// Author & Date: Kirk Korver 10 Feb 2004 -// Purpose: update the analog input scaling. If there is an Analog Output -// channel which is monitoring this channel, then update the output -// scaling to match the displayed scaling -// Inputs: -// chan - the analog input channel being altered (1 based) -// smpdispmax - the maximum digital value of the continuous display -// spkdispmax - the maximum digital value of the spike display -// lncdispmax - the maximum digital value of the LNC display -cbRESULT cbSetAnaInOutDisplay(const uint32_t chan, const int32_t smpdispmax, const int32_t spkdispmax, const int32_t lncdispmax, const uint32_t nInstance) -{ - // Start off by sending the new Analog In scaling - cbRESULT retVal = ::cbSetAinpDisplay(chan, -smpdispmax, smpdispmax, spkdispmax, lncdispmax, nInstance); - if (retVal != cbRESULT_OK) - return retVal; - - - ///// Now adjust any of the analog output monitoring ///// - - // There are 6 analog channels of interest - // They happen to be numbered so that AnalogOut and AudioOut are continuous - for (uint32_t nAnaChan = MIN_CHANS; nAnaChan <= cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); ++nAnaChan) //1-based ch numbering, 0 is reserved - { - if (IsChanAnalogOut(nAnaChan) || IsChanAudioOut(nAnaChan)) - { - uint32_t dwOptions = 0; - uint32_t dwMonChan = 0; - ::cbGetAoutOptions(nAnaChan, &dwOptions, &dwMonChan, nullptr, nInstance); - dwMonChan = cbGetExpandedChannelNumber(cbGetChanInstrument(dwMonChan), dwMonChan); - - if (dwMonChan == chan) - { - // Get the input scaling - cbSCALING isInScaling; - retVal = ::cbGetAinpScaling(chan, &isInScaling, nInstance); - if (retVal != cbRESULT_OK) - return retVal; - - if (dwOptions & cbAOUT_MONITORSMP) // Monitoring the continuous signal - { - const int32_t nAnaMax = smpdispmax * isInScaling.anamax / isInScaling.digmax; - cbSCALING isOutScaling; - - isOutScaling.digmin = static_cast(-smpdispmax); - isOutScaling.digmax = static_cast(smpdispmax); - isOutScaling.anamin = -nAnaMax; - isOutScaling.anamax = nAnaMax; - strncpy(isOutScaling.anaunit, isInScaling.anaunit, sizeof(isOutScaling.anaunit)); - retVal = ::cbSetAoutScaling(nAnaChan, &isOutScaling, nInstance); - if (retVal != cbRESULT_OK) - return retVal; - } - else if (dwOptions & cbAOUT_MONITORSPK) // Monitoring the spikes - { - int32_t nAnaMax = spkdispmax * isInScaling.anamax / isInScaling.digmax; - cbSCALING isOutScaling; - - isOutScaling.digmin = static_cast(-spkdispmax); - isOutScaling.digmax = static_cast(spkdispmax); - isOutScaling.anamin = -nAnaMax; - isOutScaling.anamax = nAnaMax; - strncpy(isOutScaling.anaunit, isInScaling.anaunit, sizeof(isOutScaling.anaunit)); - retVal = ::cbSetAoutScaling(nAnaChan, &isOutScaling, nInstance); - if (retVal != cbRESULT_OK) - return retVal; - } - } - } - } - return retVal; -} - -// Author & Date: Hyrum L. Sessions 25 Jan 2006 -// Purpose: Get the scaling of spike and analog channels -// Inputs: nChan - the 1 based channel of interest -// anSpikeScale - pointer to array to store spike scaling -// anContScale - pointer to array to store continuous scaling -// anLncScale - pointer to array to store LNC scaling -cbRESULT cbGetScaling(const uint32_t nChan, - uint32_t *anSpikeScale, - uint32_t *anContScale, - uint32_t *anLncScale, - const uint32_t nInstance) -{ - cbSCALING isScaling; - int i; - - if (!IsChanAnalogIn(nChan)) - return cbRESULT_INVALIDCHANNEL; - - cbRESULT retVal = ::cbGetAinpScaling(nChan, &isScaling, nInstance); - if (retVal != cbRESULT_OK) - return retVal; - - const int32_t nAnaMax = isScaling.anamax / isScaling.anagain; - - if (IsChanFEAnalogIn(nChan)) - { - if (anContScale) - { - for (i = 0; i < SCALE_CONTINUOUS_COUNT; ++i) - anContScale[SCALE_CONTINUOUS_COUNT - i - 1] = static_cast(nAnaMax * exp(-0.3467 * i)); - } - - if (anLncScale) - { - for (i = 0; i < SCALE_LNC_COUNT; ++i) - anLncScale[SCALE_LNC_COUNT - i - 1] = static_cast(nAnaMax * exp(-0.3467 * i)); - } - - if (anSpikeScale) - { - for (i = 0; i < SCALE_SPIKE_COUNT; ++i) - anSpikeScale[SCALE_SPIKE_COUNT - i - 1] = static_cast(0.25 * nAnaMax * exp(-0.3467 / 2.0 * i)); - - anSpikeScale[SCALE_SPIKE_COUNT - 1] = nAnaMax; - anSpikeScale[SCALE_SPIKE_COUNT - 2] = nAnaMax / 2; - anSpikeScale[SCALE_SPIKE_COUNT - 3] = nAnaMax / 4; - } - } - else - { - if (anContScale) - for (i = 0; i < SCALE_CONTINUOUS_COUNT; ++i) - anContScale[SCALE_CONTINUOUS_COUNT - i - 1] = static_cast(nAnaMax * exp(-0.3467 * i)); - - if (anLncScale) - for (i = 0; i < SCALE_LNC_COUNT; ++i) - anLncScale[SCALE_LNC_COUNT - i - 1] = static_cast(nAnaMax * exp(-0.3467 * i)); - - if (anSpikeScale) - for (i = 0; i < SCALE_SPIKE_COUNT; ++i) - anSpikeScale[SCALE_SPIKE_COUNT - i - 1] = static_cast(nAnaMax * exp(-0.3467 * i / 2.0)); - - } - return cbRESULT_OK; -} - - -////////////////////////////// acquisition groups //////////////////////////////////////////////// - -// These will be ordered by group, so I don't need that -struct cbAcqSettings -{ - uint32_t nFilter; // Which filter - uint32_t nSampleGroup; // Which sample group -}; -const cbAcqSettings isAcqData[ACQ_GROUP_COUNT] = -{ - // Filter (Low pass) Sample Group - {6, /* don't care (was 1)*/ 0 /* not sampling */}, // match "startup" - {8, /* 150 Hz Low (was 5)*/ 2 /* 1 kS/sec */ }, - {6, /* 250 Hz Low (was 1)*/ 2 /* 1 kS/sec */ }, - {9, /* 10-250 Hz Band (was 4)*/ 2 /* 1 kS/sec */ }, - {7, /* 500 Hz Low (was 2)*/ 3 /* 2 kS/sec */ }, - {10, /* 2.5 kHz Low (was 6)*/ 4 /* 10 kS/sec */ }, - {0, /* NONE - HW only (7.5 kHz) */ 5 /* 30 kS/sec */ }, - {2, /* 250 Hz High (was 3)*/ 5 /* 30 kS/sec */ }, - {8, 5 }, -}; - - - -// Author & Date: Ehsan Azar 28 May 2009 -// Purpose: tell which sampling group this channel is part of -// Inputs: -// nChan - the 1 based channel of interest -// Outpus: -// 0 means SMPGRP_NONE -uint32_t cbGetSmpGroup(const uint32_t nChan, const uint32_t nInstance) -{ - uint32_t dwGroup; - ::cbGetAinpSampling(nChan, nullptr, &dwGroup, nInstance); - return dwGroup; -} - -// Author & Date: Ehsan Azar 28 May 2009 -// Purpose: tell me how many sampling groups exist -// this number will always be larger than 1 because group 0 (empty) -// will always exist -uint32_t cbGetSmpGroupCount() -{ - return SMP_GROUP_COUNT; -} - - -// Author & Date: Kirk Korver 02 Nov 2005 -// Purpose: tell which acquisition group this channel is part of -// See SetAcqGroup -// Inputs: -// nChan - the 1 based channel of interest -// Outpus: -// 0 means not part of any acquisition group; 1+ means part of that group -// Note: Do not use this function to find the sampling rate, -// use cbGetSmpGroup instead. -uint32_t cbGetAcqGroup(const uint32_t nChan, const uint32_t nInstance) -{ - uint32_t nFilter = 0; - uint32_t nSG = 0; - ::cbGetAinpSampling(nChan, &nFilter, &nSG, nInstance); - - for (const cbAcqSettings * pTest = isAcqData; pTest != ARRAY_END(isAcqData); ++pTest) - { - if (pTest->nFilter == nFilter && - pTest->nSampleGroup == nSG) - { - return static_cast(pTest - &isAcqData[0]); - } - } - - // If I got here then there was no match. I had better make it "not sample" - // and return that. - cbSetAcqGroup(nChan, 0, nInstance); - return 0; -} - -// Author & Date: Kirk Korver 02 Nov 2005 -// Purpose: tell which acquisition group this channel is part of -// See SetAcqGroup -// Inputs: -// nChan - the 1 based channel of interest -// nGroup - the group to now belong to: 0 = not in a grup; 1+, a member of that group -// Outpus: -// 0 means not part of any acquisition group; 1+ means part of that group -cbRESULT cbSetAcqGroup(const uint32_t nChan, const uint32_t nGroup, const uint32_t nInstance) -{ - if (nGroup >= ACQ_GROUP_COUNT) - return cbRESULT_INVALIDFUNCTION; - - const uint32_t nFilter = isAcqData[nGroup].nFilter; - const uint32_t nSG = isAcqData[nGroup].nSampleGroup; - const cbRESULT nRet = ::cbSetAinpSampling(nChan, nFilter, nSG, nInstance); - return nRet; -} - - -// Purpose: tell me how many acquisition groups exist -// this number will always be larger than 1 because group 0 (empty) -// will always exist -uint32_t cbGetAcqGroupCount() -{ - return ACQ_GROUP_COUNT; -} - -const char * asContFilter[ACQ_GROUP_COUNT] = -{ - "", - "0.3 Hz - 150 Hz - 1 kS/s", - "0.3 Hz - 250 Hz - 1 kS/s", - "10 Hz - 250 Hz - 1 kS/s", - "0.3 Hz - 500 Hz - 2 kS/s", - "0.3 Hz - 2.5 kHz - 10 kS/s", - "0.3 Hz - 7.5 kHz - 30 kS/s", - "250 Hz - 7.5 kHz - 30 kS/s", - "0.3 Hz - 150 Hz - 30 kS/s", -}; - - -// Author & Date: Hyrum L. Sessions 25 May 2007 -// Purpose: get a textual description of the acquisition group -// Inputs: nGroup - group in question -// Outputs: pointer to the description of the group -const char * cbGetAcqGroupDesc(const uint32_t nGroup) -{ - return asContFilter[nGroup < ACQ_GROUP_COUNT ? nGroup : 0]; -} - -// Author & Date: Hyrum L. Sessions 29 May 2007 -// Purpose: get the filter used by an acquision group -// Inputs: nGroup - group in question -// Outputs: filter value -uint32_t cbGetAcqGroupFilter(const uint32_t nGroup) -{ - return isAcqData[nGroup < ACQ_GROUP_COUNT ? nGroup : 0].nFilter; -} - -// Author & Date: Hyrum L. Sessions 29 May 2007 -// Purpose: get the sample group used by an acquision group -// Inputs: nGroup - group in question -// Outputs: sample group value -uint32_t cbGetAcqGroupSampling(const uint32_t nGroup) -{ - return isAcqData[nGroup < ACQ_GROUP_COUNT ? nGroup : 0].nSampleGroup; -} - -// Purpose: get the number of units for this channel -// Inputs: nChan - 1 based channel of interest -// Returns: number of valid units for this channel -uint32_t cbGetChanUnits(const uint32_t nChan, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - ASSERT(nChan >= MIN_CHANS); - ASSERT(nChan <= cbMAXCHANS); - - uint32_t nValidUnits = 0; - - // let's start at 1 so we don't get the noise unit which is 0 - for (uint32_t nUnit = 1; nUnit < cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); ++nUnit) - { - if (cb_cfg_buffer_ptr[nIdx]->isSortingOptions.asSortModel[nChan - 1][nUnit].valid) - ++nValidUnits; - } - return nValidUnits; -} - -// Purpose: tells if the unit in the channel is valid -// Inputs: nChan - 1 based channel of interest -// nUnit - 1 based unit of interest (0 is noise unit) -// Returns: 1 if the unit in the channel is valid, 0 is otherwise -uint32_t cbIsChanUnitValid(const uint32_t nChan, const int32_t nUnit, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - bool bValid = false; - cbMANUALUNITMAPPING isUnitMapping[cbMAXUNITS]; - - ASSERT(nChan >= MIN_CHANS); - ASSERT(nChan <= cbMAXCHANS); - ASSERT(nUnit <= cbMAXUNITS); - - // get the override array for the requested channel - cbGetChanUnitMapping(nChan, &isUnitMapping[0], nInstance); - - // find a translated unit that points to this unit - for (const auto & i : isUnitMapping) - { - if (nUnit == i.nOverride + 1) // zero based - if ((cb_cfg_buffer_ptr[nIdx]->isSortingOptions.asSortModel[nChan - 1][i.nOverride + 1].valid) || - (i.bValid)) - { - bValid = true; - break; - } - } - return bValid; -} - -// Author & Date: Ehsan Azar 18 Nov 2010 -// Purpose: Set the noise boundary parameters (compatibility for int16_t) -// Inputs: -// chanIdx - channel number (1-based) -// anCentroid - the center of an ellipsoid -// anMajor - major axis of the ellipsoid -// anMinor_1 - first minor axis of the ellipsoid -// anMinor_2 - second minor axis of the ellipsoid -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSSSetNoiseBoundary( - const uint32_t chanIdx, const int16_t anCentroid[3], - const int16_t anMajor[3], const int16_t anMinor_1[3], const int16_t anMinor_2[3], const uint32_t nInstance -) -{ - float afCentroid[3], afMajor[3], afMinor_1[3], afMinor_2[3]; - for (int i = 0; i < 3; ++i) { - afCentroid[i] = anCentroid[i]; - afMajor[i] = anMajor[i]; - afMinor_1[i] = anMinor_1[i]; - afMinor_2[i] = anMinor_2[i]; - } - return cbSSSetNoiseBoundary(chanIdx, afCentroid, afMajor, afMinor_1, afMinor_2, nInstance); -} - -// Author & Date: Ehsan Azar 19 Nov 2010 -// Purpose: Get NTrode unitmapping for a particular site -// Inputs: -// ntrode - NTrode number (1-based) -// nSite - the site within NTrode (1 to cbMAXSITEPLOTS) -// Outputs: -// unitmapping - the unitmapping for the given site in given NTrode -// cbRESULT_OK if life is good -cbRESULT cbGetNTrodeUnitMapping(const uint32_t ntrode, const uint16_t nSite, cbMANUALUNITMAPPING *unitmapping, uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the NTrode number is valid and initialized - if ((ntrode - 1) >= cbMAXNTRODES) return cbRESULT_INVALIDNTRODE; - if (nSite >= cbMAXSITEPLOTS) return cbRESULT_INVALIDFUNCTION; - if (cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].cbpkt_header.chid==0) return cbRESULT_INVALIDNTRODE; - - // Return the requested data from the rec buffer - if (unitmapping) memcpy(unitmapping,&cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].ellipses[nSite][0],cbMAXUNITS*sizeof(cbMANUALUNITMAPPING)); - return cbRESULT_OK; -} - -// Author & Date: Ehsan Azar 19 Nov 2010 -// Purpose: Set NTrode unitmapping of a particular site -// Inputs: -// ntrode - NTrode number (1-based) -// nSite - the site within NTrode (1 to cbMAXSITEPLOTS) -// unitmapping - the unitmapping for the given site in given NTrode -// fs - if valid, set as new NTrode feature space otherwise leave unchanged -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSetNTrodeUnitMapping(const uint32_t ntrode, const uint16_t nSite, const cbMANUALUNITMAPPING *unitmapping, int16_t fs, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the NTrode number is valid and initialized - if ((ntrode - 1) >= cbMAXNTRODES) return cbRESULT_INVALIDNTRODE; - if (cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDNTRODE; - if (nSite >= cbMAXSITEPLOTS) return cbRESULT_INVALIDFUNCTION; - if (fs < 0) - fs = static_cast(cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].fs); // previous feature space - cbMANUALUNITMAPPING ellipses[cbMAXSITEPLOTS][cbMAXUNITS]; // for the mapping structure called in from the NSP - char label[cbLEN_STR_LABEL]; - // Get previous ellipses - cbGetNTrodeInfo(ntrode, label, ellipses, nullptr, nullptr, nullptr, nInstance); - - if (unitmapping) memcpy(&ellipses[nSite][0], unitmapping, cbMAXUNITS * sizeof(cbMANUALUNITMAPPING)); - cbSetNTrodeInfo(ntrode, label, ellipses, fs, nInstance); - return cbRESULT_OK; -} - -// Author & Date: Ehsan Azar 19 Nov 2010 -// Purpose: Set NTrode feature space if changed, keeping the rest of NTrode information intact -// Inputs: -// ntrode - NTrode number (1-based) -// fs - set as new NTrode feature space -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSetNTrodeFeatureSpace(const uint32_t ntrode, const int16_t fs, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the NTrode number is valid and initialized - if ((ntrode - 1) >= cbMAXNTRODES) return cbRESULT_INVALIDNTRODE; - if (cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDNTRODE; - // If feature space has changed - if (fs != cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].fs) - return cbSetNTrodeUnitMapping(ntrode, 0, nullptr, fs, nInstance); - return cbRESULT_OK; -} - -uint32_t cbGetNTrodeInstrument(const uint32_t nNTrode, const uint32_t nInstance) -{ - uint32_t nInstrument = cbNSP1; // add to chan for instruments > 0 - const uint32_t nIdx = cb_library_index[nInstance]; -#ifndef CBPROTO_311 - if (cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[nNTrode - 1].cbpkt_header.instrument < cbMAXPROCS) - nInstrument = cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[nNTrode - 1].cbpkt_header.instrument + 1; -#endif - ASSERT(nInstrument - 1 < cbMAXPROCS); - - return nInstrument; -} - -// Author & Date: Ehsan Azar 6 Nov 2012 -// Purpose: Find if file recording is active -// Outputs: -// returns if file is being recorded -bool IsFileRecording(const uint32_t nInstance) -{ - cbPKT_FILECFG filecfg; - if (cbGetFileInfo(&filecfg, nInstance) == cbRESULT_OK) - { - if (filecfg.options == cbFILECFG_OPT_REC) - return true; - } - return false; -} - -uint32_t GetAIAnalogInChanNumber(uint32_t nOrdinal, const uint32_t nInstance) -{ - uint32_t nReturn = 0; - const uint32_t nMaxChan = cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); - - if (1 <= nOrdinal) - { - for (uint32_t nChan = MIN_CHANS; nChan <= nMaxChan; ++nChan) - { - if (IsChanAIAnalogIn(nChan, nInstance) && (0 == --nOrdinal)) - { - nReturn = nChan; - break; - } - } - } - return nReturn; -} - -uint32_t GetAnalogOutChanNumber(uint32_t nOrdinal, const uint32_t nInstance) -{ - uint32_t nReturn = 0; - const uint32_t nMaxChan = cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); - - if (1 <= nOrdinal) - { - for (uint32_t nChan = MIN_CHANS; nChan <= nMaxChan; ++nChan) - { - if (IsChanAnalogOut(nChan, nInstance) && (0 == --nOrdinal)) - { - nReturn = nChan; - break; - } - } - } - return nReturn; -} - -uint32_t GetAudioOutChanNumber(uint32_t nOrdinal, const uint32_t nInstance) -{ - uint32_t nReturn = 0; - const uint32_t nMaxChan = cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); - - if (1 <= nOrdinal) - { - for (uint32_t nChan = MIN_CHANS; nChan <= nMaxChan; ++nChan) - { - if (IsChanAudioOut(nChan, nInstance) && (0 == --nOrdinal)) - { - nReturn = nChan; - break; - } - } - } - return nReturn; -} - -uint32_t GetAnalogOrAudioOutChanNumber(uint32_t nOrdinal, const uint32_t nInstance) -{ - uint32_t nReturn = 0; - const uint32_t nMaxChan = cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); - - if (1 <= nOrdinal) - { - for (uint32_t nChan = MIN_CHANS; nChan <= nMaxChan; ++nChan) - { - if ((IsChanAnalogOut(nChan, nInstance) || IsChanAudioOut(nChan, nInstance)) && - (0 == --nOrdinal)) - { - nReturn = nChan; - break; - } - } - } - return nReturn; -} - -uint32_t GetDiginChanNumber(uint32_t nOrdinal, const uint32_t nInstance) -{ - uint32_t nReturn = 0; - const uint32_t nMaxChan = cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); - - if (1 <= nOrdinal) - { - for (uint32_t nChan = MIN_CHANS; nChan <= nMaxChan; ++nChan) - { - if (IsChanDigin(nChan, nInstance) && (0 == --nOrdinal)) - { - nReturn = nChan; - break; - } - } - } - return nReturn; -} - -uint32_t GetSerialChanNumber(uint32_t nOrdinal, const uint32_t nInstance) -{ - uint32_t nReturn = 0; - const uint32_t nMaxChan = cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); - - if (1 <= nOrdinal) - { - for (uint32_t nChan = MIN_CHANS; nChan <= nMaxChan; ++nChan) - { - if (IsChanSerial(nChan, nInstance) && (0 == --nOrdinal)) - { - nReturn = nChan; - break; - } - } - } - return nReturn; -} - -uint32_t GetDigoutChanNumber(uint32_t nOrdinal, const uint32_t nInstance) -{ - uint32_t nReturn = 0; - const uint32_t nMaxChan = cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); - - if (1 <= nOrdinal) - { - for (uint32_t nChan = MIN_CHANS; nChan <= nMaxChan; ++nChan) - { - if (IsChanDigout(nChan, nInstance) && (0 == --nOrdinal)) - { - nReturn = nChan; - break; - } - } - } - return nReturn; -} - -uint32_t cbGetNumActiveInstruments() -{ - uint32_t nNumActiveInstruments = 0; - - for (int nProc = 0; nProc < cbMAXPROCS; ++nProc) - if (NSP_FOUND == cbGetNspStatus(nProc + 1)) - ++nNumActiveInstruments; - - return nNumActiveInstruments; -} - - -NSP_STATUS cbGetNspStatus(const uint32_t nInstrument) -{ - if (nInstrument > cbMAXPROCS) - return NSP_INVALID; - return cb_pc_status_buffer_ptr[0]->cbGetNspStatus(nInstrument - 1); -} - -#ifndef CBPROTO_311 -void cbSetNspStatus(const uint32_t nInstrument, const NSP_STATUS nStatus) -{ - ASSERT(nInstrument - 1 < cbMAXPROCS); - if (nInstrument - 1 < cbMAXPROCS) - cb_pc_status_buffer_ptr[0]->cbSetNspStatus(nInstrument - 1, nStatus); -} -#endif - -uint32_t cbGetExpandedChannelNumber(const uint32_t nInstrument, const uint32_t nChannel) -{ - uint32_t nChanAdd = 0; // add to chan for instruments > 0 - cbPROCINFO isProcInfo; - - if ((nChannel - 1) >= cbMAXCHANS) - return 0; - - ASSERT(nInstrument - 1 < cbMAXPROCS); - if (nInstrument > cbMAXPROCS) - return nChannel; - - for (UINT nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if (nInstrument == nProc) - return nChannel + nChanAdd; - - memset(&isProcInfo, 0, sizeof(cbPROCINFO)); - ::cbGetProcInfo(nProc, &isProcInfo); - nChanAdd += isProcInfo.chancount; - } - return nChannel; -} - - -uint32_t cbGetChanInstrument(const uint32_t nChannel, const uint32_t nInstance) -{ - uint32_t nInstrument = cbNSP1; // add to chan for instruments > 0 - const uint32_t nIdx = cb_library_index[nInstance]; - - if ((nChannel - 1) >= cbMAXCHANS) - return 0; - -#ifndef CBPROTO_311 - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[nChannel - 1].cbpkt_header.instrument < cbMAXPROCS) - nInstrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[nChannel - 1].cbpkt_header.instrument + 1; -#endif - ASSERT(nInstrument - 1 < cbMAXPROCS); - - return nInstrument; -} - -/// @author Hyrum L. Sessions -/// @date 14 April 2021 -/// @brief Get the channel number that is local to the instrument -/// e.g. channel 285 is the first channel on instrument two so return 1 -uint32_t cbGetInstrumentLocalChannelNumber(const uint32_t nChan) -{ - // Test that the channel address is valid and initialized - if ((nChan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[0]->chaninfo[nChan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - return cb_cfg_buffer_ptr[0]->chaninfo[nChan - 1].chan; -} diff --git a/src/cbhwlib/cbHwlibHi.h b/src/cbhwlib/cbHwlibHi.h deleted file mode 100755 index 6e370485..00000000 --- a/src/cbhwlib/cbHwlibHi.h +++ /dev/null @@ -1,339 +0,0 @@ -/* =STS=> cbHwlibHi.h[1689].aa11 submit SMID:13 */ -////////////////////////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003-2008 Cyberkinetics, Inc. -// (c) Copyright 2008-2021 Blackrock Microsystems -// -// $Workfile: cbHwlibHi.h $ -// $Archive: /Cerebus/WindowsApps/cbhwlib/cbHwlibHi.h $ -// $Revision: 10 $ -// $Date: 2/11/04 1:49p $ -// $Author: Kkorver $ -// -// $NoKeywords: $ -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#ifndef CBHWLIBHI_H_INCLUDED -#define CBHWLIBHI_H_INCLUDED - -#if _MSC_VER > 1000 -#pragma once -#endif // _MSC_VER > 1000 - -#include "../include/cerelink/cbhwlib.h" - -#define MIN_CHANS 1 -#define MAX_CHANS_ARRAY 96 // In neuroport there are only 96 channels - -#define MAX_CHANS_FRONT_END cbNUM_FE_CHANS -#define MIN_CHANS_ANALOG_IN (MAX_CHANS_FRONT_END + 1) -#define MAX_CHANS_ANALOG_IN (MAX_CHANS_FRONT_END + cbNUM_ANAIN_CHANS) -#define MIN_CHANS_ANALOG_OUT (MAX_CHANS_ANALOG_IN + 1) -#define MAX_CHANS_ANALOG_OUT (MAX_CHANS_ANALOG_IN + cbNUM_ANAOUT_CHANS) -#define MIN_CHANS_AUDIO (MAX_CHANS_ANALOG_OUT + 1) -#define MAX_CHANS_AUDIO (MAX_CHANS_ANALOG_OUT + cbNUM_AUDOUT_CHANS) -#define MIN_CHANS_DIGITAL_IN (MAX_CHANS_AUDIO + 1) -#define MAX_CHANS_DIGITAL_IN (MAX_CHANS_AUDIO + cbNUM_DIGIN_CHANS) -#define MIN_CHANS_SERIAL (MAX_CHANS_DIGITAL_IN + 1) -#define MAX_CHANS_SERIAL (MAX_CHANS_DIGITAL_IN + cbNUM_SERIAL_CHANS) -#define MIN_CHANS_DIGITAL_OUT (MAX_CHANS_SERIAL + 1) -#define MAX_CHANS_DIGITAL_OUT (MAX_CHANS_SERIAL + cbNUM_DIGOUT_CHANS) - -// Currently we have 8 acquisition groups: -// Sampling Filter -enum { ACQGRP_NONE, // not sampled - ACQGRP_1KS_EEG, // 1 kS/s EEG/ECG/EOG/ERG - ACQGRP_1KS, // 1 kS/s LFP Wide - ACQGRP_1KS_SPIKE_MED, // 1 kS/s Spike Medium - ACQGRP_2KS, // 2 kS/s LFP XWide - ACQGRP_10KS, // 10 kS/s Activity - ACQGRP_30KS, // 30 kS/s Unfiltered - ACQGRP_30KS_EMG, // 30 kS/s EMG - ACQGRP_30KS_EEG, // 30 kS/s EEG - ACQ_GROUP_COUNT -}; - -// Currently we have 5 sampling rates: -// Sampling -enum { SMPGRP_NONE, // not sampled - SMPGRP_500S, // 500 S/s - SMPGRP_1KS, // 1 kS/s - SMPGRP_2KS, // 2 kS/s - SMPGRP_10KS, // 10 kS/s - SMPGRP_30KS, // 30 kS/s - SMPGRP_RAW, // Raw which is 30 kS/s - SMP_GROUP_COUNT -}; - - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -bool IsSimRunning(uint32_t nInstance = 0); // TRUE means that CentralSim is being used; FALSE, not - -bool IsSpikeProcessingEnabled(uint32_t nChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsContinuousProcessingEnabled(uint32_t nChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsRawProcessingEnabled(uint32_t nChan, uint32_t nInstance = 0); // true means yes -// Is this channel enabled? -// This will figure out what kind of channel, and then find out if it is enabled -bool IsChannelEnabled(uint32_t nChannel, uint32_t nInstance = 0); - -// Use these with care. make sure you know what kind of channel you have -bool IsAnalogInEnabled(uint32_t nChannel); -bool IsAudioEnabled(uint32_t nChannel); -bool IsAnalogOutEnabled(uint32_t nChannel); -bool IsDigitalInEnabled(uint32_t nChannel); -bool IsSerialEnabled(uint32_t nChannel, uint32_t nInstance = 0); -bool IsDigitalOutEnabled(uint32_t nChannel, uint32_t nInstance = 0); - -// Is it "this" kind of channel? (very fast) -bool IsChanAnalogIn(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanFEAnalogIn(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanAIAnalogIn(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanAnyDigIn(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanSerial(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanDigin(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanDigout(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanAnalogOut(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanAudioOut(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanCont(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no - -bool AreHoopsDefined(uint32_t nChannel, uint32_t nInstance = 0); -bool AreHoopsDefined(uint32_t nChannel, uint32_t nUnit, uint32_t nInstance = 0); - -// Get channel number from ordinal -/// @author Hyrum L. Sessions -/// @date 24 Feb 2017 -/// @brief Get the channel number of the specified ordinal analog input channel -/// e.g. on a 128 channel system, the 1st digin channel is 151 -/// -/// @param [in] nOrdinal 1 based ordinal digin to find -/// @param [in] nInstance Instance number of the library -/// @return Channel number of the ordinal digin, 0 if none exist or invalid ordinal -uint32_t GetAIAnalogInChanNumber(uint32_t nOrdinal, uint32_t nInstance = 0); - -/// @author Hyrum L. Sessions -/// @date 24 Feb 2017 -/// @brief Get the channel number of the specified ordinal digin channel -/// e.g. on a 128 channel system, the 1st digin channel is 151 -/// -/// @param [in] nOrdinal 1 based ordinal digin to find -/// @param [in] nInstance Instance number of the library -/// @return Channel number of the ordinal digin, 0 if none exist or invalid ordinal -uint32_t GetAnalogOutChanNumber(uint32_t nOrdinal, uint32_t nInstance = 0); - -/// @author Hyrum L. Sessions -/// @date 24 Feb 2017 -/// @brief Get the channel number of the specified ordinal digin channel -/// e.g. on a 128 channel system, the 1st digin channel is 151 -/// -/// @param [in] nOrdinal 1 based ordinal digin to find -/// @param [in] nInstance Instance number of the library -/// @return Channel number of the ordinal digin, 0 if none exist or invalid ordinal -uint32_t GetAudioOutChanNumber(uint32_t nOrdinal, uint32_t nInstance = 0); - -/// @author Hyrum L. Sessions -/// @date 27 Feb 2017 -/// @brief Get the channel number of the specified ordinal analog or audio channel -/// e.g. on a 128 channel system, the 1st digin channel is 145 -/// -/// @param [in] nOrdinal 1 based ordinal digin to find -/// @param [in] nInstance Instance number of the library -/// @return Channel number of the ordinal digin, 0 if none exist or invalid ordinal -uint32_t GetAnalogOrAudioOutChanNumber(uint32_t nOrdinal, uint32_t nInstance = 0); - -/// @author Hyrum L. Sessions -/// @date 24 Feb 2017 -/// @brief Get the channel number of the specified ordinal digin channel -/// e.g. on a 128 channel system, the 1st digin channel is 151 -/// -/// @param [in] nOrdinal 1 based ordinal digin to find -/// @param [in] nInstance Instance number of the library -/// @return Channel number of the ordinal digin, 0 if none exist or invalid ordinal -uint32_t GetDiginChanNumber(uint32_t nOrdinal, uint32_t nInstance = 0); - -/// @author Hyrum L. Sessions -/// @date 24 Feb 2017 -/// @brief Get the channel number of the specified ordinal digin channel -/// e.g. on a 128 channel system, the 1st digin channel is 151 -/// -/// @param [in] nOrdinal 1 based ordinal digin to find -/// @param [in] nInstance Instance number of the library -/// @return Channel number of the ordinal digin, 0 if none exist or invalid ordinal -uint32_t GetSerialChanNumber(uint32_t nOrdinal, uint32_t nInstance = 0); - -/// @author Hyrum L. Sessions -/// @date 24 Feb 2017 -/// @brief Get the channel number of the specified ordinal digin channel -/// e.g. on a 128 channel system, the 1st digin channel is 151 -/// -/// @param [in] nOrdinal 1 based ordinal digin to find -/// @param [in] nInstance Instance number of the library -/// @return Channel number of the ordinal digin, 0 if none exist or invalid ordinal -uint32_t GetDigoutChanNumber(uint32_t nOrdinal, uint32_t nInstance = 0); - -uint32_t cbGetNumActiveInstruments(); -NSP_STATUS cbGetNspStatus(uint32_t nInstrument); -#ifndef CBPROTO_311 -void cbSetNspStatus(uint32_t nInstrument, NSP_STATUS nStatus); -#endif -uint32_t cbGetExpandedChannelNumber(uint32_t nInstrument, uint32_t nChannel); -uint32_t cbGetChanInstrument(uint32_t nChannel, uint32_t nInstance = 0); -uint32_t cbGetInstrumentLocalChannelNumber(uint32_t nChan); - -// Author & Date: Ehsan Azar June 3, 2009 -// Purpose: determine if a channel has valid sorting unit -// Input: nChannel = channel to track 1 - based -// dwUnit the unit number (1=< dwUnit <=cbMAXUNITS) -bool cbHasValidUnit(uint32_t dwChan, uint32_t dwUnit, uint32_t nInstance = 0); // TRUE means yes; FALSE, no - -// Author & Date: Ehsan Azar June 2, 2009 -// Purpose: Return TRUE if given channel has a sorting algorithm -// If cbAINPSPK_ALLSORT is passed it returns TRUE if there is any sorting -// If cbAINPSPK_NOSORT is passed it returns TRUE if there is no sorting -// Input: spkSrtOpt = Sorting methods: -// cbAINPSPK_HOOPSORT, cbAINPSPK_SPREADSORT, -// cbAINPSPK_CORRSORT, cbAINPSPK_PEAKMAJSORT, -// cbAINPSPK_PEAKFISHSORT, cbAINPSPK_PCAMANSORT, -// cbAINPSPK_PCAKMEANSORT, cbAINPSPK_PCAEMSORT, -// cbAINPSPK_PCADBSORT, cbAINPSPK_NOSORT, -// cbAINPSPK_OLDAUTOSORT, cbAINPSPK_ALLSORT -bool cbHasSpikeSorting(uint32_t dwChan, uint32_t spkSrtOpt, uint32_t nInstance = 0); - -// Author & Date: Ehsan Azar June 2, 2009 -// Purpose: Set a sorting algorithm for a channel -// Input: spkSrtOpt = Sorting methods: -// cbAINPSPK_HOOPSORT, cbAINPSPK_SPREADSORT, -// cbAINPSPK_CORRSORT, cbAINPSPK_PEAKMAJSORT, -// cbAINPSPK_PEAKFISHSORT, cbAINPSPK_PCAMANSORT, -// cbAINPSPK_PCAKMEANSORT, cbAINPSPK_PCAEMSORT, -// cbAINPSPK_PCADBSORT, cbAINPSPK_NOSORT -cbRESULT cbSetSpikeSorting(uint32_t dwChan, uint32_t spkSrtOpt, uint32_t nInstance = 0); - - -// Other functions -void TrackChannel(uint32_t nChannel, uint32_t nInstance = 0); - - -// Purpose: update the analog input scaling. If there is an Analog Output -// channel which is monitoring this channel, then update the output -// scaling to match the displayed scaling -// Inputs: -// chan - the analog input channel being altered (1 based) -// smpdispmax - the maximum digital value of the continuous display -// spkdispmax - the maximum digital value of the spike display -// lncdispmax - the maximum digital value of the LNC display -cbRESULT cbSetAnaInOutDisplay(uint32_t chan, int32_t smpdispmax, int32_t spkdispmax, int32_t lncdispmax, uint32_t nInstance = 0); - -// Purpose: transfrom a digital value to an analog value -// Inputs: -// nChan - the 1 based channel of interest -// nDigital - the digital value -// Outputs: -// the corresponding analog value -inline int32_t cbXfmDigToAna(uint32_t nChan, int32_t nDigital, uint32_t nInstance = 0) -{ - int32_t nVal = nDigital; - cbSCALING isScaling; - ::cbGetAinpScaling(nChan, &isScaling, nInstance); - - if (0 != isScaling.anagain) - nVal = nDigital * (isScaling.anamax / isScaling.anagain) / isScaling.digmax; - return nVal; -} - -// Purpose: transform an analog value to a digital value -// Inputs: -// nChan - the 1 based channel of interest -// nAnalog - the analog value -// Outputs: -// the corresponding digital value -inline int32_t cbXfmAnaToDig(uint32_t nChan, int32_t nAnalog, uint32_t nInstance = 0) -{ - cbSCALING isScaling; - ::cbGetAinpScaling(nChan, &isScaling, nInstance); - - int32_t nVal = nAnalog * isScaling.digmax / (isScaling.anamax / isScaling.anagain); - return nVal; -} - -// Author & Date: Hyrum L. Sessions 25 Jan 2006 -// Purpose: Get the scaling of spike and analog channels -// Inputs: nChan - the 1 based channel of interest -// anSpikeScale - pointer to array to store spike scaling -// anContScale - pointer to array to store continuous scaling -// anLncScale - pointer to array to store LNC scaling -cbRESULT cbGetScaling(uint32_t nChan, - uint32_t *anSpikeScale, - uint32_t *anContScale, - uint32_t *anLncScale, - uint32_t nInstance = 0); - -// Purpose: tell which acquisition group this channel is part of -// Inputs: -// nChan - the 1 based channel of interest -// Outpus: -// 0 means not part of any acquisition group; 1+ means part of that group -uint32_t cbGetAcqGroup(uint32_t nChan, uint32_t nInstance = 0); -cbRESULT cbSetAcqGroup(uint32_t nChan, uint32_t nGroup, uint32_t nInstance = 0); - -// Author & Date: Ehsan Azar 28 May 2009 -// Purpose: tell which sampling group this channel is part of -// Inputs: -// nChan - the 1 based channel of interest -// Outpus: -// 0 means SMPGRP_NONEuint32_t -uint32_t cbGetSmpGroup(uint32_t nChan, uint32_t nInstance = 0); - -// Author & Date: Ehsan Azar 28 May 2009 -// Purpose: tell me how many sampling groups exist -// this number will always be larger than 1 because group 0 (empty) -// will always exist -uint32_t cbGetSmpGroupCount(); - -// Purpose: tell me how many acquisition groups exist -// this number will always be larger than 1 because group 0 (empty) -// will always exist -uint32_t cbGetAcqGroupCount(); - -// Purpose: get a textual description of the acquisition group -// Inputs: nGroup - group in question -// Outputs: pointer to the description of the group -const char * cbGetAcqGroupDesc(uint32_t nGroup); - -// Purpose: get the individual components (filter & sample group) -// Inputs: nGroup - group in question -// Outputs: component value -uint32_t cbGetAcqGroupFilter(uint32_t nGroup); -uint32_t cbGetAcqGroupSampling(uint32_t nGroup); - -// Purpose: get the number of units for this channel -// Inputs: nChan - 1 based channel of interest -// Returns: number of valid units for this channel -uint32_t cbGetChanUnits(uint32_t nChan, uint32_t nInstance = 0); - -// Purpose: tells if the unit in the channel is valid -// Inputs: nChan - 1 based channel of interest -// nUnit - 1 based unit of interest (0 is noise unit) -// Returns: 1 if the unit in the channel is valid, 0 is otherwise -uint32_t cbIsChanUnitValid(uint32_t nChan, int32_t nUnit, uint32_t nInstance = 0); - -// Purpose: Set the noise boundary parameters (compatibility for int16_t) -cbRESULT cbSSSetNoiseBoundary(uint32_t chanIdx, const int16_t anCentroid[3], const int16_t anMajor[3], const int16_t anMinor_1[3], const int16_t anMinor_2[3], uint32_t nInstance = 0); - -// Purpose: Get NTrode unitmapping for a particular site -cbRESULT cbGetNTrodeUnitMapping(uint32_t ntrode, uint16_t nSite, cbMANUALUNITMAPPING *unitmapping, uint32_t nInstance = 0); - -// Purpose: Set NTrode unitmapping of a particular site -cbRESULT cbSetNTrodeUnitMapping(uint32_t ntrode, uint16_t nSite, const cbMANUALUNITMAPPING *unitmapping, int16_t fs = -1, uint32_t nInstance = 0); - -// Purpose: Set NTrode feature space, keeping the rest of NTrode information intact -cbRESULT cbSetNTrodeFeatureSpace(uint32_t ntrode, int16_t fs, uint32_t nInstance = 0); - -// Purpose: Get the instrument this NTrode belongs to -uint32_t cbGetNTrodeInstrument(uint32_t nNTrode, uint32_t nInstance = 0); - -// Returns: returns if file is being recorded -bool IsFileRecording(uint32_t nInstance = 0); - -#endif // include guards diff --git a/src/cbhwlib/cbhwlib.cpp b/src/cbhwlib/cbhwlib.cpp deleted file mode 100644 index e8ee8770..00000000 --- a/src/cbhwlib/cbhwlib.cpp +++ /dev/null @@ -1,3761 +0,0 @@ -// =STS=> cbhwlib.cpp[2730].aa14 open SMID:15 -////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Cerebus Interface Library -// -// Copyright (C) 2001-2003 Bionic Technologies, Inc. -// (c) Copyright 2003-2008 Cyberkinetics, Inc -// (c) Copyright 2008-2021 Blackrock Microsystems -// -// Developed by Shane Guillory and Angela Wang -// -// $Workfile: cbhwlib.cpp $ -// $Archive: /Cerebus/Human/WindowsApps/cbhwlib/cbhwlib.cpp $ -// $Revision: 24 $ -// $Date: 4/26/05 2:56p $ -// $Author: Kkorver $ -// -// $NoKeywords: $ -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#include "StdAfx.h" -// cbhwlib library is currently based on non Unicode only -#undef UNICODE -#undef _UNICODE -#ifndef WIN32 - // For non-Windows systems, include POSIX headers - #include - #include - #include - #include - #include - #include -#endif -#include "DataVector.h" -#ifndef _MSC_VER -#include -#endif -#ifndef WIN32 - #define Sleep(x) usleep((x) * 1000) - #define strncpy_s( dst, dstSize, src, count ) strncpy( dst, src, count < dstSize ? count : dstSize ) -#endif // WIN32 -#include "debugmacs.h" -#include "../include/cerelink/cbhwlib.h" -#include "cbHwlibHi.h" - - -// forward reference -cbRESULT CreateSharedObjects(uint32_t nInstance); - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Internal Library variables -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - - -// buffer handles -cbSharedMemHandle cb_xmt_global_buffer_hnd[cbMAXOPEN] = {nullptr}; // Transmit queues to send out of this PC -cbXMTBUFF* cb_xmt_global_buffer_ptr[cbMAXOPEN] = {nullptr}; -auto GLOBAL_XMT_NAME = "XmtGlobal"; -cbSharedMemHandle cb_xmt_local_buffer_hnd[cbMAXOPEN] = {nullptr}; // Transmit queues only for local (this PC) use -cbXMTBUFF* cb_xmt_local_buffer_ptr[cbMAXOPEN] = {nullptr}; -auto LOCAL_XMT_NAME = "XmtLocal"; - -auto REC_BUF_NAME = "cbRECbuffer"; -cbSharedMemHandle cb_rec_buffer_hnd[cbMAXOPEN] = {nullptr}; -cbRECBUFF* cb_rec_buffer_ptr[cbMAXOPEN] = {nullptr}; -auto CFG_BUF_NAME = "cbCFGbuffer"; -cbSharedMemHandle cb_cfg_buffer_hnd[cbMAXOPEN] = {nullptr}; -cbCFGBUFF* cb_cfg_buffer_ptr[cbMAXOPEN] = {nullptr}; -auto STATUS_BUF_NAME = "cbSTATUSbuffer"; -cbSharedMemHandle cb_pc_status_buffer_hnd[cbMAXOPEN] = {nullptr}; -cbPcStatus* cb_pc_status_buffer_ptr[cbMAXOPEN] = {nullptr}; -auto SPK_BUF_NAME = "cbSPKbuffer"; -cbSharedMemHandle cb_spk_buffer_hnd[cbMAXOPEN] = {nullptr}; -cbSPKBUFF* cb_spk_buffer_ptr[cbMAXOPEN] = {nullptr}; -auto SIG_EVT_NAME = "cbSIGNALevent"; -HANDLE cb_sig_event_hnd[cbMAXOPEN] = {nullptr}; - -// -uint32_t cb_library_index[cbMAXOPEN] = {0}; -uint32_t cb_library_initialized[cbMAXOPEN] = {false}; -uint32_t cb_recbuff_tailwrap[cbMAXOPEN] = {0}; -uint32_t cb_recbuff_tailindex[cbMAXOPEN] = {0}; -uint32_t cb_recbuff_processed[cbMAXOPEN] = {0}; -PROCTIME cb_recbuff_lasttime[cbMAXOPEN] = {0}; - -// Handle to system lock associated with shared resources -HANDLE cb_sys_lock_hnd[cbMAXOPEN] = {nullptr}; - - -// Local functions to make life easier -inline cbOPTIONTABLE & GetOptionTable(const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - return cb_cfg_buffer_ptr[nIdx]->optiontable; -} - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Library Initialization Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - - - -// Returns the major/minor revision of the current library in the upper/lower uint16_t fields. -uint32_t cbVersion() -{ - return MAKELONG(cbVERSION_MINOR, cbVERSION_MAJOR); -} - -// Author & Date: Kirk Korver 17 Jun 2003 -// Purpose: Release and clear the memory assocated with this handle/pointer -// Inputs: -// hMem - the handle to the memory to free up -// ppMem - the pointer to this same memory -void DestroySharedObject(cbSharedMemHandle & shmem, void ** ppMem) -{ -#ifdef WIN32 - if (*ppMem != nullptr) - UnmapViewOfFile(*ppMem); - if (shmem.hnd != nullptr) - CloseHandle(shmem.hnd); -#else - if (*ppMem != nullptr) - { - // Get the number of current attachments - if (munmap(shmem.hnd, shmem.size) == -1) - { - printf("munmap() failed with errno %d\n", errno); - } - close(shmem.fd); - shm_unlink(shmem.name); - } -#endif - shmem.hnd = nullptr; - *ppMem = nullptr; -} - -// Author & Date: Almut Branner 28 Mar 2006 -// Purpose: Release and clear the shared memory objects -// Inputs: -// nInstance - nsp number to close library for -void DestroySharedObjects(const bool bStandAlone, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - if (bStandAlone) - { - // clear out the version string - if (cb_cfg_buffer_ptr[nIdx]) - cb_cfg_buffer_ptr[nIdx]->version = 0; - } - - // close out the signal events -#ifdef WIN32 - if (cb_sig_event_hnd[nIdx]) - CloseHandle(cb_sig_event_hnd[nIdx]); -#else - if (cb_sig_event_hnd[nIdx]) - { - sem_close(static_cast(cb_sig_event_hnd[nIdx])); - if (bStandAlone) - { - char buf[64] = {0}; - if (nInstance == 0) - _snprintf(buf, sizeof(buf), "%s", SIG_EVT_NAME); - else - _snprintf(buf, sizeof(buf), "%s%d", SIG_EVT_NAME, nInstance); - sem_unlink(buf); - } - } -#endif - - // release the shared pc status memory space - DestroySharedObject(cb_pc_status_buffer_hnd[nIdx], reinterpret_cast(&cb_pc_status_buffer_ptr[nIdx])); - - // release the shared spike buffer memory space - DestroySharedObject(cb_spk_buffer_hnd[nIdx], reinterpret_cast(&cb_spk_buffer_ptr[nIdx])); - - // release the shared configuration memory space - DestroySharedObject(cb_cfg_buffer_hnd[nIdx], reinterpret_cast(&cb_cfg_buffer_ptr[nIdx])); - - // release the shared global transmit memory space - DestroySharedObject(cb_xmt_global_buffer_hnd[nIdx], reinterpret_cast(&cb_xmt_global_buffer_ptr[nIdx])); - - // release the shared local transmit memory space - DestroySharedObject(cb_xmt_local_buffer_hnd[nIdx], reinterpret_cast(&cb_xmt_local_buffer_ptr[nIdx])); - - // release the shared receive memory space - DestroySharedObject(cb_rec_buffer_hnd[nIdx], reinterpret_cast(&cb_rec_buffer_ptr[nIdx])); -} - -// Author & Date: Ehsan Azar 29 April 2012 -// Purpose: Open shared memory section -// Inputs: -// shmem - cbSharedMemHandle object with .name filled in. -// This method will update .hnd (and .size on POSIX) -// bReadOnly - if should open memory for read-only operation -void OpenSharedBuffer(cbSharedMemHandle& shmem, bool bReadOnly) -{ -#ifdef WIN32 - // Keep windows version unchanged - shmem.hnd = OpenFileMappingA(bReadOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, 0, shmem.name); -#else - const int oflag = (bReadOnly ? O_RDONLY : O_RDWR); - const mode_t omode = (bReadOnly ? 0444 : 0660); - shmem.fd = shm_open(shmem.name, oflag, omode); - if (shmem.fd == -1) - /* Handle error */; - struct stat shm_stat{}; - if (fstat(shmem.fd, &shm_stat) == -1) - /* Handle error */; - const int prot = (bReadOnly ? PROT_READ : PROT_READ | PROT_WRITE); - shmem.size = shm_stat.st_size; - shmem.hnd = mmap(nullptr, shm_stat.st_size, prot, MAP_SHARED, shmem.fd, 0); - if (shmem.hnd == MAP_FAILED || !shmem.hnd) - /* Handle error */; -#endif -} - -// Author & Date: Ehsan Azar 29 April 2012 -// Purpose: Open shared memory section -// Inputs: -// shmem - a cbSharedMemHandle object with correct szName and minimum size. Passed by ref and will be updated. -void CreateSharedBuffer(cbSharedMemHandle& shmem) -{ -#ifdef WIN32 - // Keep windows version unchanged - shmem.hnd = CreateFileMappingA((HANDLE)INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, shmem.size, shmem.name); -#else - // round up to the next pagesize. - const int pagesize = getpagesize(); - shmem.size = shmem.size - (shmem.size % pagesize) + pagesize; - // Pre-emptively attempt to unlink in case it already exists. - int errsv = 0; - if (shm_unlink(shmem.name)) { - errsv = errno; - if (errsv != ENOENT) - printf("shm_unlink() failed with errno %d\n", errsv); - } - // Create the shared memory object - shmem.fd = shm_open(shmem.name, O_CREAT | O_RDWR, 0660); - if (shmem.fd == -1){ - errsv = errno; - printf("shm_open() failed with errno %d\n", errsv); - } - // Set the size of the shared memory object. - if (ftruncate(shmem.fd, shmem.size) == -1) { - errsv = errno; - printf("ftruncate(fd, %d) failed with errno %d\n", shmem.size, errsv); - close(shmem.fd); - shm_unlink(shmem.name); - return; - } - // Get a pointer to the shmem object. - void *rptr = mmap(nullptr, shmem.size, - PROT_READ | PROT_WRITE, MAP_SHARED, shmem.fd, 0); - if (rptr == MAP_FAILED) { - errsv = errno; - printf("mmap(nullptr, %d, ...) failed with errno %d\n", shmem.size, errsv); - close(shmem.fd); - shm_unlink(shmem.name); - } - else - shmem.hnd = rptr; -#endif -} - -// Author & Date: Ehsan Azar 29 April 2012 -// Purpose: Get access to shared memory section data -// Inputs: -// hnd - shared memory handle -// bReadOnly - if should open memory for read-only operation -void * GetSharedBuffer(HANDLE hnd, bool bReadOnly) -{ - void * ret = nullptr; - if (hnd == nullptr) - return ret; -#ifdef WIN32 - // Keep windows version unchanged - ret = MapViewOfFile(hnd, bReadOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, 0, 0, 0); -#else - ret = hnd; -#endif - return ret; -} - -// Purpose: Open library as stand-alone or under given application with thread -// identifier for multiple threads each with its own IP -// Inputs: -// bStandAlone - if should open library as stand-alone -// nInstance - integer index identifier of library instance (0-based up to cbMAXOPEN-1) -cbRESULT cbOpen(const bool bStandAlone, const uint32_t nInstance) -{ - char buf[64] = {0}; - cbRESULT cbRet; - - if (nInstance >= cbMAXOPEN) - return cbRESULT_INSTINVALID; - - // Find an empty stub - uint32_t nIdx = cbMAXOPEN; - for (int i = 0; i < cbMAXOPEN; ++i) - { - if (!cb_library_initialized[i]) - { - nIdx = i; - break; - } - } - if (nIdx >= cbMAXOPEN) - return cbRESULT_LIBINITERROR; - - char szLockName[64] = {0}; - if (nInstance == 0) - _snprintf(szLockName, sizeof(szLockName), "cbSharedDataMutex"); - else - _snprintf(szLockName, sizeof(szLockName), "cbSharedDataMutex%d", nInstance); - - // If it is stand-alone application - if (bStandAlone) - { - // Acquire lock - cbRet = cbAcquireSystemLock(szLockName, cb_sys_lock_hnd[nInstance]); - if (cbRet) - return cbRet; - cb_library_index[nInstance] = nIdx; - // Create the shared memory and synchronization objects - cbRet = CreateSharedObjects(nInstance); - // Library initialized if the objects are created - if (cbRet == cbRESULT_OK) - { - cb_library_initialized[nIdx] = true; // We are in the library, so it is initialized - } else { - cbReleaseSystemLock(szLockName, cb_sys_lock_hnd[nInstance]); - cbClose(bStandAlone, nInstance); - } - return cbRet; - } else { - // Check if mutex is locked - cbRet = cbCheckApp(szLockName); - if (cbRet == cbRESULT_NOCENTRALAPP) - return cbRet; - } - - // Create the shared neuromatic receive buffer, if unsuccessful, return FALSE - if (nInstance == 0) - _snprintf(cb_rec_buffer_hnd[nIdx].name, sizeof(cb_rec_buffer_hnd[nIdx].name), "%s", REC_BUF_NAME); - else - _snprintf(cb_rec_buffer_hnd[nIdx].name, sizeof(cb_rec_buffer_hnd[nIdx].name), "%s%d", REC_BUF_NAME, nInstance); - OpenSharedBuffer(cb_rec_buffer_hnd[nIdx], true); - cb_rec_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_rec_buffer_hnd[nIdx].hnd, true)); - if (cb_rec_buffer_ptr[nIdx] == nullptr) - { - cbClose(false, nInstance); - return cbRESULT_LIBINITERROR; - } - - if (nInstance == 0) - _snprintf(cb_xmt_global_buffer_hnd[nIdx].name, sizeof(cb_xmt_global_buffer_hnd[nIdx].name), "%s", GLOBAL_XMT_NAME); - else - _snprintf(cb_xmt_global_buffer_hnd[nIdx].name, sizeof(cb_xmt_global_buffer_hnd[nIdx].name), "%s%d", GLOBAL_XMT_NAME, nInstance); - // Create the shared global transmit buffer; if unsuccessful, release rec buffer and return FALSE - OpenSharedBuffer(cb_xmt_global_buffer_hnd[nIdx], false); - cb_xmt_global_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_xmt_global_buffer_hnd[nIdx].hnd, false)); - if (cb_xmt_global_buffer_ptr[nIdx] == nullptr) - { - cbClose(false, nInstance); - return cbRESULT_LIBINITERROR; - } - - if (nInstance == 0) - _snprintf(cb_xmt_local_buffer_hnd[nIdx].name, sizeof(cb_xmt_local_buffer_hnd[nIdx].name), "%s", LOCAL_XMT_NAME); - else - _snprintf(cb_xmt_local_buffer_hnd[nIdx].name, sizeof(cb_xmt_local_buffer_hnd[nIdx].name), "%s%d", LOCAL_XMT_NAME, nInstance); - // Create the shared local transmit buffer; if unsuccessful, release rec buffer and return FALSE - OpenSharedBuffer(cb_xmt_local_buffer_hnd[nIdx], false); - cb_xmt_local_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_xmt_local_buffer_hnd[nIdx].hnd, false)); - if (cb_xmt_local_buffer_ptr[nIdx] == nullptr) - { - cbClose(false, nInstance); - return cbRESULT_LIBINITERROR; - } - - if (nInstance == 0) - _snprintf(cb_cfg_buffer_hnd[nIdx].name, sizeof(cb_cfg_buffer_hnd[nIdx].name), "%s", CFG_BUF_NAME); - else - _snprintf(cb_cfg_buffer_hnd[nIdx].name, sizeof(cb_cfg_buffer_hnd[nIdx].name), "%s%d", CFG_BUF_NAME, nInstance); - // Create the shared neuromatic configuration buffer; if unsuccessful, release rec buffer and return FALSE - OpenSharedBuffer(cb_cfg_buffer_hnd[nIdx], true); - cb_cfg_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_cfg_buffer_hnd[nIdx].hnd, true)); - if (cb_cfg_buffer_ptr[nIdx] == nullptr) - { - cbClose(false, nInstance); - return cbRESULT_LIBINITERROR; - } - - // Check version of hardware protocol if available - if (cb_cfg_buffer_ptr[nIdx]->procinfo[0].cbpkt_header.chid != 0) { - if (cb_cfg_buffer_ptr[nIdx]->procinfo[0].version > cbVersion()) { - cbClose(false, nInstance); - return cbRESULT_LIBOUTDATED; - } - if (cb_cfg_buffer_ptr[nIdx]->procinfo[0].version < cbVersion()) { - cbClose(false, nInstance); - return cbRESULT_INSTOUTDATED; - } - } - - if (nInstance == 0) - _snprintf(cb_pc_status_buffer_hnd[nIdx].name, sizeof(cb_pc_status_buffer_hnd[nIdx].name), "%s", STATUS_BUF_NAME); - else - _snprintf(cb_pc_status_buffer_hnd[nIdx].name, sizeof(cb_pc_status_buffer_hnd[nIdx].name), "%s%d", STATUS_BUF_NAME, nInstance); - // Create the shared pc status buffer; if unsuccessful, release rec buffer and return FALSE - OpenSharedBuffer(cb_pc_status_buffer_hnd[nIdx], false); - cb_pc_status_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_pc_status_buffer_hnd[nIdx].hnd, false)); - if (cb_pc_status_buffer_ptr[nIdx] == nullptr) - { - cbClose(false, nInstance); - return cbRESULT_LIBINITERROR; - } - - // Create the shared spike buffer - if (nInstance == 0) - _snprintf(cb_spk_buffer_hnd[nIdx].name, sizeof(cb_spk_buffer_hnd[nIdx].name), "%s", SPK_BUF_NAME); - else - _snprintf(cb_spk_buffer_hnd[nIdx].name, sizeof(cb_spk_buffer_hnd[nIdx].name), "%s%d", SPK_BUF_NAME, nInstance); - OpenSharedBuffer(cb_spk_buffer_hnd[nIdx], false); - cb_spk_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_spk_buffer_hnd[nIdx].hnd, false)); - if (cb_spk_buffer_ptr[nIdx] == nullptr) - { - cbClose(false, nInstance); - return cbRESULT_LIBINITERROR; - } - - // open the data availability signals - if (nInstance == 0) - _snprintf(buf, sizeof(buf), "%s", SIG_EVT_NAME); - else - _snprintf(buf, sizeof(buf), "%s%d", SIG_EVT_NAME, nInstance); -#ifdef WIN32 - cb_sig_event_hnd[nIdx] = OpenEventA(SYNCHRONIZE, TRUE, buf); - if (cb_sig_event_hnd[nIdx] == nullptr) - { - cbClose(false, nInstance); - return cbRESULT_LIBINITERROR; - } -#else - sem_t *sem = sem_open(buf, 0); - if (sem == SEM_FAILED) { cbClose(false, nInstance); return cbRESULT_LIBINITERROR; } - cb_sig_event_hnd[nIdx] = sem; -#endif - - cb_library_index[nInstance] = nIdx; - cb_library_initialized[nIdx] = true; - - // Initialize read indices to the current head position - cbMakePacketReadingBeginNow(nInstance); - - return cbRESULT_OK; -} - -// Author & Date: Ehsan Azar 29 April 2012 -// Purpose: Find if a Cerebus app instance is running -// Inputs: -// lpName - name of the Cerebus application mutex -// Outputs: -// Returns cbRESULT_NOCENTRALAPP if application is not running, otherwise cbRESULT_OK -cbRESULT cbCheckApp(const char * lpName) -{ - cbRESULT cbRet = cbRESULT_OK; - if (lpName == nullptr) - return cbRESULT_SYSLOCK; -#ifdef WIN32 - // Test for availability of central application by attempting to open/close Central App Mutex - HANDLE hCentralMutex = OpenMutexA(MUTEX_ALL_ACCESS, FALSE, lpName); - CloseHandle(hCentralMutex); - if (hCentralMutex == nullptr) - cbRet = cbRESULT_NOCENTRALAPP; -#else - { - char szLockName[256] = {0}; - char * szTmpDir = getenv("TMPDIR"); - _snprintf(szLockName, sizeof(szLockName), "%s/%s.lock", szTmpDir == nullptr ? "/tmp" : szTmpDir, lpName); - FILE * pflock = fopen(szLockName, "w+"); - if (pflock == nullptr) - cbRet = cbRESULT_OK; // Assume root has the lock - else if (flock(fileno(pflock), LOCK_EX | LOCK_NB) == 0) - cbRet = cbRESULT_NOCENTRALAPP; - if (pflock != nullptr) - { - fclose(pflock); - if (cbRet == cbRESULT_NOCENTRALAPP) - remove(szLockName); - } - } -#endif - return cbRet; -} - -// Author & Date: Ehsan Azar 29 Oct 2012 -// Purpose: Aquire a system mutex for Cerebus application -// Inputs: -// lpName - name of the Cerebus application mutex -// Outputs: -// hLock - the handle to newly created system lock -// Returns the error code (cbRESULT_OK if successful) -cbRESULT cbAcquireSystemLock(const char * lpName, HANDLE & hLock) -{ - if (lpName == nullptr) - return cbRESULT_SYSLOCK; -#ifdef WIN32 - // Try creating the system mutex - HANDLE hMutex = CreateMutexA(nullptr, TRUE, lpName); - if (hMutex == 0 || GetLastError() == ERROR_ACCESS_DENIED || GetLastError() == ERROR_ALREADY_EXISTS) - return cbRESULT_SYSLOCK; - hLock = hMutex; -#else - // There are other methods such as sharedmemory but they break if application crash - // only a file lock seems resilient to crash, also with tmp mounted as tmpfs this is as fast as it could be - char szLockName[256] = {0}; - char * szTmpDir = getenv("TMPDIR"); - _snprintf(szLockName, sizeof(szLockName), "%s/%s.lock", szTmpDir == nullptr ? "/tmp" : szTmpDir, lpName); - FILE * pflock = fopen(szLockName, "w+"); - if (pflock == nullptr) - return cbRESULT_SYSLOCK; - if (flock(fileno(pflock), LOCK_EX | LOCK_NB) != 0) - { - return cbRESULT_SYSLOCK; - } - fprintf(pflock, "%u", static_cast(getpid())); - hLock = static_cast(pflock); -#endif - return cbRESULT_OK; -} - -// Author & Date: Ehsan Azar 29 Oct 2012 -// Purpose: Release system mutex -// Inputs: -// lpName - name of the Cerebus application mutex -// hLock - System mutex handle -// Outputs: -// Returns the error code (cbRESULT_OK if successful) -cbRESULT cbReleaseSystemLock(const char * lpName, HANDLE & hLock) -{ - if (lpName == nullptr) - return cbRESULT_SYSLOCK; -#ifdef WIN32 - if (CloseHandle(hLock) == 0) - return cbRESULT_SYSLOCK; -#else - if (hLock) - { - char szLockName[256] = {0}; - const char * szTmpDir = getenv("TMPDIR"); - _snprintf(szLockName, sizeof(szLockName), "%s/%s.lock", szTmpDir == nullptr ? "/tmp" : szTmpDir, lpName); - const auto pflock = static_cast(hLock); - fclose(pflock); // Close mutex - remove(szLockName); // Cleanup - } -#endif - hLock = nullptr; - return cbRESULT_OK; -} - -// Author & Date: Ehsan Azar 15 March 2010 -// Purpose: get instrument information. -// If hardware is offline, detect local instrument if any -// Outputs: -// instInfo - combination of cbINSTINFO_* -// cbRESULT_OK if successful -cbRESULT cbGetInstInfo(uint8_t nInstrument, uint32_t *instInfo, const uint32_t nInstance) -{ - *instInfo = 0; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) - return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid - if ((nInstrument) > cbMAXPROCS) return cbRESULT_INVALIDADDRESS; - - int type = 0; - cbPROCINFO isInfo; - if (cbGetProcInfo(nInstrument, &isInfo, nInstance) == cbRESULT_OK) - { - if (strstr(isInfo.ident, "player") != nullptr) - type = cbINSTINFO_NPLAY; - else if (strstr(isInfo.ident, "Cereplex") != nullptr) - type = cbINSTINFO_CEREPLEX; - else if (strstr(isInfo.ident, "Emulator") != nullptr) - type = cbINSTINFO_EMULATOR; - else if (strstr(isInfo.ident, "wNSP") != nullptr) - type = cbINSTINFO_WNSP; - else if (strstr(isInfo.ident, "Gemini NSP ") != nullptr) - type |= cbINSTINFO_GEMINI_NSP; - else if (strstr(isInfo.ident, "Gemini Hub ") != nullptr) - type |= cbINSTINFO_GEMINI_HUB; - else if (strstr(isInfo.ident, "NSP ") != nullptr) - type |= cbINSTINFO_NSP1; - *instInfo |= type; - } - - if (cbCheckApp("cbNPlayMutex") == cbRESULT_OK) - { - *instInfo |= cbINSTINFO_LOCAL; // Local - *instInfo |= type; - } - - uint32_t flags = 0; - // If online get more info (this will detect remote instruments) - if (cbGetNplay(nullptr, nullptr, &flags, nullptr, nullptr, nullptr, nullptr, nInstance) == cbRESULT_OK) - { - if (flags != cbNPLAY_FLAG_NONE) // If nPlay is running - *instInfo |= type; - } - - // Sysinfo is the last config packet - if (cb_cfg_buffer_ptr[nIdx]->sysinfo.cbpkt_header.chid == 0) - return cbRESULT_HARDWAREOFFLINE; - - // If this is reached instrument is ready and known - *instInfo |= cbINSTINFO_READY; - - return cbRESULT_OK; -} - -// Author & Date: Ehsan Azar 30 Aug 2012 -// Purpose: get NSP latency based on its version -// this is latency between tail and irq output -// Outputs: -// nLatency - the latency (in number of samples) -// cbRESULT_OK if successful -cbRESULT cbGetLatency(uint32_t *nLatency, const uint32_t nInstance) -{ - *nLatency = 0; - - uint32_t spikelen; - if (const cbRESULT res = cbGetSpikeLength(&spikelen, nullptr, nullptr, nInstance)) - return res; - if (nLatency) *nLatency = (2 * spikelen + 16); - return cbRESULT_OK; -} - -cbRESULT cbMakePacketReadingBeginNow(const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Initialize read indices to the current head position - cb_recbuff_tailwrap[nIdx] = cb_rec_buffer_ptr[nIdx]->headwrap; - cb_recbuff_tailindex[nIdx] = cb_rec_buffer_ptr[nIdx]->headindex; - cb_recbuff_processed[nIdx] = cb_rec_buffer_ptr[nIdx]->received; - cb_recbuff_lasttime[nIdx] = cb_rec_buffer_ptr[nIdx]->lasttime; - - return cbRESULT_OK; -} - -cbRESULT cbClose(const bool bStandAlone, const uint32_t nInstance) -{ - if (nInstance >= cbMAXOPEN) - return cbRESULT_INSTINVALID; - const uint32_t nIdx = cb_library_index[nInstance]; - - // clear lib init flag variable - cb_library_initialized[nIdx] = false; - - // destroy the shared neuromatic memory and synchronization objects - DestroySharedObjects(bStandAlone, nInstance); - // If it is stand-alone application - if (bStandAlone) - { - char buf[256] = {0}; - if (nInstance == 0) - _snprintf(buf, sizeof(buf), "cbSharedDataMutex"); - else - _snprintf(buf, sizeof(buf), "cbSharedDataMutex%d", nInstance); - if (cb_sys_lock_hnd[nInstance]) - cbReleaseSystemLock(buf, cb_sys_lock_hnd[nInstance]); - return cbRESULT_OK; - } - - return cbRESULT_OK; -} - - -// Purpose: Processor Inquiry Function -// -cbRESULT cbGetProcInfo(uint32_t proc, cbPROCINFO *procinfo, const uint32_t nInstance) -{ - uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid and that requested procinfo structure is not empty - if ((proc - 1) >= cbMAXPROCS) return cbRESULT_INVALIDADDRESS; - if (!cb_cfg_buffer_ptr[nIdx]) return cbRESULT_NOLIBRARY; - if (cb_cfg_buffer_ptr[nIdx]->procinfo[proc - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDADDRESS; - - // otherwise, return the data - if (procinfo) memcpy(procinfo, &(cb_cfg_buffer_ptr[nIdx]->procinfo[proc-1].idcode), sizeof(cbPROCINFO)); - return cbRESULT_OK; -} - - -// Purpose: Bank Inquiry Function -// -cbRESULT cbGetBankInfo(uint32_t proc, uint32_t bank, cbBANKINFO *bankinfo, const uint32_t nInstance) -{ - uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the addresses are valid and that requested procinfo structure is not empty - if (((proc - 1) >= cbMAXPROCS) || ((bank - 1) >= cb_cfg_buffer_ptr[nIdx]->procinfo[0].bankcount)) return cbRESULT_INVALIDADDRESS; - if (cb_cfg_buffer_ptr[nIdx]->bankinfo[proc - 1][bank - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDADDRESS; - - // otherwise, return the data - if (bankinfo) memcpy(bankinfo, &(cb_cfg_buffer_ptr[nIdx]->bankinfo[proc - 1][bank - 1].idcode), sizeof(cbBANKINFO)); - return cbRESULT_OK; -} - - -uint32_t GetInstrumentLocalChan(uint32_t nChan, const uint32_t nInstance) -{ - uint32_t nIdx = cb_library_index[nInstance]; - return cb_cfg_buffer_ptr[nIdx]->chaninfo[nChan - 1].chan; -} - - -// Purpose: -// Retreives the total number of channels in the system -// Returns: cbRESULT_OK if data successfully retreived. -// cbRESULT_NOLIBRARY if the library was not properly initialized. -cbRESULT cbGetChanCount(uint32_t *count, const uint32_t nInstance) -{ - uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Sweep through the processor information banks and sum up the number of channels - *count = 0; - for(const auto & p : cb_cfg_buffer_ptr[nIdx]->procinfo) - if (p.cbpkt_header.chid) *count += p.chancount; - - return cbRESULT_OK; -} - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Systemwide Inquiry and Configuration Functions -// -/////////////////////////////////////////////////////////////////////////////////////////////////// -cbRESULT cbSendPacketToInstrument(void * pPacket, const uint32_t nInstance, uint32_t nInstrument) -{ -#ifndef CBPROTO_311 - auto * pPkt = static_cast(pPacket); - pPkt->cbpkt_header.instrument = nInstrument; -#endif - return cbSendPacket(pPacket, nInstance); -} - -cbRESULT cbSendPacket(void * pPacket, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - auto * pPkt = static_cast(pPacket); - - // The logic here is quite complicated. Data is filled in from other processes - // in a 2 pass mode. First they fill all except they skip the first sizeof(PROCTIME) bytes. - // The final step in the process is to convert the 1st PROCTIME from "0" to some other number. - // This step is done in a thread-safe manner - // Consequently, all packets can not have "0" as the first PROCTIME. At the time of writing, - // We were looking at the "time" value of a packet. - - pPkt->cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - - // Time cannot be equal to 0 - if (pPkt->cbpkt_header.time == 0) - pPkt->cbpkt_header.time = 1; - - ASSERT( *(static_cast(pPacket)) != 0); - - - uint32_t quadletcount = pPkt->cbpkt_header.dlen + cbPKT_HEADER_32SIZE; - uint32_t orig_headindex; - - // give 3 attempts to claim a spot in the circular xmt buffer for the packet - int insertionattempts = 0; - static const int NUM_INSERTION_ATTEMPTS = 3; - while (true) - { - bool bTryToFill = false; // Should I try to stuff data into the queue, or is it filled - // TRUE = go ahead a try; FALSE = wait a bit - - // Copy the current buffer indices - // NOTE: may want to use a 64-bit transfer to atomically get indices. Otherwise, grab - // tail index first to get "worst case" scenario; - uint32_t orig_tailindex = cb_xmt_global_buffer_ptr[nIdx]->tailindex; - orig_headindex = cb_xmt_global_buffer_ptr[nIdx]->headindex; - - // Compute the new head index at after the target packet position. - // If the new head index is within (cbCER_UDP_SIZE_MAX / 4) quadlets of the buffer end, wrap it around. - // Also, make sure that we are not wrapping around the tail pointer - uint32_t mod_headindex = orig_headindex + quadletcount; - uint32_t nLastOKPosition = cb_xmt_global_buffer_ptr[nIdx]->last_valid_index; - if (mod_headindex > nLastOKPosition) - { - // time to wrap around in the circular buffer.... - mod_headindex = 0; - - // Is there room? - if (mod_headindex < orig_tailindex) - bTryToFill = true; - } - else if (orig_tailindex > orig_headindex) // wrapped recently, but not yet caught up? - { - // Is there room? - if (mod_headindex < orig_tailindex) // Do I have space to continue? - bTryToFill = true; - } - - else // no wrapping at all...and plenty of room - { - bTryToFill = true; - } - - - if (bTryToFill) - { - // attempt to atomically update the head pointer and verify that the head pointer - // was not changed since orig_head_index was loaded and calculated upon - auto * pDest = reinterpret_cast(&cb_xmt_global_buffer_ptr[nIdx]->headindex); - auto dwExch = static_cast(mod_headindex); - auto dwComp = static_cast(orig_headindex); -#ifdef WIN32 - if ((int32_t)InterlockedCompareExchange((volatile unsigned long *)pDest, (unsigned long)dwExch, (unsigned long)dwComp) == dwComp) - break; -#else - if (__sync_bool_compare_and_swap(pDest, dwComp, dwExch)) - break; -#endif - // NORMAL EXIT - } - - // check to see if we should give up or not - if ((++insertionattempts) >= NUM_INSERTION_ATTEMPTS) - { - return cbRESULT_MEMORYUNAVAIL; - } - - // Sleep for a bit to let buffers clear and try again - Sleep(10); - } - - // Copy all but the first 4 bytes of the packet to the target packet location. - // The Central App will not transmit the packet while the first uint32_t is zero. - memcpy(&(cb_xmt_global_buffer_ptr[nIdx]->buffer[orig_headindex+1]), static_cast(pPacket)+1, ((quadletcount-1)<<2) ); - - // atomically copy the first 4 bytes of the packet -#ifdef WIN32 - InterlockedExchange((LPLONG)&(cb_xmt_global_buffer_ptr[nIdx]->buffer[orig_headindex]),*((LONG*)pPacket)); -#else - // buffer is uint32_t[] which guarantees 4-byte alignment, but compiler can't prove it at compile-time - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Watomic-alignment" - __atomic_exchange_n(reinterpret_cast(&(cb_xmt_global_buffer_ptr[nIdx]->buffer[orig_headindex])),*static_cast(pPacket), __ATOMIC_SEQ_CST); - #pragma clang diagnostic pop -#endif - return cbRESULT_OK; -} - - -// Author & Date: Kirk Korver 17 Jun 2003 -// Purpose: send a packet only to ourselves (think IPC) -cbRESULT cbSendLoopbackPacket(void * pPacket, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - auto * pPkt = static_cast(pPacket); - - // The logic here is quite complicated. Data is filled in from other processes - // in a 2 pass mode. First they fill all except they skip the first 4 bytes. - // The final step in the process is to convert the 1st dword from "0" to some other number. - // This step is done in a thread-safe manner - // Consequently, all packets can not have "0" as the first DWORD. At the time of writing, - // We were looking at the "time" value of a packet. - - pPkt->cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - - // Time cannot be equal to 0 - if (pPkt->cbpkt_header.time == 0) - pPkt->cbpkt_header.time = 1; - - ASSERT( *(static_cast(pPacket)) != 0); - - const uint32_t quadletcount = pPkt->cbpkt_header.dlen + cbPKT_HEADER_32SIZE; - uint32_t orig_headindex; - - // give 3 attempts to claim a spot in the circular xmt buffer for the packet - int insertionattempts = 0; - static const int NUM_INSERTION_ATTEMPTS = 3; - for (;;) - { - bool bTryToFill = false; // Should I try to stuff data into the queue, or is it filled - // TRUE = go ahead a try; FALSE = wait a bit - - // Copy the current buffer indices - // NOTE: may want to use a 64-bit transfer to atomically get indices. Otherwise, grab - // tail index first to get "worst case" scenario; - const uint32_t orig_tailindex = cb_xmt_local_buffer_ptr[nIdx]->tailindex; - orig_headindex = cb_xmt_local_buffer_ptr[nIdx]->headindex; - - // Compute the new head index at after the target packet position. - // If the new head index is within (cbCER_UDP_SIZE_MAX / 4) quadlets of the buffer end, wrap it around. - // Also, make sure that we are not wrapping around the tail pointer, if we are, next if will get it - uint32_t mod_headindex = orig_headindex + quadletcount; - uint32_t nLastOKPosition = cb_xmt_local_buffer_ptr[nIdx]->last_valid_index; - if (mod_headindex > nLastOKPosition) - { - // time to wrap around in the circular buffer.... - mod_headindex = 0; - - // Is there room? - if (mod_headindex < orig_tailindex) - bTryToFill = true; - } - else if (orig_tailindex > orig_headindex) // wrapped recently, but not yet caught up? - { - // Is there room? - if (mod_headindex < orig_tailindex) // Do I have space to continue? - bTryToFill = true; - } - - else // no wrapping at all...and plenty of room - { - bTryToFill = true; - } - - - if (bTryToFill) - { - // attempt to atomically update the head pointer and verify that the head pointer - // was not changed since orig_head_index was loaded and calculated upon - auto * pDest = reinterpret_cast(&cb_xmt_local_buffer_ptr[nIdx]->headindex); - auto dwExch = static_cast(mod_headindex); - auto dwComp = static_cast(orig_headindex); -#ifdef WIN32 - if ((int32_t)InterlockedCompareExchange((volatile unsigned long *)pDest, (unsigned long)dwExch, (unsigned long)dwComp) == dwComp) - break; -#else - if (__sync_bool_compare_and_swap(pDest, dwComp, dwExch)) - break; -#endif - // NORMAL EXIT - } - - // check to see if we should give up or not - if ((++insertionattempts) >= NUM_INSERTION_ATTEMPTS) - return cbRESULT_MEMORYUNAVAIL; - - // Sleep for a bit to let buffers clear and try again - Sleep(10); - } - - // Copy all but the first 4 bytes of the packet to the target packet location. - // The Central App will not transmit the packet while the first uint32_t is zero. - memcpy(&(cb_xmt_local_buffer_ptr[nIdx]->buffer[orig_headindex+1]), static_cast(pPacket)+1, ((quadletcount-1)<<2) ); - - // atomically copy the first 4 bytes of the packet -#ifdef WIN32 - InterlockedExchange((LPLONG)&(cb_xmt_local_buffer_ptr[nIdx]->buffer[orig_headindex]),*((LONG*)pPacket)); -#else - // buffer is uint32_t[] which guarantees 4-byte alignment, but compiler can't prove it at compile-time - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Watomic-alignment" - __atomic_exchange_n(reinterpret_cast(&(cb_xmt_local_buffer_ptr[nIdx]->buffer[orig_headindex])),*static_cast(pPacket), __ATOMIC_SEQ_CST); - #pragma clang diagnostic pop -#endif - - return cbRESULT_OK; -} - - - -cbRESULT cbGetSystemRunLevel(uint32_t *runlevel, uint32_t *runflags, uint32_t *resetque, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Check for cached packet available and initialized - if ((cb_cfg_buffer_ptr[nIdx]->sysinfo.cbpkt_header.chid == 0)||(cb_cfg_buffer_ptr[nIdx]->sysinfo.runlevel == 0)) - { - if (runlevel) *runlevel=0; - if (resetque) *resetque=0; - if (runflags) *runflags=0; - return cbRESULT_HARDWAREOFFLINE; - } - - // otherwise, return the data - if (runlevel) *runlevel = cb_cfg_buffer_ptr[nIdx]->sysinfo.runlevel; - if (resetque) *resetque = cb_cfg_buffer_ptr[nIdx]->sysinfo.resetque; - if (runflags) *runflags = cb_cfg_buffer_ptr[nIdx]->sysinfo.runflags; - return cbRESULT_OK; -} - - -cbRESULT cbSetSystemRunLevel(const uint32_t runlevel, const uint32_t runflags, const uint32_t resetque, const uint8_t nInstrument, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Create the packet data structure and fill it in - cbPKT_SYSINFO sysinfo; - sysinfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - sysinfo.cbpkt_header.chid = 0x8000; - sysinfo.cbpkt_header.type = cbPKTTYPE_SYSSETRUNLEV; - sysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; - sysinfo.runlevel = runlevel; - sysinfo.resetque = resetque; - sysinfo.runflags = runflags; - - // Enter the packet into the XMT buffer queue - return cbSendPacketToInstrument(&sysinfo, nInstance, nInstrument); -} - - -cbRESULT cbGetSystemClockFreq(uint32_t *freq, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // hardwire the rate to 30ksps - *freq = 30000; - - return cbRESULT_OK; -} - - -cbRESULT cbGetSystemClockTime(PROCTIME *time, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - *time = cb_rec_buffer_ptr[nIdx]->lasttime; - - return cbRESULT_OK; -} - -cbRESULT cbSetComment(const uint8_t charset, const uint32_t rgba, const PROCTIME time, const char* comment, const uint32_t nInstance) -{ - cbRESULT bRes = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) - return cbRESULT_NOLIBRARY; - - // Create the packet data structure and fill it in - cbPKT_COMMENT pktComment = {}; - pktComment.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - pktComment.cbpkt_header.chid = 0x8000; - pktComment.cbpkt_header.type = cbPKTTYPE_COMMENTSET; - pktComment.info.charset = charset; -#ifndef CBPROTO_311 - pktComment.rgba = rgba; - pktComment.timeStarted = time; -// else pktComment.info.flags, pktComment.data -#endif - uint32_t nLen = 0; - if (comment) - strncpy(pktComment.comment, comment, cbMAX_COMMENT); - pktComment.comment[cbMAX_COMMENT - 1] = 0; - nLen = static_cast(strlen(pktComment.comment)) + 1; // include terminating null - pktComment.cbpkt_header.dlen = static_cast(cbPKTDLEN_COMMENTSHORT) + (nLen + 3) / 4; - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == bRes) && (NSP_FOUND == cbGetNspStatus(nProc))) - bRes = cbSendPacketToInstrument(&pktComment, nInstance, nProc - 1); - } - return bRes; -} - -cbRESULT cbGetLncParameters(const uint32_t nProc, uint32_t* nLncFreq, uint32_t* nLncRefChan, uint32_t* nLncGMode, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Check for cached sysinfo packet initialized - if (cb_cfg_buffer_ptr[nIdx]->isLnc[nProc - 1].cbpkt_header.chid == 0) return cbRESULT_HARDWAREOFFLINE; - - // otherwise, return the data - if (nLncFreq) *nLncFreq = cb_cfg_buffer_ptr[nIdx]->isLnc[nProc - 1].lncFreq; - if (nLncRefChan) *nLncRefChan = cb_cfg_buffer_ptr[nIdx]->isLnc[nProc - 1].lncRefChan; - if (nLncGMode) *nLncGMode = cb_cfg_buffer_ptr[nIdx]->isLnc[nProc - 1].lncGlobalMode; - return cbRESULT_OK; -} - - -cbRESULT cbSetLncParameters(const uint32_t nProc, const uint32_t nLncFreq, const uint32_t nLncRefChan, const uint32_t nLncGMode, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Create the packet data structure and fill it in - cbPKT_LNC pktLnc; - pktLnc.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - pktLnc.cbpkt_header.chid = 0x8000; - pktLnc.cbpkt_header.type = cbPKTTYPE_LNCSET; - pktLnc.cbpkt_header.dlen = cbPKTDLEN_LNC; - pktLnc.lncFreq = nLncFreq; - pktLnc.lncRefChan = nLncRefChan; - pktLnc.lncGlobalMode = nLncGMode; - - // Enter the packet into the XMT buffer queue - return cbSendPacketToInstrument(&pktLnc, nInstance, nProc - 1); -} - -cbRESULT cbGetNplay(char *fname, float *speed, uint32_t *flags, PROCTIME *ftime, PROCTIME *stime, PROCTIME *etime, PROCTIME * filever, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Check for cached sysinfo packet initialized - if (cb_cfg_buffer_ptr[nIdx]->isNPlay.cbpkt_header.chid == 0) return cbRESULT_HARDWAREOFFLINE; - - // otherwise, return the data - if (fname) strncpy(fname, cb_cfg_buffer_ptr[nIdx]->isNPlay.fname, cbNPLAY_FNAME_LEN); - if (speed) *speed = cb_cfg_buffer_ptr[nIdx]->isNPlay.speed; - if (flags) *flags = cb_cfg_buffer_ptr[nIdx]->isNPlay.flags; - if (ftime) *ftime = cb_cfg_buffer_ptr[nIdx]->isNPlay.ftime; - if (stime) *stime = cb_cfg_buffer_ptr[nIdx]->isNPlay.stime; - if (etime) *etime = cb_cfg_buffer_ptr[nIdx]->isNPlay.etime; - if (filever) *filever = cb_cfg_buffer_ptr[nIdx]->isNPlay.val; - return cbRESULT_OK; -} - -cbRESULT cbSetNplay(const char *fname, const float speed, const uint32_t mode, const PROCTIME val, const PROCTIME stime, const PROCTIME etime, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Check for cached sysinfo packet initialized - if (cb_cfg_buffer_ptr[nIdx]->isNPlay.cbpkt_header.chid == 0) return cbRESULT_HARDWAREOFFLINE; - - // Create the packet data structure and fill it in - cbPKT_NPLAY pktNPlay; - pktNPlay.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - pktNPlay.cbpkt_header.chid = 0x8000; - pktNPlay.cbpkt_header.type = cbPKTTYPE_NPLAYSET; - pktNPlay.cbpkt_header.dlen = cbPKTDLEN_NPLAY; - pktNPlay.speed = speed; - pktNPlay.mode = mode; - pktNPlay.flags = cbNPLAY_FLAG_NONE; // No flags here - pktNPlay.val = val; - pktNPlay.stime = stime; - pktNPlay.etime = etime; - pktNPlay.fname[0] = 0; - if (fname) strncpy(pktNPlay.fname, fname, cbNPLAY_FNAME_LEN); - - // Enter the packet into the XMT buffer queue - return cbSendPacketToInstrument(&pktNPlay, nInstance, cbNSP1 - 1); -} - -// Author & Date: Ehsan Azar 25 May 2010 -// Inputs: -// id - video source ID (1 to cbMAXVIDEOSOURCE) -// Outputs: -// name - name of the video source -// fps - the frame rate of the video source -cbRESULT cbGetVideoSource(char *name, float *fps, const uint32_t id, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - if ((id - 1) >= cbMAXVIDEOSOURCE) return cbRESULT_INVALIDADDRESS; - if (cb_cfg_buffer_ptr[nIdx]->isVideoSource[id - 1].fps == 0) return cbRESULT_INVALIDADDRESS; - - // otherwise, return the data - if (name) strncpy_s(name, cbLEN_STR_LABEL, cb_cfg_buffer_ptr[nIdx]->isVideoSource[id - 1].name, cbLEN_STR_LABEL-1); - if (fps) *fps = cb_cfg_buffer_ptr[nIdx]->isVideoSource[id - 1].fps; - - return cbRESULT_OK; -} - -// Author & Date: Ehsan Azar 25 May 2010 -// Inputs: -// id - trackable object ID (1 to cbMAXTRACKOBJ) -// Outputs: -// name - name of the video source -// type - type of the trackable object (start from 0) -// pointCount - the maximum number of points for this trackable -cbRESULT cbGetTrackObj(char *name, uint16_t *type, uint16_t *pointCount, const uint32_t id, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - if ((id - 1) >= cbMAXTRACKOBJ) return cbRESULT_INVALIDADDRESS; - if (cb_cfg_buffer_ptr[nIdx]->isTrackObj[id - 1].type == 0) return cbRESULT_INVALIDADDRESS; - - // otherwise, return the data - if (name) strncpy_s(name, cbLEN_STR_LABEL, cb_cfg_buffer_ptr[nIdx]->isTrackObj[id - 1].name, cbLEN_STR_LABEL - 1); - if (type) *type = cb_cfg_buffer_ptr[nIdx]->isTrackObj[id-1].type; - if (pointCount) *pointCount = cb_cfg_buffer_ptr[nIdx]->isTrackObj[id-1].pointCount; - - return cbRESULT_OK; -} - - -// Author & Date: Ehsan Azar 25 May 2010 -// Inputs: -// vs - video source ID (start from 0) -// name - name of the video source -// fps - the frame rate of the video source -cbRESULT cbSetVideoSource(const char *name, const float fps, const uint32_t id, const uint32_t nInstance) -{ - cbRESULT cbRes = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_NM pktNm = {}; - pktNm.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - pktNm.cbpkt_header.type = cbPKTTYPE_NMSET; - pktNm.cbpkt_header.dlen = cbPKTDLEN_NM; - pktNm.mode = cbNM_MODE_SETVIDEOSOURCE; - pktNm.flags = id + 1; - pktNm.value = static_cast(fps * 1000); // frame rate times 1000 - strncpy(pktNm.name, name, cbLEN_STR_LABEL); - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == cbRes) && (NSP_FOUND == cbGetNspStatus(nProc))) - cbRes = cbSendPacketToInstrument(&pktNm, nInstance, nProc - 1); - } - return cbRes; -} - -// Author & Date: Ehsan Azar 25 May 2010 -// Inputs: -// id - trackable object ID (start from 0) -// name - name of the video source -// type - type of the trackable object (start from 0) -// pointCount - the maximum number of points for this trackable object -cbRESULT cbSetTrackObj(const char *name, const uint16_t type, const uint16_t pointCount, const uint32_t id, const uint32_t nInstance) -{ - cbRESULT cbRes = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_NM pktNm = {}; - pktNm.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - pktNm.cbpkt_header.type = cbPKTTYPE_NMSET; - pktNm.cbpkt_header.dlen = cbPKTDLEN_NM; - pktNm.mode = cbNM_MODE_SETTRACKABLE; - pktNm.flags = id + 1; - pktNm.value = type | (pointCount << 16); - strncpy(pktNm.name, name, cbLEN_STR_LABEL); - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == cbRes) && (NSP_FOUND == cbGetNspStatus(nProc))) - cbRes = cbSendPacketToInstrument(&pktNm, nInstance, nProc - 1); - } - return cbRes; -} - -cbRESULT cbGetSpikeLength(uint32_t *length, uint32_t *pretrig, uint32_t * pSysfreq, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Check for cached sysinfo packet initialized - if (cb_cfg_buffer_ptr[nIdx]->sysinfo.cbpkt_header.chid == 0) return cbRESULT_HARDWAREOFFLINE; - - // otherwise, return the data - if (length) *length = cb_cfg_buffer_ptr[nIdx]->sysinfo.spikelen; - if (pretrig) *pretrig = cb_cfg_buffer_ptr[nIdx]->sysinfo.spikepre; - if (pSysfreq) *pSysfreq = cb_cfg_buffer_ptr[nIdx]->sysinfo.sysfreq; - return cbRESULT_OK; -} - - -cbRESULT cbSetSpikeLength(const uint32_t length, const uint32_t pretrig, const uint32_t nInstance) -{ - cbRESULT bRes = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Create the packet data structure and fill it in - cbPKT_SYSINFO sysinfo; - sysinfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - sysinfo.cbpkt_header.chid = 0x8000; - sysinfo.cbpkt_header.type = cbPKTTYPE_SYSSETSPKLEN; - sysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; - sysinfo.spikelen = length; - sysinfo.spikepre = pretrig; - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == bRes) && (NSP_FOUND == cbGetNspStatus(nProc))) - bRes = cbSendPacketToInstrument(&sysinfo, nInstance, nProc - 1); // Enter the packet into the XMT buffer queue - } - return bRes; -} - -// Author & Date: Ehsan Azar 16 Jan 2012 -// Inputs: -// channel - 1-based channel number -// mode - waveform type -// repeats - number of repeats -// trig - trigget type -// trigChan - Channel for trigger -// wave - waveform data -cbRESULT cbGetAoutWaveform(uint32_t channel, uint8_t const trigNum, uint16_t * mode, uint32_t * repeats, uint16_t * trig, - uint16_t * trigChan, uint16_t * trigValue, cbWaveformData * wave, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - uint32_t wavenum = 0; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[channel - 1].chancaps & cbCHAN_AOUT)) return cbRESULT_INVALIDFUNCTION; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[channel - 1].aoutcaps & cbAOUT_WAVEFORM)) return cbRESULT_INVALIDFUNCTION; - if (trigNum >= cbMAX_AOUT_TRIGGER) return cbRESULT_INVALIDFUNCTION; - //hls channels not in contiguous order anymore if (channel <= cb_pc_status_buffer_ptr[0]->GetNumAnalogChans()) return cbRESULT_INVALIDCHANNEL; - if (cbRESULT_OK != cbGetAoutWaveformNumber(channel, &wavenum)) return cbRESULT_INVALIDCHANNEL; - channel = wavenum; - //hls channel -= (cb_pc_status_buffer_ptr[0]->GetNumAnalogChans() + 1); // make it 0-based - if (channel >= AOUT_NUM_GAIN_CHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->isWaveform[channel][trigNum].cbpkt_header.type == 0) return cbRESULT_INVALIDCHANNEL; - - // otherwise, return the data - if (mode) *mode = cb_cfg_buffer_ptr[nIdx]->isWaveform[channel][trigNum].mode; - if (repeats) *repeats = cb_cfg_buffer_ptr[nIdx]->isWaveform[channel][trigNum].repeats; - if (trig) *trig = cb_cfg_buffer_ptr[nIdx]->isWaveform[channel][trigNum].trig; - if (trigChan) *trigChan = cb_cfg_buffer_ptr[nIdx]->isWaveform[channel][trigNum].trigChan; - if (trigValue) *trigValue = cb_cfg_buffer_ptr[nIdx]->isWaveform[channel][trigNum].trigValue; - if (wave) *wave = cb_cfg_buffer_ptr[nIdx]->isWaveform[channel][trigNum].wave; - return cbRESULT_OK; -} - - -cbRESULT cbGetAoutWaveformNumber(const uint32_t channel, uint32_t* wavenum, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - uint32_t nWaveNum = 0; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[channel - 1].chancaps & cbCHAN_AOUT)) return cbRESULT_INVALIDFUNCTION; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[channel - 1].aoutcaps & cbAOUT_WAVEFORM)) return cbRESULT_INVALIDFUNCTION; - - for (uint32_t nChan = 0; nChan < cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans(); ++nChan) - { - if (IsChanAnalogOut(nChan) || IsChanAudioOut(nChan)) - { - if (nChan == channel) - { - break; - } - ++nWaveNum; - } - } - - if (nWaveNum >= AOUT_NUM_GAIN_CHANS) return cbRESULT_INVALIDCHANNEL; - - // otherwise, return the data - if (wavenum) *wavenum = nWaveNum; - return cbRESULT_OK; -} - - -cbRESULT cbGetFilterDesc(const uint32_t proc, const uint32_t filt, cbFILTDESC *filtdesc, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid and that requested procinfo structure is not empty - if ((proc - 1) >= cbMAXPROCS) return cbRESULT_INVALIDADDRESS; - if ((filt-1) >= cbMAXFILTS) return cbRESULT_INVALIDADDRESS; - if (cb_cfg_buffer_ptr[nIdx]->filtinfo[proc-1][filt-1].cbpkt_header.chid == 0) return cbRESULT_INVALIDADDRESS; - - // otherwise, return the data - memcpy(filtdesc,&(cb_cfg_buffer_ptr[nIdx]->filtinfo[proc-1][filt-1].label[0]),sizeof(cbFILTDESC)); - return cbRESULT_OK; -} - -cbRESULT cbGetFileInfo(cbPKT_FILECFG * filecfg, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - if (cb_cfg_buffer_ptr[nIdx]->fileinfo.cbpkt_header.chid == 0) return cbRESULT_HARDWAREOFFLINE; - - // otherwise, return the data - if (filecfg) *filecfg = cb_cfg_buffer_ptr[nIdx]->fileinfo; - return cbRESULT_OK; -} - -cbRESULT cbGetSampleGroupInfo(const uint32_t proc, const uint32_t group, char *label, uint32_t *period, uint32_t* length, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid and that requested procinfo structure is not empty - if (((proc - 1) >= cbMAXPROCS)||((group - 1) >= cbMAXGROUPS)) return cbRESULT_INVALIDADDRESS; - if (cb_cfg_buffer_ptr[nIdx]->groupinfo[proc - 1][group - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDADDRESS; - - // otherwise, return the data - if (label) memcpy(label,cb_cfg_buffer_ptr[nIdx]->groupinfo[proc-1][group-1].label, cbLEN_STR_LABEL); - if (period) *period = cb_cfg_buffer_ptr[nIdx]->groupinfo[proc-1][group-1].period; - if (length) *length = cb_cfg_buffer_ptr[nIdx]->groupinfo[proc-1][group-1].length; - return cbRESULT_OK; -} - - -cbRESULT cbGetSampleGroupList(const uint32_t proc, const uint32_t group, uint32_t *length, uint16_t *list, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) - return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid and that requested procinfo structure is not empty - if (((proc - 1) >= cbMAXPROCS)||((group - 1) >= cbMAXGROUPS)) - return cbRESULT_INVALIDADDRESS; - if (cb_cfg_buffer_ptr[nIdx]->groupinfo[proc - 1][group - 1].cbpkt_header.chid == 0) - return cbRESULT_INVALIDADDRESS; - - // otherwise, return the data - if (length) - *length = cb_cfg_buffer_ptr[nIdx]->groupinfo[proc - 1][group - 1].length; - - if (list) - memcpy(list, &(cb_cfg_buffer_ptr[nIdx]->groupinfo[proc-1][group-1].list[0]), - cb_cfg_buffer_ptr[nIdx]->groupinfo[proc-1][group-1].length * sizeof(cb_cfg_buffer_ptr[nIdx]->groupinfo[proc-1][group-1].list[0])); - - return cbRESULT_OK; -} - -cbRESULT cbGetChanLoc(const uint32_t chan, uint32_t *proc, uint32_t *bank, char *banklabel, uint32_t *term, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the addresses are valid and that requested procinfo structure is not empty - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - const uint32_t nProcessor = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].proc; - const uint32_t nBank = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].bank; - - // Return the requested data from the rec buffer - if (proc) *proc = nProcessor; // 1 based - if (bank) *bank = nBank; // 1 based - if (banklabel) - memcpy(banklabel, - cb_cfg_buffer_ptr[nIdx]->bankinfo[nProcessor-1][nBank-1].label, - cbLEN_STR_LABEL); - - if (term) - *term = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].term; - - return cbRESULT_OK; -} - -cbRESULT cbGetChanCaps(const uint32_t chan, uint32_t *chancaps, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (chancaps) *chancaps = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps; - - return cbRESULT_OK; -} - -cbRESULT cbGetChanLabel(const uint32_t chan, char *label, uint32_t *userflags, int32_t *position,const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (label) memcpy(label,cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].label, cbLEN_STR_LABEL); - if (userflags) *userflags = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].userflags; - if (position) memcpy(position,&(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].position[0]),4*sizeof(int32_t)); - - return cbRESULT_OK; -} - -cbRESULT cbSetChanLabel(const uint32_t chan, const char *label, const uint32_t userflags, const int32_t *position, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETLABEL; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - memcpy(chaninfo.label, label, cbLEN_STR_LABEL); - chaninfo.userflags = userflags; - if (position) - memcpy(&chaninfo.position, position, 4 * sizeof(int32_t)); - else - memcpy(&chaninfo.position, &(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].position[0]), 4 * sizeof(int32_t)); - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - - -cbRESULT cbGetChanUnitMapping(const uint32_t chan, cbMANUALUNITMAPPING *unitmapping, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (unitmapping) - memcpy(unitmapping, &cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].unitmapping[0], cbMAXUNITS * sizeof(cbMANUALUNITMAPPING)); - - return cbRESULT_OK; -} - - -cbRESULT cbSetChanUnitMapping(const uint32_t chan, const cbMANUALUNITMAPPING *unitmapping, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // if null pointer, nothing to do so return cbRESULT_OK - if (!unitmapping) return cbRESULT_OK; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETUNITOVERRIDES; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - if (unitmapping) - memcpy(&chaninfo.unitmapping, unitmapping, cbMAXUNITS * sizeof(cbMANUALUNITMAPPING)); - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - - -cbRESULT cbGetChanNTrodeGroup(const uint32_t chan, uint32_t *NTrodeGroup, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (NTrodeGroup) *NTrodeGroup = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkgroup; - - return cbRESULT_OK; -} - - -cbRESULT cbSetChanNTrodeGroup(const uint32_t chan, const uint32_t NTrodeGroup, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETNTRODEGROUP; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFOSHORT; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - if (0 == NTrodeGroup) - chaninfo.spkgroup = 0; - else - chaninfo.spkgroup = cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[NTrodeGroup - 1].ntrode; //NTrodeGroup; - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -cbRESULT cbGetChanAmplitudeReject(const uint32_t chan, cbAMPLITUDEREJECT *AmplitudeReject, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (AmplitudeReject) - { - AmplitudeReject->bEnabled = cbAINPSPK_REJAMPL == (cbAINPSPK_REJAMPL & cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkopts); - AmplitudeReject->nAmplPos = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].amplrejpos; - AmplitudeReject->nAmplNeg = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].amplrejneg; - } - - return cbRESULT_OK; -} - - -cbRESULT cbSetChanAmplitudeReject(const uint32_t chan, const cbAMPLITUDEREJECT AmplitudeReject, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETREJECTAMPLITUDE; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFOSHORT; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.spkopts = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkopts & ~cbAINPSPK_REJAMPL; - chaninfo.spkopts |= AmplitudeReject.bEnabled ? cbAINPSPK_REJAMPL : 0; - chaninfo.amplrejpos = AmplitudeReject.nAmplPos; - chaninfo.amplrejneg = AmplitudeReject.nAmplNeg; - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -// Author & Date: Ehsan Azar 22 Jan 2013 -// Purpose: Get full channel config -// Inputs: -// chan - channel number (1-based) -// Outputs: -// chaninfo - shared segment size to create -// Returns the error code -cbRESULT cbGetChanInfo(const uint32_t chan, cbPKT_CHANINFO *pChanInfo, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (pChanInfo) memcpy(pChanInfo, &(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1]), sizeof(cbPKT_CHANINFO)); - - return cbRESULT_OK; -} - -cbRESULT cbGetChanAutoThreshold(const uint32_t chan, uint32_t *bEnabled, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (bEnabled) - *bEnabled = (cbAINPSPK_THRAUTO == (cbAINPSPK_THRAUTO & cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkopts)); - - return cbRESULT_OK; -} - - -cbRESULT cbSetChanAutoThreshold(const uint32_t chan, const uint32_t bEnabled, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // if null pointer, nothing to do so return cbRESULT_OK - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETAUTOTHRESHOLD; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFOSHORT; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.spkopts = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkopts & ~cbAINPSPK_THRAUTO; - chaninfo.spkopts |= bEnabled ? cbAINPSPK_THRAUTO : 0; - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - - -cbRESULT cbGetNTrodeInfo(const uint32_t ntrode, char *label, cbMANUALUNITMAPPING ellipses[][cbMAXUNITS], - uint16_t * nSite, uint16_t * chans, uint16_t * fs, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the NTrode number is valid and initialized - if ((ntrode - 1) >= cbMAXNTRODES) return cbRESULT_INVALIDNTRODE; - if (cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode-1].cbpkt_header.chid == 0) return cbRESULT_INVALIDNTRODE; - - if (label) memcpy(label, cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].label, cbLEN_STR_LABEL); - int n_size = sizeof(cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].ellipses); - if (ellipses) memcpy(ellipses, &cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].ellipses[0][0], n_size); - if (nSite) *nSite = cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].nSite; - if (chans) memcpy(chans, cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].nChan, cbMAXSITES * sizeof(uint16_t)); - if (fs) *fs = cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].fs; - return cbRESULT_OK; -} - -cbRESULT cbSetNTrodeInfo( const uint32_t ntrode, const char *label, cbMANUALUNITMAPPING ellipses[][cbMAXUNITS], const uint16_t fs, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the NTrode number is valid and initialized - if ((ntrode - 1) >= cbMAXCHANS/2) return cbRESULT_INVALIDNTRODE; - - // Create the packet data structure and fill it in - cbPKT_NTRODEINFO ntrodeinfo; - ntrodeinfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - ntrodeinfo.cbpkt_header.chid = 0x8000; - ntrodeinfo.cbpkt_header.type = cbPKTTYPE_SETNTRODEINFO; - ntrodeinfo.cbpkt_header.dlen = cbPKTDLEN_NTRODEINFO; - ntrodeinfo.ntrode = cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].ntrode; - ntrodeinfo.fs = fs; - if (label) memcpy(ntrodeinfo.label, label, sizeof(ntrodeinfo.label)); - int size_ell = sizeof(ntrodeinfo.ellipses); - if (ellipses) - memcpy(ntrodeinfo.ellipses, ellipses, size_ell); - else - memset(ntrodeinfo.ellipses, 0, size_ell); - - return cbSendPacketToInstrument(&ntrodeinfo, nInstance, cbGetNTrodeInstrument(ntrode) - 1); -} - - -/// @author Hyrum L. Sessions -/// @date May the Forth be with you 2020 -/// @brief Set the N-Trode label without affecting other data -cbRESULT cbSetNTrodeLabel(const uint32_t ntrode, const char* label, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the NTrode number is valid and initialized - if ((ntrode - 1) >= cbMAXCHANS / 2) return cbRESULT_INVALIDNTRODE; - - // Create the packet data structure and fill it in - cbPKT_NTRODEINFO ntrodeinfo; - ntrodeinfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - ntrodeinfo.cbpkt_header.chid = 0x8000; - ntrodeinfo.cbpkt_header.type = cbPKTTYPE_SETNTRODEINFO; - ntrodeinfo.cbpkt_header.dlen = cbPKTDLEN_NTRODEINFO; - ntrodeinfo.ntrode = cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].ntrode; - ntrodeinfo.fs = cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].fs; - if (label) memcpy(ntrodeinfo.label, label, sizeof(ntrodeinfo.label)); - int size_ell = sizeof(ntrodeinfo.ellipses); - memcpy(ntrodeinfo.ellipses, cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[ntrode - 1].ellipses, size_ell); - - return cbSendPacketToInstrument(&ntrodeinfo, nInstance, cbGetNTrodeInstrument(ntrode) - 1); -} - - -// Purpose: Digital Input Inquiry and Configuration Functions -// -cbRESULT cbGetDinpCaps(const uint32_t chan, uint32_t *dinpcaps, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (dinpcaps) *dinpcaps = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].dinpcaps; - - return cbRESULT_OK; -} - -// Purpose: Digital Input Inquiry and Configuration Functions -// -cbRESULT cbGetDinpOptions(const uint32_t chan, uint32_t *options, uint32_t *eopchar, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_DINP)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the rec buffer - if (options) *options = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].dinpopts; - if (eopchar) *eopchar = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].eopchar; - - return cbRESULT_OK; -} - - -// Purpose: Digital Input Inquiry and Configuration Functions -// -cbRESULT cbSetDinpOptions(const uint32_t chan, const uint32_t options, const uint32_t eopchar, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_DINP)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETDINP; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.dinpopts = options; // digital input options (composed of nmDINP_* flags) - chaninfo.eopchar = eopchar; // digital input capablities (given by nmDINP_* flags) - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -// Purpose: Digital Output Inquiry and Configuration Functions -// -cbRESULT cbGetDoutCaps(const uint32_t chan, uint32_t *doutcaps, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (doutcaps) *doutcaps = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].doutcaps; - - return cbRESULT_OK; -} - -// Purpose: Digital Output Inquiry and Configuration Functions -// -cbRESULT cbGetDoutOptions(const uint32_t chan, uint32_t *options, uint32_t *monchan, int32_t *doutval, - uint8_t *triggertype, uint16_t *trigchan, uint16_t *trigval, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_DOUT)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the rec buffer - if (options) *options = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].doutopts; - if (monchan) - { - if ((cbDOUT_FREQUENCY & cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].doutopts) || - (cbDOUT_TRIGGERED & cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].doutopts)) - { -#ifdef CBPROTO_311 - *monchan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].monsource; - } - else { - *monchan = cbGetExpandedChannelNumber(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].monsource&0xfff, - (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].monsource >> 16)&0xfff); - } -#else - *monchan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].moninst | (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].monchan << 16); - } - else { - *monchan = cbGetExpandedChannelNumber(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].moninst + 1, - cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].monchan); - } -#endif - } - if (doutval) *doutval = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].outvalue; - if (triggertype) *triggertype = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].trigtype; - if (trigchan) *trigchan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].trigchan; - if (trigval) *trigval = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].trigval; - - return cbRESULT_OK; -} - -// Purpose: Digital Output Inquiry and Configuration Functions -// -cbRESULT cbSetDoutOptions(const uint32_t chan, const uint32_t options, uint32_t monchan, const int32_t doutval, - const uint8_t triggertype, const uint16_t trigchan, const uint16_t trigval, const uint32_t nInstance) -{ - cbRESULT nResult = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_DOUT)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETDOUT; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.doutopts = options; - if ((cbDOUT_FREQUENCY & options) || (cbDOUT_TRIGGERED & options)) - { -#ifdef CBPROTO_311 - chaninfo.monsource = monchan; -#else - chaninfo.moninst = monchan & 0xFFFF; - chaninfo.monchan = (monchan >> 16) & 0xFFFF; -#endif - } - else - { - if (0 != monchan) - { -#ifdef CBPROTO_311 - chaninfo.monsource = cbGetInstrumentLocalChannelNumber(monchan); -#else - chaninfo.moninst = cbGetChanInstrument(monchan) - 1; - chaninfo.monchan = cbGetInstrumentLocalChannelNumber(monchan); -#endif - } - } - chaninfo.outvalue = doutval; - chaninfo.trigtype = triggertype; - chaninfo.trigchan = trigchan; - chaninfo.trigval = trigval; - - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == nResult) && (NSP_FOUND == cbGetNspStatus(nProc))) - nResult = cbSendPacketToInstrument(&chaninfo, nInstance, nProc - 1); - } - return nResult; -} - - -// Purpose: Analog Input Inquiry and Configuration Functions -// -cbRESULT cbGetAinpCaps(const uint32_t chan, uint32_t *ainpcaps, cbSCALING *physcalin, cbFILTDESC *phyfiltin, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (ainpcaps) *ainpcaps = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpcaps; - if (physcalin) *physcalin = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].physcalin; - if (phyfiltin) *phyfiltin = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].phyfiltin; - - return cbRESULT_OK; -} - - -// Purpose: Analog Input Inquiry and Configuration Functions -// -cbRESULT cbGetAinpOpts(const uint32_t chan, uint32_t *ainpopts, uint32_t *LNCrate, uint32_t *refElecChan, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpcaps & cbAINP_LNC)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the rec buffer - if (ainpopts) *ainpopts = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpopts; - if (LNCrate) *LNCrate = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].lncrate; - if (refElecChan) *refElecChan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].refelecchan; - - return cbRESULT_OK; -} - - -// Purpose: Analog Input Inquiry and Configuration Functions -// -cbRESULT cbSetAinpOpts(const uint32_t chan, const uint32_t ainpopts, const uint32_t LNCrate, const uint32_t refElecChan, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpcaps & cbAINP_LNC)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETAINP; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFOSHORT; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.ainpopts = ainpopts; - chaninfo.lncrate = LNCrate; - chaninfo.refelecchan = refElecChan; - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -// Purpose: Analog Input Inquiry and Configuration Functions -// -cbRESULT cbGetAinpScaling(const uint32_t chan, cbSCALING *scaling, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AINP)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the rec buffer - if (scaling) *scaling = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].scalin; - return cbRESULT_OK; -} - - -// Purpose: Analog Input Inquiry and Configuration Functions -// -cbRESULT cbSetAinpScaling(const uint32_t chan, const cbSCALING *scaling, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AINP)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETSCALE; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.scalin = *scaling; - chaninfo.scalout = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].scalout; - - // Enter the packet into the XMT buffer queue - return cbSendPacketToInstrument(&chaninfo, nInstance, cbGetChanInstrument(chan) - 1); -} - -// Purpose: Analog Input Inquiry and Configuration Functions -// -cbRESULT cbGetAinpDisplay(uint32_t chan, int32_t *smpdispmin, int32_t *smpdispmax, int32_t *spkdispmax, int32_t *lncdispmax, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if ((cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AINP) != cbCHAN_AINP) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the cfg buffer - if (smpdispmin) *smpdispmin = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].smpdispmin; - if (smpdispmax) *smpdispmax = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].smpdispmax; - if (spkdispmax) *spkdispmax = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkdispmax; - if (lncdispmax) *lncdispmax = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].lncdispmax; - - return cbRESULT_OK; -} - - -// Purpose: Analog Input Inquiry and Configuration Functions -// -cbRESULT cbSetAinpDisplay( - const uint32_t chan, - const int32_t smpdispmin, const int32_t smpdispmax, const int32_t spkdispmax, const int32_t lncdispmax, - const uint32_t nInstance -) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETDISP; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - if (smpdispmin) chaninfo.smpdispmin = smpdispmin; - else chaninfo.smpdispmin = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].smpdispmin; - if (smpdispmax) chaninfo.smpdispmax = smpdispmax; - else chaninfo.smpdispmax = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].smpdispmax; - if (spkdispmax) chaninfo.spkdispmax = spkdispmax; - else chaninfo.spkdispmax = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkdispmax; - if (lncdispmax) chaninfo.lncdispmax = lncdispmax; - else chaninfo.lncdispmax = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].lncdispmax; - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - - -// Purpose: Analog Input Inquiry and Configuration Functions -// -cbRESULT cbSetAinpPreview(const uint32_t chan, const int32_t prevopts, const uint32_t nInstance) -{ - cbRESULT res = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - ASSERT(prevopts == cbAINPPREV_LNC || - prevopts == cbAINPPREV_STREAM || - prevopts == cbAINPPREV_ALL ); - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - // chan == 0 means a request for ALL possible channels, - // the NSP will find the good channels, so we don't have - // to worry about the testing - if (chan != 0) - { - if ((chan - 1) >= cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans()) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AINP)) return cbRESULT_INVALIDFUNCTION; - } - - if ( ! - (prevopts == cbAINPPREV_LNC || - prevopts == cbAINPPREV_STREAM || - prevopts == cbAINPPREV_ALL )) - { - return cbRESULT_INVALIDFUNCTION; - } - - // Create the packet data structure and fill it in - cbPKT_GENERIC packet; - packet.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - packet.cbpkt_header.type = prevopts; - packet.cbpkt_header.dlen = 0; - if (0 == chan) - { - packet.cbpkt_header.chid = 0x8000; - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == res) && (NSP_FOUND == cbGetNspStatus(nProc))) - res = cbSendPacketToInstrument(&packet, nInstance, nProc - 1); - } - } - else - { - packet.cbpkt_header.chid = 0x8000 + cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - // Enter the packet into the XMT buffer queue - res = cbSendPacketToInstrument(&packet, nInstance, cbGetChanInstrument(chan) - 1); - } - - return res; -} - -// Purpose: AINP Sampling Stream Functions -cbRESULT cbGetAinpSampling(uint32_t chan, uint32_t *filter, uint32_t *group, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpcaps & cbAINP_SMPSTREAM)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the rec buffer for non-null pointers - if (group) *group = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].smpgroup; - if (filter) *filter = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].smpfilter; - - return cbRESULT_OK; -} - -// Purpose: AINP Sampling Stream Functions -cbRESULT cbSetAinpSampling(uint32_t chan, uint32_t filter, uint32_t group, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpcaps & cbAINP_SMPSTREAM)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETSMP; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.smpfilter = filter; - chaninfo.smpgroup = group; - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -// Purpose: AINP Spike Stream Functions -cbRESULT cbGetAinpSpikeCaps(uint32_t chan, uint32_t *spkcaps, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpcaps & cbAINP_SPKSTREAM)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the cfg buffer - if (spkcaps) *spkcaps = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkcaps; - - return cbRESULT_OK; -} - -// Purpose: AINP Spike Stream Functions -cbRESULT cbGetAinpSpikeOptions(uint32_t chan, uint32_t *options, uint32_t *filter, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpcaps & cbAINP_SPKSTREAM)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the cfg buffer - if (options) *options = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkopts; - if (filter) *filter = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkfilter; - - return cbRESULT_OK; -} - -// Purpose: AINP Spike Stream Functions -cbRESULT cbSetAinpSpikeOptions(uint32_t chan, uint32_t spkopts, uint32_t spkfilter, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].ainpcaps & cbAINP_SPKSTREAM)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETSPK; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.spkopts = spkopts; - chaninfo.spkfilter = spkfilter; - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -// Purpose: AINP Spike Stream Functions -cbRESULT cbGetAinpSpikeThreshold(uint32_t chan, int32_t *spkthrlevel, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkcaps & cbAINPSPK_EXTRACT)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the cfg buffer - if (spkthrlevel) *spkthrlevel = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkthrlevel; - - return cbRESULT_OK; -} - -// Purpose: AINP Spike Stream Functions -cbRESULT cbSetAinpSpikeThreshold(uint32_t chan, int32_t spkthrlevel, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) - return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) - return cbRESULT_INVALIDCHANNEL; - - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) - return cbRESULT_INVALIDCHANNEL; - - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkcaps & cbAINPSPK_EXTRACT)) - return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETSPKTHR; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.spkthrlevel = spkthrlevel; - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -// Purpose: AINP Spike Stream Functions -cbRESULT cbGetAinpSpikeHoops(uint32_t chan, cbHOOP *hoops, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkcaps & cbAINPSPK_HOOPSORT)) return cbRESULT_INVALIDFUNCTION; - - memcpy(hoops, &(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkhoops[0][0]), - sizeof(cbHOOP)*cbMAXUNITS*cbMAXHOOPS ); - - // Return the requested data from the cfg buffer - return cbRESULT_OK; -} - -// Purpose: AINP Spike Stream Functions -cbRESULT cbSetAinpSpikeHoops(const uint32_t chan, const cbHOOP *hoops, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the channel address is valid and initialized - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].spkcaps & cbAINPSPK_HOOPSORT)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETSPKHPS; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - - memcpy(&(chaninfo.spkhoops[0][0]), hoops, sizeof(cbHOOP)*cbMAXUNITS*cbMAXHOOPS ); - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -#define nmAOUTCAP_AUDIO 0x00000001 // Channel is physically optimized for audio output -#define nmAOUTCAP_STATIC 0x00000002 // Output a static value -#define nmAOUTCAP_MONITOR 0x00000004 // Monitor an analog signal line -#define nmAOUTCAP_MONITORGAIN 0x00000008 // Channel can set gain to the channel -#define nmAOUTCAP_STIMULATE 0x00000010 // Stimulation waveform functions are available. - -// Purpose: Analog Output Inquiry and Configuration Functions -// -cbRESULT cbGetAoutCaps( uint32_t chan, uint32_t *aoutcaps, cbSCALING *physcalout, cbFILTDESC *phyfiltout, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the addresses are valid and that necessary structures are not empty - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - - // Return the requested data from the rec buffer - if (aoutcaps) *aoutcaps = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].aoutcaps; - if (physcalout) *physcalout = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].physcalout; - if (phyfiltout) *phyfiltout = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].phyfiltout; - - return cbRESULT_OK; -} - -// Purpose: Analog Output Inquiry and Configuration Functions -// -cbRESULT cbGetAoutScaling(uint32_t chan, cbSCALING *scalout, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the addresses are valid and that necessary structures are not empty - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AOUT)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the rec buffer - if (scalout) *scalout = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].scalout; - - return cbRESULT_OK; -} - -// Purpose: Analog Output Inquiry and Configuration Functions -// -cbRESULT cbSetAoutScaling(const uint32_t chan, const cbSCALING *scaling, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the addresses are valid and that necessary structures are not empty - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AOUT)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETSCALE; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; -#ifndef CBPROTO_311 - chaninfo.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.instrument; -#endif - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.scalin = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].scalin; - chaninfo.scalout = *scaling; - - // Enter the packet into the XMT buffer queue - return cbSendPacket(&chaninfo, nInstance); -} - -// Purpose: Analog Output Inquiry and Configuration Functions -// -cbRESULT cbGetAoutOptions(const uint32_t chan, uint32_t *options, uint32_t *monchan, int32_t *value, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the addresses are valid and that necessary structures are not empty - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AOUT)) return cbRESULT_INVALIDFUNCTION; - - // Return the requested data from the rec buffer - if (options) *options = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].aoutopts; -#ifdef CBPROTO_311 - if (monchan) *monchan = (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].monsource >> 16) & 0xFFFF; -#else - if (monchan) *monchan = cbGetExpandedChannelNumber(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].moninst + 1, cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].monchan); -#endif - if (value) *value = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].outvalue; - - return cbRESULT_OK; -} - -// Purpose: Analog Output Inquiry and Configuration Functions -// -cbRESULT cbSetAoutOptions(uint32_t chan, const uint32_t options, const uint32_t monchan, const int32_t value, const uint32_t nInstance) -{ - cbRESULT nResult = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the addresses are valid and that necessary structures are not empty - if ((chan - 1) >= cbMAXCHANS) return cbRESULT_INVALIDCHANNEL; - // If cb_cfg_buffer_ptr was built for 128-channel system, but passed in channel is for 256-channel firmware. - // TODO: Again, maybe we need a m_ChanIdxInType array. - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AOUT) && (chan > (cbNUM_FE_CHANS / 2))) - chan -= (cbNUM_FE_CHANS / 2); - if (cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].cbpkt_header.chid == 0) return cbRESULT_INVALIDCHANNEL; - if (!(cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chancaps & cbCHAN_AOUT)) return cbRESULT_INVALIDFUNCTION; - - // Create the packet data structure and fill it in - cbPKT_CHANINFO chaninfo; - chaninfo.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - chaninfo.cbpkt_header.chid = 0x8000; - chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETAOUT; - chaninfo.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; - chaninfo.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chan - 1].chan; - chaninfo.aoutopts = options; -#ifdef CBPROTO_311 - chaninfo.monsource = (0 == monchan) ? 0 : cbGetInstrumentLocalChannelNumber(monchan); -#else - chaninfo.moninst = (0 == monchan) ? 0 : cbGetChanInstrument(monchan) - 1; - chaninfo.monchan = (0 == monchan) ? 0 : cbGetInstrumentLocalChannelNumber(monchan); -#endif - chaninfo.outvalue = value; - - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == nResult) && (NSP_FOUND == cbGetNspStatus(nProc))) - nResult = cbSendPacketToInstrument(&chaninfo, nInstance, nProc - 1); - } - return nResult; -} - -// Author & Date: Kirk Korver 26 Apr 2005 -// Purpose: Request that the ENTIRE sorting model be updated -cbRESULT cbGetSortingModel(const uint32_t nInstance) -{ - cbRESULT ret = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Send out the request for the sorting rules - cbPKT_SS_MODELALLSET isPkt; - isPkt.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - isPkt.cbpkt_header.chid = 0x8000; - isPkt.cbpkt_header.type = cbPKTTYPE_SS_MODELALLSET; - isPkt.cbpkt_header.dlen = 0; - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == ret) && (NSP_FOUND == cbGetNspStatus(nProc))) - ret = cbSendPacketToInstrument(&isPkt, nInstance, nProc - 1); - } - - // FIXME: relying on sleep is racy, refactor the code - Sleep(250); // give the "model" packets a chance to show up - - return ret; -} - - -// Author & Date: Hyrum L. Sessions 22 Apr 2009 -// Purpose: Request that the ENTIRE sorting model be updated -cbRESULT cbGetFeatureSpaceDomain(const uint32_t nInstance) -{ - cbRESULT ret = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Send out the request for the sorting rules - cbPKT_FS_BASIS isPkt; - isPkt.cbpkt_header.time = cb_rec_buffer_ptr[nIdx]->lasttime; - isPkt.cbpkt_header.chid = 0x8000; - isPkt.cbpkt_header.type = cbPKTTYPE_FS_BASISSET; - isPkt.cbpkt_header.dlen = cbPKTDLEN_FS_BASISSHORT; - - isPkt.chan = 0; - isPkt.mode = cbBASIS_CHANGE; - isPkt.fs = cbAUTOALG_PCA; - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == ret) && (NSP_FOUND == cbGetNspStatus(nProc))) - ret = cbSendPacketToInstrument(&isPkt, nInstance, nProc - 1); - } - - // FIXME: relying on sleep is racy, refactor the code - Sleep(250); // give the packets a chance to show up - - return ret; -} - -void GetAxisLengths(const cbPKT_SS_NOISE_BOUNDARY* pPkt, float afAxisLen[3]) -{ - afAxisLen[0] = sqrt(pPkt->afS[0][0] * pPkt->afS[0][0] + - pPkt->afS[0][1] * pPkt->afS[0][1] + - pPkt->afS[0][2] * pPkt->afS[0][2]); - afAxisLen[1] = sqrt(pPkt->afS[1][0] * pPkt->afS[1][0] + - pPkt->afS[1][1] * pPkt->afS[1][1] + - pPkt->afS[1][2] * pPkt->afS[1][2]); - afAxisLen[2] = sqrt(pPkt->afS[2][0] * pPkt->afS[2][0] + - pPkt->afS[2][1] * pPkt->afS[2][1] + - pPkt->afS[2][2] * pPkt->afS[2][2]); -} - -void GetRotationAngles(const cbPKT_SS_NOISE_BOUNDARY* pPkt, float afTheta[3]) -{ - const Vector3f major(pPkt->afS[0]); - const Vector3f minor_1(pPkt->afS[1]); - const Vector3f minor_2(pPkt->afS[2]); - - ::GetRotationAngles(major, minor_1, minor_2, afTheta); -} - -// Author & Date: Jason Scott 23 Jan 2009 -// Purpose: Get the noise boundary parameters -// Inputs: -// chanIdx - channel number (1-based) -// Outputs: -// centroid - the center of an ellipsoid -// major - major axis of the ellipsoid -// minor_1 - first minor axis of the ellipsoid -// minor_2 - second minor axis of the ellipsoid -// cbRESULT_OK if life is good -cbRESULT cbSSGetNoiseBoundary(const uint32_t chanIdx, float afCentroid[3], float afMajor[3], float afMinor_1[3], float afMinor_2[3], const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - if ((chanIdx - 1) >= cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans()) return cbRESULT_INVALIDCHANNEL; - if (!IsChanAnalogIn(chanIdx)) return cbRESULT_INVALIDCHANNEL; - - cbPKT_SS_NOISE_BOUNDARY const & rPkt = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktNoiseBoundary[chanIdx - 1]; - if (afCentroid) - { - afCentroid[0] = rPkt.afc[0]; - afCentroid[1] = rPkt.afc[1]; - afCentroid[2] = rPkt.afc[2]; - } - - if (afMajor) - { - afMajor[0] = rPkt.afS[0][0]; - afMajor[1] = rPkt.afS[0][1]; - afMajor[2] = rPkt.afS[0][2]; - } - - if (afMinor_1) - { - afMinor_1[0] = rPkt.afS[1][0]; - afMinor_1[1] = rPkt.afS[1][1]; - afMinor_1[2] = rPkt.afS[1][2]; - } - - if (afMinor_2) - { - afMinor_2[0] = rPkt.afS[2][0]; - afMinor_2[1] = rPkt.afS[2][1]; - afMinor_2[2] = rPkt.afS[2][2]; - } - - return cbRESULT_OK; -} - -// Author & Date: Hyrum Sessions 17 January 2023 -// Purpose: Initialize SS Noise Boundary packet -void InitPktSSNoiseBoundary( - cbPKT_SS_NOISE_BOUNDARY* pPkt, const uint32_t chan, - const float cen1, const float cen2, const float cen3, - const float maj1, const float maj2, const float maj3, - const float min11, const float min12, const float min13, - const float min21, const float min22, const float min23 -) -{ - pPkt->cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - pPkt->cbpkt_header.type = cbPKTTYPE_SS_NOISE_BOUNDARYSET; - pPkt->cbpkt_header.dlen = cbPKTDLEN_SS_NOISE_BOUNDARY; - pPkt->chan = chan; - pPkt->afc[0] = cen1; - pPkt->afc[1] = cen2; - pPkt->afc[2] = cen3; - pPkt->afS[0][0] = maj1; - pPkt->afS[0][1] = maj2; - pPkt->afS[0][2] = maj3; - pPkt->afS[1][0] = min11; - pPkt->afS[1][1] = min12; - pPkt->afS[1][2] = min13; - pPkt->afS[2][0] = min21; - pPkt->afS[2][1] = min22; - pPkt->afS[2][2] = min23; -} - -// Author & Date: Jason Scott 23 Jan 2009 -// Purpose: Set the noise boundary parameters -// Inputs: -// chanIdx - channel number (1-based) -// centroid - the center of an ellipsoid -// major - major axis of the ellipsoid -// minor_1 - first minor axis of the ellipsoid -// minor_2 - second minor axis of the ellipsoid -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSSSetNoiseBoundary(const uint32_t chanIdx, float afCentroid[3], float afMajor[3], float afMinor_1[3], float afMinor_2[3], const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - if ((chanIdx - 1) >= cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans()) return cbRESULT_INVALIDCHANNEL; - if (!IsChanAnalogIn(chanIdx)) return cbRESULT_INVALIDCHANNEL; - - cbPKT_SS_NOISE_BOUNDARY icPkt; - InitPktSSNoiseBoundary(&icPkt, chanIdx, afCentroid[0], afCentroid[1], afCentroid[2], - afMajor[0], afMajor[1], afMajor[2], - afMinor_1[0], afMinor_1[1], afMinor_1[2], - afMinor_2[0], afMinor_2[1], afMinor_2[2]); - icPkt.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chanIdx - 1].chan; -#ifndef CBPROTO_311 - icPkt.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chanIdx - 1].cbpkt_header.instrument; -#endif - - return cbSendPacket(&icPkt, nInstance); -} - -// Author & Date: Jason Scott August 7 2009 -// Purpose: Get the noise boundary center, axis lengths, and rotation angles -// Inputs: -// chanIdx - channel number (1-based) -// centroid - the center of an ellipsoid -// axisLen - lengths of the major, first minor, and second minor axes (in that order) -// theta - angles of rotation around the x-axis, y-axis, and z-axis (in that order) -// Rotations should be performed in that order (yes, it matters) -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSSGetNoiseBoundaryByTheta(const uint32_t chanIdx, float afCentroid[3], float afAxisLen[3], float afTheta[3], const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - if ((chanIdx - 1) >= cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans()) return cbRESULT_INVALIDCHANNEL; - if (!IsChanAnalogIn(chanIdx)) return cbRESULT_INVALIDCHANNEL; - - // get noise boundary info - const cbPKT_SS_NOISE_BOUNDARY & rPkt = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktNoiseBoundary[chanIdx - 1]; - - // move over the centroid info - if (afCentroid) - { - afCentroid[0] = rPkt.afc[0]; - afCentroid[1] = rPkt.afc[1]; - afCentroid[2] = rPkt.afc[2]; - } - - // calculate the lengths - if(afAxisLen) - { - GetAxisLengths(&rPkt, afAxisLen); - } - - // calculate the rotation angels - if(afTheta) - { - GetRotationAngles(&rPkt, afTheta); - } - - return cbRESULT_OK; -} - -// Author & Date: Jason Scott August 7 2009 -// Purpose: Set the noise boundary via center, axis lengths, and rotation angles -// Inputs: -// chanIdx - channel number (1-based) -// centroid - the center of an ellipsoid -// axisLen - lengths of the major, first minor, and second minor axes (in that order) -// theta - angles of rotation around the x-axis, y-axis, and z-axis (in that order) -// Rotations will be performed in that order (yes, it matters) -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSSSetNoiseBoundaryByTheta(const uint32_t chanIdx, const float afCentroid[3], const float afAxisLen[3], const float afTheta[3], const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - if ((chanIdx - 1) >= cb_pc_status_buffer_ptr[0]->cbGetNumTotalChans()) return cbRESULT_INVALIDCHANNEL; - if (!IsChanAnalogIn(chanIdx)) return cbRESULT_INVALIDCHANNEL; - - // TODO: must be implemented for non MSC -#ifndef QT_APP - // initialize the axes on the coordinate axes - Vector3f major(afAxisLen[0], 0, 0); - Vector3f minor_1(0, afAxisLen[1], 0); - Vector3f minor_2(0, 0, afAxisLen[2]); - - // rotate the axes - ApplyRotationAngles(major, afTheta[0], afTheta[1], afTheta[2]); - ApplyRotationAngles(minor_1, afTheta[0], afTheta[1], afTheta[2]); - ApplyRotationAngles(minor_2, afTheta[0], afTheta[1], afTheta[2]); - - // Create the packet - cbPKT_SS_NOISE_BOUNDARY icPkt = {}; - InitPktSSNoiseBoundary(&icPkt, chanIdx, - afCentroid[0], afCentroid[1], afCentroid[2], - major[0], major[1], major[2], - minor_1[0], minor_1[1], minor_1[2], - minor_2[0], minor_2[1], minor_2[2]); - icPkt.chan = cb_cfg_buffer_ptr[nIdx]->chaninfo[chanIdx - 1].chan; - icPkt.cbpkt_header.instrument = cb_cfg_buffer_ptr[nIdx]->chaninfo[chanIdx - 1].cbpkt_header.instrument; - // Send it - return cbSendPacket(&icPkt, nInstance); -#else - return 0; -#endif - -} - -// Author & Date: Kirk Korver 21 Jun 2005 -// Purpose: Getting spike sorting statistics (nullptr = don't want that value) -// Outputs: -// cbRESULT_OK if life is good -// -// pfFreezeMinutes - how many minutes until the number of units is "frozen" -// pnUpdateSpikes - update rate in spike counts -// pfUpdateMinutes - update rate in minutes -// pfMinClusterSpreadFactor - larger number = more apt to combine 2 clusters into 1 -// pfMaxSubclusterSpreadFactor - larger number = less apt to split because of 2 clusers -// fMinClusterHistCorrMajMeasure - larger number = more apt to split 1 cluster into 2 -// fMaxClusterPairHistCorrMajMeasure - larger number = less apt to combine 2 clusters into 1 -// fClusterHistMajValleyPercentage - larger number = less apt to split nearby clusters -// fClusterHistMajPeakPercentage - larger number = less apt to split separated clusters -cbRESULT cbSSGetStatistics(uint32_t * pnUpdateSpikes, uint32_t * pnAutoalg, uint32_t * pnMode, - float * pfMinClusterPairSpreadFactor, - float * pfMaxSubclusterSpreadFactor, - float * pfMinClusterHistCorrMajMeasure, - float * pfMaxClusterPairHistCorrMajMeasure, - float * pfClusterHistValleyPercentage, - float * pfClusterHistClosePeakPercentage, - float * pfClusterHistMinPeakPercentage, - uint32_t * pnWaveBasisSize, - uint32_t * pnWaveSampleSize, - const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_SS_STATISTICS const & rPkt = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktStatistics; - - if (pnUpdateSpikes) *pnUpdateSpikes = rPkt.nUpdateSpikes; - - if (pnAutoalg) *pnAutoalg = rPkt.nAutoalg; - if (pnMode) *pnMode = rPkt.nMode; - - if (pfMinClusterPairSpreadFactor) *pfMinClusterPairSpreadFactor = rPkt.fMinClusterPairSpreadFactor; - if (pfMaxSubclusterSpreadFactor) *pfMaxSubclusterSpreadFactor = rPkt.fMaxSubclusterSpreadFactor; - - if (pfMinClusterHistCorrMajMeasure) *pfMinClusterHistCorrMajMeasure = rPkt.fMinClusterHistCorrMajMeasure; - if (pfMaxClusterPairHistCorrMajMeasure) *pfMaxClusterPairHistCorrMajMeasure = rPkt.fMaxClusterPairHistCorrMajMeasure; - - if (pfClusterHistValleyPercentage) *pfClusterHistValleyPercentage = rPkt.fClusterHistValleyPercentage; - if (pfClusterHistClosePeakPercentage) *pfClusterHistClosePeakPercentage = rPkt.fClusterHistClosePeakPercentage; - if (pfClusterHistMinPeakPercentage) *pfClusterHistMinPeakPercentage = rPkt.fClusterHistMinPeakPercentage; - - if (pnWaveBasisSize) *pnWaveBasisSize = rPkt.nWaveBasisSize; - if (pnWaveSampleSize) *pnWaveSampleSize = rPkt.nWaveSampleSize; - - return cbRESULT_OK; -} - - -// Author & Date: Kirk Korver 21 Jun 2005 -// Purpose: Setting spike sorting statistics -// Inputs: -// fFreezeMinutes - time (in minutes) at which to freeze the updating -// nUpdateSpikes - the update rate in spike counts -// fUpdateMinutes - the update rate in minutes -// fMinClusterSpreadFactor - larger number = more apt to combine 2 clusters into 1 -// fMaxSubclusterSpreadFactor - larger numbers = less apt to split because of 2 clusers -// fMinClusterHistCorrMajMeasure - larger number = more apt to split 1 cluster into 2 -// fMaxClusterPairHistCorrMajMeasure - larger number = less apt to combine 2 clusters into 1 -// fClusterHistMajValleyPercentage - larger number = less apt to split nearby clusters -// fClusterHistMajPeakPercentage - larger number = less apt to split separated clusters -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSSSetStatistics(uint32_t nUpdateSpikes, uint32_t nAutoalg, uint32_t nMode, - float fMinClusterPairSpreadFactor, - float fMaxSubclusterSpreadFactor, - float fMinClusterHistCorrMajMeasure, - float fMaxClusterMajHistCorrMajMeasure, - float fClusterHistValleyPercentage, - float fClusterHistClosePeakPercentage, - float fClusterHistMinPeakPercentage, - uint32_t nWaveBasisSize, - uint32_t nWaveSampleSize, - const uint32_t nInstance) -{ - cbRESULT cbRes = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_SS_STATISTICS icPkt; - - icPkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - icPkt.cbpkt_header.type = cbPKTTYPE_SS_STATISTICSSET; - icPkt.cbpkt_header.dlen = ((sizeof(cbPKT_SS_STATISTICS) / 4) - cbPKT_HEADER_32SIZE); - - icPkt.nUpdateSpikes = nUpdateSpikes; - icPkt.nAutoalg = nAutoalg; - icPkt.nMode = nMode; - icPkt.fMinClusterPairSpreadFactor = fMinClusterPairSpreadFactor; - icPkt.fMaxSubclusterSpreadFactor = fMaxSubclusterSpreadFactor; - icPkt.fMinClusterHistCorrMajMeasure = fMinClusterHistCorrMajMeasure; - icPkt.fMaxClusterPairHistCorrMajMeasure = fMaxClusterMajHistCorrMajMeasure; - icPkt.fClusterHistValleyPercentage = fClusterHistValleyPercentage; - icPkt.fClusterHistClosePeakPercentage = fClusterHistClosePeakPercentage; - icPkt.fClusterHistMinPeakPercentage = fClusterHistMinPeakPercentage; - icPkt.nWaveBasisSize = nWaveBasisSize; - icPkt.nWaveSampleSize = nWaveSampleSize; - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == cbRes) && (NSP_FOUND == cbGetNspStatus(nProc))) - cbRes = cbSendPacketToInstrument(&icPkt, nInstance, nProc - 1); - } - return cbRes; -} - -// Author & Date: Kirk Korver 21 Jun 2005 -// Purpose: set the artifact rejection parameters -// Outputs: -// pnMaxChans - the maximum number of channels that can fire within 48 samples -// pnRefractorySamples - num of samples (30 kHz) are "refractory" and thus ignored for detection -// cbRESULT_OK if life is good -cbRESULT cbSSGetArtifactReject(uint32_t * pnMaxChans, uint32_t * pnRefractorySamples, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_SS_ARTIF_REJECT const & rPkt = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktArtifReject; - *pnMaxChans = rPkt.nMaxSimulChans; - *pnRefractorySamples = rPkt.nRefractoryCount; - - return cbRESULT_OK; -} - -// Author & Date: Kirk Korver 21 Jun 2005 -// Inputs: -// nMaxChans - the maximum number of channels that can fire within 48 samples -// nRefractorySamples - num of samples (30 kHz) are "refractory" and thus ignored for detection -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSSSetArtifactReject(const uint32_t nMaxChans, const uint32_t nRefractorySamples, const uint32_t nInstance) -{ - cbRESULT cbRes = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_SS_ARTIF_REJECT isPkt = {}; - - isPkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - isPkt.cbpkt_header.type = cbPKTTYPE_SS_ARTIF_REJECTSET; - isPkt.cbpkt_header.dlen = cbPKTDLEN_SS_ARTIF_REJECT; - - isPkt.nMaxSimulChans = nMaxChans; - isPkt.nRefractoryCount = nRefractorySamples; - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == cbRes) && (NSP_FOUND == cbGetNspStatus(nProc))) - cbRes = cbSendPacketToInstrument(&isPkt, nInstance, nProc - 1); - } - return cbRes; -} - - -// Author & Date: Kirk Korver 21 Jun 2005 -// Purpose: get the spike detection parameters -// Outputs: -// pfThreshold - the base threshold value -// pfScaling - the threshold scaling factor -// cbRESULT_OK if life is good -cbRESULT cbSSGetDetect(float * pfThreshold, float * pfScaling, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_SS_DETECT const & rPkt = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktDetect; - if (pfThreshold) *pfThreshold = rPkt.fThreshold; - if (pfScaling) *pfScaling = rPkt.fMultiplier; - - return cbRESULT_OK; -} - -// Author & Date: Kirk Korver 21 Jun 2005 -// Purpose: set the spike detection parameters -// Inputs: -// pfThreshold - the base threshold value -// pfScaling - the threshold scaling factor -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSSSetDetect(const float fThreshold, const float fScaling, const uint32_t nInstance) -{ - cbRESULT cbRes = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_SS_DETECT isPkt = {}; - - isPkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - isPkt.cbpkt_header.type = cbPKTTYPE_SS_DETECTSET; - isPkt.cbpkt_header.dlen = ((sizeof(cbPKT_SS_DETECT) / 4) - cbPKT_HEADER_32SIZE); - - isPkt.fThreshold = fThreshold; - isPkt.fMultiplier = fScaling; - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == cbRes) && (NSP_FOUND == cbGetNspStatus(nProc))) - cbRes = cbSendPacketToInstrument(&isPkt, nInstance, nProc - 1); - } - return cbRes; -} - - -// Author & Date: Hyrum L. Sessions 18 Nov 2005 -// Purpose: get the spike sorting status -// Outputs: -// pnMode - 0=number of units is still adapting 1=number of units is frozen -// pfElapsedMinutes - this only makes sense if nMode=0 - minutes from start adapting -// cbRESULT_OK if life is good -cbRESULT cbSSGetStatus(cbAdaptControl * pcntlUnitStats, cbAdaptControl * pcntlNumUnits, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_SS_STATUS const & rPkt = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktStatus; - if (pcntlUnitStats) *pcntlUnitStats = rPkt.cntlUnitStats; - if (pcntlNumUnits) *pcntlNumUnits = rPkt.cntlNumUnits; - - return cbRESULT_OK; -} - - -// Author & Date: Dan Sebald 3 Dec 2005 -// Purpose: Setting spike sorting control status -// Inputs: -// cntlUnitStats - control/timer information for unit statistics adaptation -// cntlNumUnits - control/timer information for number of units -// Outputs: -// cbRESULT_OK if life is good -cbRESULT cbSSSetStatus(const cbAdaptControl cntlUnitStats, const cbAdaptControl cntlNumUnits, const uint32_t nInstance) -{ - cbRESULT cbRes = cbRESULT_OK; - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - cbPKT_SS_STATUS icPkt = {}; - - icPkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - icPkt.cbpkt_header.type = cbPKTTYPE_SS_STATUSSET; - icPkt.cbpkt_header.dlen = cbPKTDLEN_SS_STATUS; - - icPkt.cntlUnitStats = cntlUnitStats; - icPkt.cntlNumUnits = cntlNumUnits; - - // Send it to all NSPs - for (int nProc = cbNSP1; nProc <= cbMAXPROCS; ++nProc) - { - if ((cbRESULT_OK == cbRes) && (NSP_FOUND == cbGetNspStatus(nProc))) - cbRes = cbSendPacketToInstrument(&icPkt, nInstance, nProc - 1); - } - return cbRes; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Data checking and processing functions -// -// To get data from the shared memory buffers used in the Central App, the user can: -// 1) periodically poll for new data using a multimedia or windows timer -// 2) create a thread that uses a Win32 Event synchronization object to que the data polling -// -/////////////////////////////////////////////////////////////////////////////////////////////////// - - -cbRESULT cbCheckforData(cbLevelOfConcern & nLevelOfConcern, uint32_t *pktstogo /* = nullptr */, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Check for data loss by checking that - // [(head wraparound != Tail wraparound)AND((head index + (max_datagram_size/4)q) >= read index)] - // OR [head wraparound is more than twice ahead of the read pointer set] - if (((cb_rec_buffer_ptr[nIdx]->headwrap != cb_recbuff_tailwrap[nIdx]) && - ( - (cb_rec_buffer_ptr[nIdx]->headindex + (cbCER_UDP_SIZE_MAX / 4)) >= cb_recbuff_tailindex[nIdx])) || - (cb_rec_buffer_ptr[nIdx]->headwrap > (cb_recbuff_tailwrap[nIdx] + 1)) - ) - { - cbMakePacketReadingBeginNow(nInstance); - nLevelOfConcern = LOC_CRITICAL; - return cbRESULT_DATALOST; - } - - if (pktstogo) - *pktstogo = cb_rec_buffer_ptr[nIdx]->received - cb_recbuff_processed[nIdx]; - - // Level of concern is based on fourths - uint32_t nDiff = cb_rec_buffer_ptr[nIdx]->headindex - cb_recbuff_tailindex[nIdx]; - if (nDiff < 0) - nDiff += cbRECBUFFLEN; - - const uint32_t xxx = nDiff * LOC_COUNT; - const int xx = cbRECBUFFLEN; - nLevelOfConcern = static_cast(xxx / xx); - - // make sure to return a valid value - if (nLevelOfConcern < LOC_LOW) - nLevelOfConcern = LOC_LOW; - if (nLevelOfConcern > LOC_CRITICAL) - nLevelOfConcern = LOC_CRITICAL; - - return cbRESULT_OK; -} - -#if defined __APPLE__ -// Author & Date: Ehsan Azar 16 Feb 2013 -// Purpose: OSX compatibility wrapper -// Inputs: -// sem - buffer name -// ms - milliseconds to try semaphore -int sem_timedwait(sem_t * sem, int ms) -{ - int err = 1; - while (ms > 0) - { - if (sem_trywait(sem) == 0) - { - err = 0; - break; - } - usleep(1000); - ms--; - } - return err; -} -#endif - -// Purpose: Wait for master application (usually Central) to fill buffers -cbRESULT cbWaitforData(const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - -#ifdef WIN32 - if (WaitForSingleObject(cb_sig_event_hnd[nIdx], 250) == WAIT_OBJECT_0) - return cbRESULT_OK; -#elif defined __APPLE__ - if (sem_timedwait(static_cast(cb_sig_event_hnd[nIdx]), 250) == 0) - return cbRESULT_OK; -#else - timespec ts; - long ns = 250000000; - clock_gettime(CLOCK_REALTIME, &ts); -#define NANOSECONDS_PER_SEC 1000000000L - ts.tv_nsec = (ts.tv_nsec + ns) % NANOSECONDS_PER_SEC; - ts.tv_sec += (ts.tv_nsec + ns) / NANOSECONDS_PER_SEC; - if (sem_timedwait((sem_t *)cb_sig_event_hnd[nIdx], &ts) == 0) - return cbRESULT_OK; -#endif - else if (!(cb_cfg_buffer_ptr[nIdx]->version)) - { - TRACE("cbWaitforData says no Central App\n"); - return cbRESULT_NOCENTRALAPP; - } - else - return cbRESULT_NONEWDATA; -} - - -cbPKT_GENERIC * cbGetNextPacketPtr(const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // if there is new data return the next data packet and increment the pointer - if (cb_recbuff_processed[nIdx] < cb_rec_buffer_ptr[nIdx]->received) - { - // get the pointer to the current packet - auto *packetptr = reinterpret_cast(&(cb_rec_buffer_ptr[nIdx]->buffer[cb_recbuff_tailindex[nIdx]])); - - // increament the read index - cb_recbuff_tailindex[nIdx] += (cbPKT_HEADER_32SIZE + packetptr->cbpkt_header.dlen); - - // check for read buffer wraparound, if so increment relevant variables - if (cb_recbuff_tailindex[nIdx] > (cbRECBUFFLEN - (cbCER_UDP_SIZE_MAX / 4))) - { - cb_recbuff_tailindex[nIdx] = 0; - cb_recbuff_tailwrap[nIdx]++; - } - - // increment the processed count - cb_recbuff_processed[nIdx]++; - - // update the timestamp index - cb_recbuff_lasttime[nIdx] = packetptr->cbpkt_header.time; - - // return the packet - return packetptr; - } - else - return nullptr; -} - - -// Purpose: options sharing -// -cbRESULT cbGetColorTable(cbCOLORTABLE **colortable, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - *colortable = &(cb_cfg_buffer_ptr[nIdx]->colortable); - return cbRESULT_OK; -} - -// Purpose: options sharing spike cache -// -cbRESULT cbGetSpkCache(const uint32_t chid, cbSPKCACHE **cache, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - *cache = reinterpret_cast(reinterpret_cast(&(cb_spk_buffer_ptr[nIdx]->cache)) + ( - (chid - 1) * (cb_spk_buffer_ptr[nIdx]->linesize))); - return cbRESULT_OK; -} - -// Author & Date: Kirk Korver 29 May 2003 -// Purpose: Get the multiplier to use for autothresholdine when using RMS to guess noise -// This will adjust fAutoThresholdDistance above, but use the API instead -float cbGetRMSAutoThresholdDistance(const uint32_t nInstance) -{ - return GetOptionTable(nInstance).fRMSAutoThresholdDistance; -} - -// Author & Date: Kirk Korver 29 May 2003 -// Purpose: Set the multiplier to use for autothresholdine when using RMS to guess noise -// This will adjust fAutoThresholdDistance above, but use the API instead -void cbSetRMSAutoThresholdDistance(const float fRMSAutoThresholdDistance, const uint32_t nInstance) -{ - GetOptionTable(nInstance).fRMSAutoThresholdDistance = fRMSAutoThresholdDistance; -} - - -// Tell me about the current adaptive filter settings -cbRESULT cbGetAdaptFilter(const uint32_t proc, // which NSP processor? - uint32_t * pnMode, // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - float * pdLearningRate, // speed at which adaptation happens. Very small. e.g. 5e-12 - uint32_t * pnRefChan1, // The first reference channel (1 based). - uint32_t * pnRefChan2, // The second reference channel (1 based). - const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid and that requested procinfo structure is not empty - if ((proc - 1) >= cbMAXPROCS) return cbRESULT_INVALIDADDRESS; - - // Allow the parameters to be nullptr - if (pnMode) *pnMode = cb_cfg_buffer_ptr[nIdx]->adaptinfo[proc - 1].nMode; - if (pdLearningRate) *pdLearningRate = cb_cfg_buffer_ptr[nIdx]->adaptinfo[proc - 1].dLearningRate; - if (pnRefChan1) *pnRefChan1 = cb_cfg_buffer_ptr[nIdx]->adaptinfo[proc - 1].nRefChan1; - if (pnRefChan2) *pnRefChan2 = cb_cfg_buffer_ptr[nIdx]->adaptinfo[proc - 1].nRefChan2; - - return cbRESULT_OK; -} - - -// Update the adaptive filter settings -cbRESULT cbSetAdaptFilter(const uint32_t proc, // which NSP processor? - const uint32_t * pnMode, // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - const float * pdLearningRate, // speed at which adaptation happens. Very small. e.g. 5e-12 - const uint32_t * pnRefChan1, // The first reference channel (1 based). - const uint32_t * pnRefChan2, // The second reference channel (1 based). - const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid and that requested procinfo structure is not empty - if ((proc - 1) >= cbMAXPROCS) return cbRESULT_INVALIDADDRESS; - - - // Get the old values - uint32_t nMode; // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - float dLearningRate; // speed at which adaptation happens. Very small. e.g. 5e-12 - uint32_t nRefChan1; // The first reference channel (1 based). - uint32_t nRefChan2; // The second reference channel (1 based). - - const cbRESULT ret = cbGetAdaptFilter(proc, &nMode, &dLearningRate, &nRefChan1, &nRefChan2, nInstance); - ASSERT(ret == cbRESULT_OK); - if (ret != cbRESULT_OK) - return ret; - - // Handle the cases where there are "nullptr's" passed in - if (pnMode) nMode = *pnMode; - if (pdLearningRate) dLearningRate = *pdLearningRate; - if (pnRefChan1) nRefChan1 = *pnRefChan1; - if (pnRefChan2) nRefChan2 = *pnRefChan2; - - PktAdaptFiltInfo icPkt(nMode, dLearningRate, nRefChan1, nRefChan2); - return cbSendPacketToInstrument(&icPkt, nInstance, proc - 1); -} - -// Tell me about the current RefElecive filter settings -cbRESULT cbGetRefElecFilter(const uint32_t proc, // which NSP processor? - uint32_t * pnMode, // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - uint32_t * pnRefChan, // The reference channel (1 based). - const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid and that requested procinfo structure is not empty - if ((proc - 1) >= cbMAXPROCS) return cbRESULT_INVALIDADDRESS; - - // Allow the parameters to be nullptr - if (pnMode) *pnMode = cb_cfg_buffer_ptr[nIdx]->refelecinfo[proc - 1].nMode; - if (pnRefChan) *pnRefChan = cb_cfg_buffer_ptr[nIdx]->refelecinfo[proc - 1].nRefChan; - - return cbRESULT_OK; -} - - -// Update the reference electrode filter settings -cbRESULT cbSetRefElecFilter(const uint32_t proc, // which NSP processor? - const uint32_t * pnMode, // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - const uint32_t * pnRefChan, // The reference channel (1 based). - const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - // Test that the proc address is valid and that requested procinfo structure is not empty - if ((proc - 1) >= cbMAXPROCS) return cbRESULT_INVALIDADDRESS; - - - // Get the old values - uint32_t nMode; // 0=disabled, 1=filter continuous & spikes, 2=filter spikes - uint32_t nRefChan; // The reference channel (1 based). - - const cbRESULT ret = cbGetRefElecFilter(proc, &nMode, &nRefChan, nInstance); - ASSERT(ret == cbRESULT_OK); - if (ret != cbRESULT_OK) - return ret; - - // Handle the cases where there are "nullptr's" passed in - if (pnMode) nMode = *pnMode; - if (pnRefChan) nRefChan = *pnRefChan; - - cbPKT_REFELECFILTINFO icPkt = {}; - - icPkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - icPkt.cbpkt_header.type = cbPKTTYPE_REFELECFILTSET; - icPkt.cbpkt_header.dlen = cbPKTDLEN_REFELECFILTINFO; - - icPkt.nMode = nMode; - icPkt.nRefChan = nRefChan; - - return cbSendPacketToInstrument(&icPkt, nInstance, proc - 1); -} - -// Author & Date: Ehsan Azar 6 Nov 2012 -// Purpose: Get the channel selection status -// Inputs: -// szName - buffer name -// bReadOnly - if should open memory for read-only operation -cbRESULT cbGetChannelSelection(cbPKT_UNIT_SELECTION* pPktUnitSel, const uint32_t nProc, const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Test for prior library initialization - if (!cb_library_initialized[nIdx]) return cbRESULT_NOLIBRARY; - - if (cb_pc_status_buffer_ptr[nIdx]->isSelection[nProc].cbpkt_header.chid == 0) - return cbRESULT_HARDWAREOFFLINE; - - if (pPktUnitSel) *pPktUnitSel = cb_pc_status_buffer_ptr[nIdx]->isSelection[nProc]; - - return cbRESULT_OK; -} - -// Author & Date: Almut Branner 28 Mar 2006 -// Purpose: Create the shared memory objects -// Inputs: -// nInstance - nsp number to open library for -cbRESULT CreateSharedObjects(const uint32_t nInstance) -{ - const uint32_t nIdx = cb_library_index[nInstance]; - - // Create the shared neuromatic receive buffer, if unsuccessful, return the associated error code - if (nInstance == 0) - _snprintf(cb_rec_buffer_hnd[nIdx].name, sizeof(cb_rec_buffer_hnd[nIdx].name), "%s", REC_BUF_NAME); - else - _snprintf(cb_rec_buffer_hnd[nIdx].name, sizeof(cb_rec_buffer_hnd[nIdx].name), "%s%d", REC_BUF_NAME, nInstance); - cb_rec_buffer_hnd[nIdx].size = sizeof(cbRECBUFF); - CreateSharedBuffer(cb_rec_buffer_hnd[nIdx]); - cb_rec_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_rec_buffer_hnd[nIdx].hnd, false)); - if (cb_rec_buffer_ptr[nIdx] == nullptr) - return cbRESULT_BUFRECALLOCERR; - memset(cb_rec_buffer_ptr[nIdx], 0, cb_rec_buffer_hnd[nIdx].size); - - // Create the shared transmit buffer; if unsuccessful, release rec buffer and associated error code - { - // create the global transmit buffer space - if (nInstance == 0) - _snprintf(cb_xmt_global_buffer_hnd[nIdx].name, sizeof(cb_xmt_global_buffer_hnd[nIdx].name), "%s", GLOBAL_XMT_NAME); - else - _snprintf(cb_xmt_global_buffer_hnd[nIdx].name, sizeof(cb_xmt_global_buffer_hnd[nIdx].name), "%s%d", GLOBAL_XMT_NAME, nInstance); - cb_xmt_global_buffer_hnd[nIdx].size = sizeof(cbXMTBUFF) + (sizeof(uint32_t)*cbXMT_GLOBAL_BUFFLEN); - CreateSharedBuffer(cb_xmt_global_buffer_hnd[nIdx]); - // map the global memory into local ram space and get pointer - cb_xmt_global_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_xmt_global_buffer_hnd[nIdx].hnd, false)); - // clean up if error occurs - if (cb_xmt_global_buffer_ptr[nIdx] == nullptr) - return cbRESULT_BUFGXMTALLOCERR; - // initialize the buffers...they MUST all be initialized to 0 for later logic to work!! - memset(cb_xmt_global_buffer_ptr[nIdx], 0, cb_xmt_global_buffer_hnd[nIdx].size); - cb_xmt_global_buffer_ptr[nIdx]->bufferlen = cbXMT_GLOBAL_BUFFLEN; - cb_xmt_global_buffer_ptr[nIdx]->last_valid_index = - cbXMT_GLOBAL_BUFFLEN - (cbCER_UDP_SIZE_MAX / 4) - 1; // assuming largest packet array is 0 based - - // create the local transmit buffer space - if (nInstance == 0) - _snprintf(cb_xmt_local_buffer_hnd[nIdx].name, sizeof(cb_xmt_local_buffer_hnd[nIdx].name), "%s", LOCAL_XMT_NAME); - else - _snprintf(cb_xmt_local_buffer_hnd[nIdx].name, sizeof(cb_xmt_local_buffer_hnd[nIdx].name), "%s%d", LOCAL_XMT_NAME, nInstance); - cb_xmt_local_buffer_hnd[nIdx].size = sizeof(cbXMTBUFF) + (sizeof(uint32_t)*cbXMT_LOCAL_BUFFLEN); - CreateSharedBuffer(cb_xmt_local_buffer_hnd[nIdx]); - // map the global memory into local ram space and get pointer - cb_xmt_local_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_xmt_local_buffer_hnd[nIdx].hnd, false)); - // clean up if error occurs - if (cb_xmt_local_buffer_ptr[nIdx] == nullptr) - return cbRESULT_BUFLXMTALLOCERR; - // initialize the buffers...they MUST all be initialized to 0 for later logic to work!! - memset(cb_xmt_local_buffer_ptr[nIdx], 0, cb_xmt_local_buffer_hnd[nIdx].size); - cb_xmt_local_buffer_ptr[nIdx]->bufferlen = cbXMT_LOCAL_BUFFLEN; - cb_xmt_local_buffer_ptr[nIdx]->last_valid_index = - cbXMT_LOCAL_BUFFLEN - (cbCER_UDP_SIZE_MAX / 4) - 1; // assuming largest packet array is 0 based - } - - // Create the shared configuration buffer; if unsuccessful, release rec buffer and return FALSE - if (nInstance == 0) - _snprintf(cb_cfg_buffer_hnd[nIdx].name, sizeof(cb_cfg_buffer_hnd[nIdx].name), "%s", CFG_BUF_NAME); - else - _snprintf(cb_cfg_buffer_hnd[nIdx].name, sizeof(cb_cfg_buffer_hnd[nIdx].name), "%s%d", CFG_BUF_NAME, nInstance); - cb_cfg_buffer_hnd[nIdx].size = sizeof(cbCFGBUFF); - CreateSharedBuffer(cb_cfg_buffer_hnd[nIdx]); - cb_cfg_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_cfg_buffer_hnd[nIdx].hnd, false)); - if (cb_cfg_buffer_ptr[nIdx] == nullptr) - return cbRESULT_BUFCFGALLOCERR; - memset(cb_cfg_buffer_ptr[nIdx], 0, cb_cfg_buffer_hnd[nIdx].size); - - // Create the shared pc status buffer; if unsuccessful, release rec buffer and return FALSE - if (nInstance == 0) - _snprintf(cb_pc_status_buffer_hnd[nIdx].name, sizeof(cb_pc_status_buffer_hnd[nIdx].name), "%s", STATUS_BUF_NAME); - else - _snprintf(cb_pc_status_buffer_hnd[nIdx].name, sizeof(cb_pc_status_buffer_hnd[nIdx].name), "%s%d", STATUS_BUF_NAME, nInstance); - cb_pc_status_buffer_hnd[nIdx].size = sizeof(cbPcStatus); - CreateSharedBuffer(cb_pc_status_buffer_hnd[nIdx]); - cb_pc_status_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_pc_status_buffer_hnd[nIdx].hnd, false)); - if (cb_pc_status_buffer_ptr[nIdx] == nullptr) - return cbRESULT_BUFPCSTATALLOCERR; - memset(cb_pc_status_buffer_ptr[nIdx], 0, cb_pc_status_buffer_hnd[nIdx].size); - - // Create the shared spike cache buffer; if unsuccessful, release rec buffer and return FALSE - if (nInstance == 0) - _snprintf(cb_spk_buffer_hnd[nIdx].name, sizeof(cb_spk_buffer_hnd[nIdx].name), "%s", SPK_BUF_NAME); - else - _snprintf(cb_spk_buffer_hnd[nIdx].name, sizeof(cb_spk_buffer_hnd[nIdx].name), "%s%d", SPK_BUF_NAME, nInstance); - cb_spk_buffer_hnd[nIdx].size = sizeof(cbSPKBUFF); - CreateSharedBuffer(cb_spk_buffer_hnd[nIdx]); - cb_spk_buffer_ptr[nIdx] = static_cast(GetSharedBuffer(cb_spk_buffer_hnd[nIdx].hnd, false)); - if (cb_spk_buffer_ptr[nIdx] == nullptr) - return cbRESULT_BUFSPKALLOCERR; - - memset(cb_spk_buffer_ptr[nIdx], 0, sizeof(cbSPKBUFF)); - cb_spk_buffer_ptr[nIdx]->chidmax = cbPKT_SPKCACHELINECNT; - cb_spk_buffer_ptr[nIdx]->linesize = sizeof(cbSPKCACHE); - cb_spk_buffer_ptr[nIdx]->spkcount = cbPKT_SPKCACHEPKTCNT; - for (int l=0; lcache[l].chid = l+1; - cb_spk_buffer_ptr[nIdx]->cache[l].pktcnt = cbPKT_SPKCACHEPKTCNT; - cb_spk_buffer_ptr[nIdx]->cache[l].pktsize = sizeof(cbPKT_SPK); - } - - // initialize the configuration fields - cb_cfg_buffer_ptr[nIdx]->version = 96; - cb_cfg_buffer_ptr[nIdx]->colortable.dispback = RGB( 0, 0, 0); - cb_cfg_buffer_ptr[nIdx]->colortable.dispgridmaj = RGB( 80, 80, 80); - cb_cfg_buffer_ptr[nIdx]->colortable.dispgridmin = RGB( 48, 48, 48); - cb_cfg_buffer_ptr[nIdx]->colortable.disptext = RGB(192,192,192); - cb_cfg_buffer_ptr[nIdx]->colortable.dispwave = RGB(160,160,160); - cb_cfg_buffer_ptr[nIdx]->colortable.dispwavewarn = RGB(160,160, 0); - cb_cfg_buffer_ptr[nIdx]->colortable.dispwaveclip = RGB(192, 0, 0); - cb_cfg_buffer_ptr[nIdx]->colortable.dispthresh = RGB(192, 0, 0); - cb_cfg_buffer_ptr[nIdx]->colortable.dispmultunit = RGB(255,255,255); - cb_cfg_buffer_ptr[nIdx]->colortable.dispunit[0] = RGB(192,192,192); - cb_cfg_buffer_ptr[nIdx]->colortable.dispunit[1] = RGB(255, 51,153); - cb_cfg_buffer_ptr[nIdx]->colortable.dispunit[2] = RGB( 0,255,255); - cb_cfg_buffer_ptr[nIdx]->colortable.dispunit[3] = RGB(255,255, 0); - cb_cfg_buffer_ptr[nIdx]->colortable.dispunit[4] = RGB(153, 0,204); - cb_cfg_buffer_ptr[nIdx]->colortable.dispunit[5] = RGB( 0,255, 0); - cb_cfg_buffer_ptr[nIdx]->colortable.dispnoise = RGB( 78, 49, 31); - cb_cfg_buffer_ptr[nIdx]->colortable.dispchansel[0] = RGB(0,0,0); - cb_cfg_buffer_ptr[nIdx]->colortable.dispchansel[1] = RGB(192,192,192); - cb_cfg_buffer_ptr[nIdx]->colortable.dispchansel[2] = RGB(255,255,0); - cb_cfg_buffer_ptr[nIdx]->colortable.disptemp[0] = RGB(0,255,0); - - // create the shared event for data availability signalling - char buf[64] = {0}; - if (nInstance == 0) - _snprintf(buf, sizeof(buf), "%s", SIG_EVT_NAME); - else - _snprintf(buf, sizeof(buf), "%s%d", SIG_EVT_NAME, nInstance); -#ifdef WIN32 - cb_sig_event_hnd[nIdx] = CreateEventA(nullptr, TRUE, FALSE, buf); - if (cb_sig_event_hnd[nIdx] == nullptr) - return cbRESULT_EVSIGERR; -#else - sem_t * sem = sem_open(buf, O_CREAT | O_EXCL, 0666, 0); - if (sem == SEM_FAILED) - { - // Reattach: This might happen as a result of previous crash - sem_unlink(buf); - sem = sem_open(buf, O_CREAT | O_EXCL, 0666, 0); - if (sem == SEM_FAILED) - return cbRESULT_EVSIGERR; - } - cb_sig_event_hnd[nIdx] = sem; -#endif - - // No error happened - return cbRESULT_OK; -} - - diff --git a/src/cbhwlib/cki_common.h b/src/cbhwlib/cki_common.h deleted file mode 100755 index dea348f1..00000000 --- a/src/cbhwlib/cki_common.h +++ /dev/null @@ -1,118 +0,0 @@ -/* =STS=> cki_common.h[1693].aa07 open SMID:7 */ -/////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2002 - 2008 Cyberkinetics, Inc. -// (c) Copyright 2008 - 2019 Blackrock Microsystems, LLC -// -// $Workfile: cki_common.h $ -// $Archive: /Cerebus/Human/WindowsApps/cbhwlib/cki_common.h $ -// $Revision: 7 $ -// $Date: 6/02/05 3:52p $ -// $Author: Kkorver $ -// -// $NoKeywords: $ -// -/////////////////////////////////////////////////////////////////////////////// - -#ifndef CKI_COMMON_H_INCLUDED -#define CKI_COMMON_H_INCLUDED - -#ifdef _MSC_VER -#include -#include - -typedef std::vector INTVECTOR; -#endif - -enum CKI_ERROR -{ - CKI_OK = 0, - CKI_UNSPEC_ERROR, // Generic unclassified error - CKI_INVALID_CHANNEL, // Invalid channel used - CKI_INVALID_UNIT, // Invalid unit used - CKI_FILE_READ_ERR, // File read error - CKI_FILE_WRITE_ERR, // File write error -}; - -typedef CKI_ERROR CKIRETURN; // Common return type, non-zero values indicate err (type) - -enum STARTUP_OPTIONS -{ - OPT_NONE = 0x00000000, - OPT_ANY_IP = 0x00000001, // set if we want to bind to whatever ip address is available - OPT_LOOPBACK = 0x00000002, // set if we want to try the loopback ip address - OPT_LOCAL = 0x00000003, // set if we want to connect to lacalhost - OPT_REUSE = 0X00000004 // set if we want to allow other connections to the port -}; - -// The two cart types. Research gives advanced options to the researchers -#define NEUROPORT_CART "Neuroport" -#define RESEARCH_CART "Research" - -// CodeMeter firm code, product code, and feature codes -#define TEST_CM_FIRMCODE 10 // This is the test firm code -#define BMI_CM_FIRMCODE 101966 -#define BMI_CM_APP_NM 1 // NeuroMotive without any tracking capabilities -// Features are per-bits and thus power-of-two; we can have up to 32 features currently -#define BMI_CM_APP_NM_FEATURE_TRACK 1 // NeuroMotive with all tracking capabilities - -// Find out the size of an array -#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) - -#define TIMER_PERIOD_CENTRAL 10 // milliseconds period of the main timer of Central - -// I think that in this one case, the preprocessor is a better choice -// -//template -//inline size_t ARRAY_SIZE(const T array) -//{ return sizeof(array) / sizeof(*array); } -// -// - -// Use this for STL array functions..ONLY IF A STATIC ARRAY -// e.g. std::max_element(x, ARRAY_END(x)); -#define ARRAY_END(x) (x + ARRAY_SIZE(x)) - -#ifdef _MSC_VER -// Author & Date: Kirk Korver 23 May 2003 -// Purpose: Ensure that a number is between 2 other numbers (inclusive) -// Inputs: -// nVal - the value we need to clip -// nMaxValue - the maximum possible value (aka biggest positive number) -// nMinValue - the minimum possible value (aka smallest negative number) -// Outputs: -// the new value of nVal adjusted so it fits within the range -template -inline T clip(T nValToClip, T nMinValue, T nMaxValue) -{ - if (nValToClip > nMaxValue) - return nMaxValue; - - if (nValToClip < nMinValue) - return nMinValue; - - return nValToClip; -} - - -namespace std -{ - // Author & Date: Kirk Korver 03 Jun 2004 - // Purpose: wrap the std::for_each function to apply the functor to ALL entries - // Usage: - // vector v(10); - // int Double(int x) { return x+x; } - // for_all(v, Double); - // Inputs: - // c - the container we care about - // f - the function or functor we want to apply - template inline - Functor for_all(Container & c, Functor & f) - { - return std::for_each(c.begin(), c.end(), f); - } -}; -#endif - - -#endif // include guard diff --git a/src/cbhwlib/compat.h b/src/cbhwlib/compat.h deleted file mode 100644 index 49fb8a49..00000000 --- a/src/cbhwlib/compat.h +++ /dev/null @@ -1,36 +0,0 @@ -/* =STS=> compat.h[5017].aa00 submit SMID:1 */ -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2012 - 2013 Blackrock Microsystems -// -// $Workfile: compat.h $ -// $Archive: /Cerebus/WindowsApps/cbhwlib/compat.h $ -// $Revision: 1 $ -// $Date: 4/29/12 9:55a $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -/////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Purpose: This is a x-platform compatibility header file that needs to be the last to include -// It is used to override possible clashes -// -// StdAfx.h must be the first -// compat.h file must be the last -// -// This header file never should appear in another header file -// - -#ifndef COMPAT_H_INCLUDED -#define COMPAT_H_INCLUDED - -// Windows.h file may introduce min and max unless NOMINMAX is defined globally -#ifndef WIN32 - #undef max - #undef min - #define max(a,b) ((a)>(b)?(a):(b)) - #define min(a,b) ((a)<(b)?(a):(b)) -#endif - -#endif // include guard diff --git a/src/cbproto/CMakeLists.txt b/src/cbproto/CMakeLists.txt new file mode 100644 index 00000000..13f8db89 --- /dev/null +++ b/src/cbproto/CMakeLists.txt @@ -0,0 +1,35 @@ +# cbproto - Protocol Definitions Module +# Protocol packet structures, constants, connection enums, and packet translation + +project(cbproto + DESCRIPTION "CereLink Protocol Definitions" + LANGUAGES CXX +) + +# Library sources +set(CBPROTO_SOURCES + src/packet_translator.cpp +) + +# Build as STATIC library (needed for PacketTranslator implementation) +add_library(cbproto STATIC ${CBPROTO_SOURCES}) + +target_include_directories(cbproto + PUBLIC + $ + $ +) + +# C++17 required +target_compile_features(cbproto PUBLIC cxx_std_17) + +# Installation +install(TARGETS cbproto + EXPORT CBSDKTargets +) + +# TODO: Add headers when they exist +# install( +# DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +# ) diff --git a/src/cbproto/README.md b/src/cbproto/README.md new file mode 100644 index 00000000..594c6803 --- /dev/null +++ b/src/cbproto/README.md @@ -0,0 +1,46 @@ +# cbproto - Protocol Definitions + +**Status:** Phase 1 - ✅ **COMPLETE** (2025-11-11) + +## Purpose + +Pure protocol definitions module containing: +- Packet structures (cbPKT_*) +- Protocol constants (cbNSP1, cbMAXPROCS, etc.) +- Basic types (PROCTIME, etc.) +- Version information +- InstrumentId type for safe 0-based/1-based conversion + +## Key Design Principles + +1. **No Implementation Logic:** This module contains only data definitions +2. **C Compatibility:** All structures must be C-compatible for public API +3. **Zero Dependencies:** Does not depend on any other CereLink module +4. **Header-Only:** Implemented as header-only library for simplicity + +## Current Status + +- [x] Directory structure created +- [x] CMake integration added +- [x] InstrumentId type designed and implemented +- [x] Core protocol types extracted from upstream/cbproto/cbproto.h +- [x] Tests written (34 tests, all passing) +- [x] Build system working +- [x] Ground truth compatibility verified + +## Usage (Future) + +```cpp +#include +#include + +// Type-safe instrument ID conversions +cbproto::InstrumentId id = cbproto::InstrumentId::fromOneBased(1); +uint8_t index = id.toIndex(); // 0 +uint8_t oneBased = id.toOneBased(); // 1 +``` + +## References + +- Design document: `docs/refactor_plan.md` (Phase 1) +- Current protocol: `include/cerelink/cbproto.h` diff --git a/src/cbproto/StdAfx.h b/src/cbproto/StdAfx.h deleted file mode 100755 index 34646f94..00000000 --- a/src/cbproto/StdAfx.h +++ /dev/null @@ -1,96 +0,0 @@ -/* =STS=> StdAfx.h[1724].aa02 open SMID:2 */ -///////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003 - 2006 Cyberkinetics, Inc. -// (c) Copyright 2007 - 2012 Blackrock Microsystems -// -// $Workfile: $ -// $Archive: $ -// $Revision: $ -// $Date: $ -// $Author: $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////// -// StdAfx.h : -// Include file for standard system include files, -// or project specific include files that are used frequently, but -// are changed infrequently -// -// It also serves the purpose of x-platform compatibility -// - -#if !defined(AFX_STDAFX_H__30C4744E_BE06_4F52_ABD2_3FF2F67A1D18__INCLUDED_) -#define AFX_STDAFX_H__30C4744E_BE06_4F52_ABD2_3FF2F67A1D18__INCLUDED_ - -#ifdef __APPLE__ - -#define ERR_UDP_MESSAGE \ - "Unable to assign UDP interface memory\n" \ - " Consider nvram boot-args=\"ncl=65536\"\n" \ - " sysctl -w kern.ipc.maxsockbuf=8388608 and\n" \ - " Requirement of the first command depends on system memory\n" \ - " That may need to change boot parameters on OSX (and needs to reboot before the sysctl command) \n" \ - " It is possible to use 'receive-buffer-size' parameter when opening the library to override this\n" \ - " Any value below 4194304 may degrade the performance and must be avoided\n" \ - " Use 8388608 or more for maximum efficiency" - -#else - -#define ERR_UDP_MESSAGE \ - "Unable to assign UDP interface memory\n" \ - " Consider sysctl -w net.core.rmem_max=8388608\n" \ - " It is possible to use 'receive-buffer-size' parameter when opening the library to override this\n" \ - " Any value below 4194304 may degrade the performance and must be avoided\n" \ - " Use 8388608 or more for maximum efficiency" - -#endif - -#if _MSC_VER > 1000 -#pragma once -#endif // _MSC_VER > 1000 - -#ifdef WIN32 -#pragma warning (push) -#pragma warning (disable : 4005) -#define _CRT_SECURE_NO_DEPRECATE -#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1 -#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT 1 -#pragma warning (pop) -#else -#ifdef CBPYSDK -// Python is picky in its requirements -#include "Python.h" -#endif -#include -#include -#include -#define _strcmpi strcasecmp -#define _strnicmp strncasecmp -#define _snprintf snprintf -#endif - -#ifdef NO_AFX -#ifdef WIN32 -#include -#include -#include -#include -#endif -#else -#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers - -#ifdef WIN32 -#include // MFC core and standard components -#include // MFC extensions -#include // MFC support for Internet Explorer 4 Common Controls -#ifndef _AFX_NO_AFXCMN_SUPPORT -#include // MFC support for Windows Common Controls -#endif // _AFX_NO_AFXCMN_SUPPORT -#endif -#endif -//{{AFX_INSERT_LOCATION}} -// Microsoft Visual C++ will insert additional declarations immediately before the previous line. - -#endif // !defined(AFX_STDAFX_H__30C4744E_BE06_4F52_ABD2_3FF2F67A1D18__INCLUDED_) diff --git a/src/cbproto/debugmacs.h b/src/cbproto/debugmacs.h deleted file mode 100755 index 7d3fe755..00000000 --- a/src/cbproto/debugmacs.h +++ /dev/null @@ -1,111 +0,0 @@ -/* =STS=> debugmacs.h[1694].aa08 open SMID:9 */ -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003-2008 Cyberkinetics, Inc. -// (c) Copyright 2008-2011 Blackrock Microsystems -// -// $Workfile: debugmacs.h $ -// $Archive: /Cerebus/WindowsApps/ConfigDlgs/debugmacs.h $ -// $Revision: 1 $ -// $Date: 9/30/03 3:19p $ -// $Author: Awang $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// - -#ifndef DEBUGMACS_H_INCLUDED // include guards -#define DEBUGMACS_H_INCLUDED - - -// Macro Definitions ------------------------------------------- -// - -#ifdef DEBUG - #ifndef _DEBUG - #define _DEBUG - #endif -#endif - -// If you have VERBOSE turned on, then turn on all of the Debugs as well -#ifdef VERBOSE_DEBUG - #ifndef _DEBUG - #define _DEBUG - #endif -#endif - -// A simple example of how to use these -// DEBUG_PRINTF("This is an int %i", myInt); -// -// The above code will then only be written if the DEBUG or VERBOSE_DEBUG macros -// are turned on - -#ifndef NDEBUG - #ifdef WIN32 - #ifndef _CRT_SECURE_NO_DEPRECATE - #define _CRT_SECURE_NO_DEPRECATE - #endif - #ifndef _CRT_SECURE_NO_WARNINGS - #define _CRT_SECURE_NO_WARNINGS - #endif - #ifdef NO_AFX - #include - #include - #endif - #include - #ifdef TRACE - #undef TRACE - #endif - #ifdef _CONSOLE - #define TRACE printf - #else - #define TRACE _cprintf - #endif - #else - #define TRACE printf - #endif - #ifndef ASSERT - #include - #define ASSERT assert - #endif - // Ok.. We want debugging output - #define DEBUG_CODE(X) X - - #ifdef __KERNEL__ - #define DEBUG_PRINTF(FMT, ARGS...) printk(FMT, ## ARGS) - - - #ifdef VERBOSE_DEBUG - #define VERBOSE_DEBUG_PRINTF(FMT, ARGS...) printk(FMT, ## ARGS) - #else - #define VERBOSE_DEBUG_PRINTF(FMT, ARGS...) - #endif - - - #endif -#else - #define DEBUG_CODE(X) - #ifndef TRACE - #ifndef _MSC_VER - #define TRACE(FMT, ARGS...) - #else - #define TRACE(FMT, ...) - #endif - #endif - #ifndef ASSERT - #define ASSERT(X) - #endif - #ifndef _ASSERT - #define _ASSERT(X) - #endif -#endif - -#ifndef WIN32 -#define _cprintf printf -#endif - -#ifndef DEBUG_PRINTF -#define DEBUG_PRINTF TRACE -#endif - -#endif // include guard diff --git a/src/central/BmiVersion.h b/src/cbproto/include/cbproto/BmiVersion.h similarity index 100% rename from src/central/BmiVersion.h rename to src/cbproto/include/cbproto/BmiVersion.h diff --git a/src/cbproto/include/cbproto/cbproto.h b/src/cbproto/include/cbproto/cbproto.h new file mode 100644 index 00000000..24dc6bff --- /dev/null +++ b/src/cbproto/include/cbproto/cbproto.h @@ -0,0 +1,65 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file cbproto.h +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Main header for cbproto module +/// +/// This is the primary include file for the protocol definitions module. +/// Include this file to access all protocol types, constants, and utilities. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBPROTO_CBPROTO_H +#define CBPROTO_CBPROTO_H + +// Core protocol types and constants (C-compatible) +#include "types.h" + +// C++-only utilities +#ifdef __cplusplus +#include "instrument_id.h" +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @mainpage CereLink Protocol Definitions +/// +/// @section intro Introduction +/// +/// This module provides protocol definitions for the Cerebus protocol specification. +/// It serves as the foundation for the CereLink architecture. +/// +/// @section key_types Key Types +/// +/// - cbPKT_HEADER: Every packet contains this header +/// - cbPKT_GENERIC: Generic packet structure +/// - cbproto::InstrumentId: Type-safe instrument ID (C++ only) +/// +/// @section ground_truth Ground Truth +/// +/// All structures and constants in this module match Blackrock's cbproto.h exactly +/// to ensure compatibility with Central. DO NOT modify without updating from upstream. +/// +/// @section usage Usage +/// +/// C code: +/// @code +/// #include +/// +/// cbPKT_HEADER pkt; +/// pkt.instrument = 0; // 0-based in packet! +/// @endcode +/// +/// C++ code: +/// @code +/// #include +/// +/// // Type-safe instrument ID conversions +/// cbproto::InstrumentId id = cbproto::InstrumentId::fromPacketField(pkt.cbpkt_header.instrument); +/// uint8_t idx = id.toIndex(); // For array access +/// uint8_t oneBased = id.toOneBased(); // For API calls (cbNSP1, etc.) +/// @endcode +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // CBPROTO_CBPROTO_H diff --git a/src/cbproto/include/cbproto/config.h b/src/cbproto/include/cbproto/config.h new file mode 100644 index 00000000..284a6866 --- /dev/null +++ b/src/cbproto/include/cbproto/config.h @@ -0,0 +1,87 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file config.h +/// @author CereLink Development Team +/// @date 2025-01-21 +/// +/// @brief Device configuration structure +/// +/// Streamlined device configuration buffer that holds the packets returned by REQCONFIGALL. +/// This is similar in concept to the upstream cbCFGBUFF but simplified: +/// - Single processor (no cbMAXPROCS arrays) +/// - Uses cbMAXCHANS = 256 (not 768) +/// - Only contains configuration packets (no runtime state) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBPROTO_CONFIG_H +#define CBPROTO_CONFIG_H + +#include + +// Constants not yet in types.h but needed for config structure +// TODO: Move these to types.h when updating from upstream +#ifndef cbMAXBANKS +#define cbMAXBANKS 15 ///< cbNUM_FE_BANKS(8) + ANAIN(1) + ANAOUT(1) + AUDOUT(1) + DIGIN(1) + SERIAL(1) + DIGOUT(1) +#endif + +namespace cbproto { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Spike sorting configuration +/// +/// Groups all spike-sorting related configuration packets together. +/// This matches the upstream cbSPIKE_SORTING structure. +/// +struct SpikeSorting { + // Spike sorting models and basis functions + // NOTE: These must be first in the structure (see upstream WriteCCFNoPrompt) + cbPKT_FS_BASIS basis[cbMAXCHANS]; ///< PCA basis values per channel + cbPKT_SS_MODELSET models[cbMAXCHANS][cbMAXUNITS + 2]; ///< Sorting models/rules per channel + + // Spike sorting parameters + cbPKT_SS_DETECT detect; ///< Detection parameters + cbPKT_SS_ARTIF_REJECT artifact_reject; ///< Artifact rejection parameters + cbPKT_SS_NOISE_BOUNDARY noise_boundary[cbMAXCHANS]; ///< Noise boundaries per channel + cbPKT_SS_STATISTICS statistics; ///< Spike statistics + cbPKT_SS_STATUS status; ///< Spike sorting status +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Device configuration buffer +/// +/// Contains all configuration packets returned by REQCONFIGALL. +/// This is a streamlined version of the upstream cbCFGBUFF: +/// - Single processor (arrays sized for 1, not cbMAXPROCS) +/// - Uses cbMAXCHANS = 256 (the actual hardware channel count) +/// - Focuses only on configuration packets (no UI state, no Central window handles) +/// +struct DeviceConfig { + // System configuration + cbPKT_SYSINFO sysinfo; ///< System information and capabilities + cbPKT_PROCINFO procinfo; ///< Processor information (single proc) + + // Channel configuration + cbPKT_CHANINFO chaninfo[cbMAXCHANS]; ///< Channel configuration (256 channels) + cbPKT_NTRODEINFO ntrodeinfo[cbMAXNTRODES]; ///< N-trode configuration + + // Signal processing configuration + cbPKT_GROUPINFO groupinfo[cbMAXGROUPS]; ///< Sample group configuration + cbPKT_FILTINFO filtinfo[cbMAXFILTS]; ///< Digital filter configuration + cbPKT_BANKINFO bankinfo[cbMAXBANKS]; ///< Filter bank configuration + cbPKT_ADAPTFILTINFO adaptinfo; ///< Adaptive filter settings + cbPKT_REFELECFILTINFO refelecinfo; ///< Reference electrode filtering + + // Spike sorting configuration + SpikeSorting spike_sorting; ///< All spike sorting parameters + + // Analog output waveform configuration + cbPKT_AOUT_WAVEFORM waveform[AOUT_NUM_GAIN_CHANS][cbMAX_AOUT_TRIGGER]; ///< Waveform triggers per analog output + + // Recording configuration + cbPKT_LNC lnc; ///< LNC parameters + cbPKT_FILECFG fileinfo; ///< File recording configuration +}; + +} // namespace cbproto + +#endif // CBPROTO_CONFIG_H diff --git a/src/cbproto/include/cbproto/connection.h b/src/cbproto/include/cbproto/connection.h new file mode 100644 index 00000000..1f6e43df --- /dev/null +++ b/src/cbproto/include/cbproto/connection.h @@ -0,0 +1,115 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file connection.h +/// @author CereLink Development Team +/// @date 2025-01-23 +/// +/// @brief C-compatible connection enumerations for Cerebus devices +/// +/// This header provides C-compatible enumerations for device types, protocol versions, and +/// channel types. These can be used from both C and C++ code and serve as the canonical +/// definitions used throughout the codebase. +/// +/// Usage from C: +/// #include +/// cbproto_device_type_t device = CBPROTO_DEVICE_TYPE_NSP; +/// +/// Usage from C++: +/// #include +/// cbproto_device_type_t device = CBPROTO_DEVICE_TYPE_NSP; +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBPROTO_CONNECTION_H +#define CBPROTO_CONNECTION_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Device Type Enumeration +/// @brief Enumeration of supported Cerebus device types +/// +/// Each device type maps to specific network addresses and ports. These values are ABI-stable +/// and must not be changed to maintain binary compatibility. +/// @{ +/////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef enum cbproto_device_type { + CBPROTO_DEVICE_TYPE_LEGACY_NSP = 0, ///< Neural Signal Processor (legacy, 192.168.137.128) + CBPROTO_DEVICE_TYPE_NSP = 1, ///< Gemini NSP (192.168.137.128, port 51001) + CBPROTO_DEVICE_TYPE_HUB1 = 2, ///< Gemini Hub 1 (192.168.137.200, port 51002) + CBPROTO_DEVICE_TYPE_HUB2 = 3, ///< Gemini Hub 2 (192.168.137.201, port 51003) + CBPROTO_DEVICE_TYPE_HUB3 = 4, ///< Gemini Hub 3 (192.168.137.202, port 51004) + CBPROTO_DEVICE_TYPE_NPLAY = 5, ///< nPlayServer (127.0.0.1, ports 51001/51002) + CBPROTO_DEVICE_TYPE_CUSTOM = 6 ///< Custom IP/port configuration +} cbproto_device_type_t; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Protocol Version Enumeration +/// @brief Enumeration of Cerebus protocol versions +/// +/// Different device firmware versions use different protocol formats. These values are +/// ABI-stable and must not be changed to maintain binary compatibility. +/// @{ +/////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef enum cbproto_protocol_version { + CBPROTO_PROTOCOL_UNKNOWN = 0, ///< Unknown or undetected protocol version + CBPROTO_PROTOCOL_311 = 1, ///< Legacy protocol 3.11 (32-bit timestamps, 8-bit packet types) + CBPROTO_PROTOCOL_400 = 2, ///< Legacy protocol 4.0 (64-bit timestamps, 8-bit packet types) + CBPROTO_PROTOCOL_410 = 3, ///< Protocol 4.1 (64-bit timestamps, 16-bit packet types) + CBPROTO_PROTOCOL_CURRENT = 4 ///< Current protocol 4.2+ (64-bit timestamps, 16-bit packet types) +} cbproto_protocol_version_t; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Channel Type Enumeration +/// @brief Enumeration of Cerebus channel types based on capabilities +/// +/// Channels are categorized by their capabilities (analog input, digital I/O, etc.). +/// These values are ABI-stable and must not be changed to maintain binary compatibility. +/// @{ +/////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef enum cbproto_channel_type { + CBPROTO_CHANNEL_TYPE_FRONTEND = 0, ///< Front-end analog input (isolated, cbCHAN_AINP | cbCHAN_ISOLATED) + CBPROTO_CHANNEL_TYPE_ANALOG_IN = 1, ///< Analog input (non-isolated, cbCHAN_AINP only) + CBPROTO_CHANNEL_TYPE_ANALOG_OUT = 2, ///< Analog output (non-audio, cbCHAN_AOUT, not cbAOUT_AUDIO) + CBPROTO_CHANNEL_TYPE_AUDIO = 3, ///< Audio output (cbCHAN_AOUT with cbAOUT_AUDIO) + CBPROTO_CHANNEL_TYPE_DIGITAL_IN = 4, ///< Digital input (cbCHAN_DINP, not serial) + CBPROTO_CHANNEL_TYPE_SERIAL = 5, ///< Serial input (cbCHAN_DINP with cbDINP_SERIALMASK) + CBPROTO_CHANNEL_TYPE_DIGITAL_OUT = 6 ///< Digital output (cbCHAN_DOUT) +} cbproto_channel_type_t; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Device Rate Type Enumeration +/// @brief Enumeration of Cerebus sampling group rates +/// +/// @{ +/////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef enum cbproto_group_rate { + CBPROTO_GROUP_RATE_NONE = 0, + CBPROTO_GROUP_RATE_500Hz = 1, + CBPROTO_GROUP_RATE_1000Hz = 2, + CBPROTO_GROUP_RATE_2000Hz = 3, + CBPROTO_GROUP_RATE_10000Hz = 4, + CBPROTO_GROUP_RATE_30000Hz = 5, + CBPROTO_GROUP_RATE_RAW = 6 +} cbproto_group_rate_t; + +/// @} + +#ifdef __cplusplus +} +#endif + +#endif // CBPROTO_CONNECTION_H diff --git a/src/cbproto/include/cbproto/instrument_id.h b/src/cbproto/include/cbproto/instrument_id.h new file mode 100644 index 00000000..22c265db --- /dev/null +++ b/src/cbproto/include/cbproto/instrument_id.h @@ -0,0 +1,141 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file instrument_id.h +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Type-safe instrument ID with explicit 0-based/1-based conversions +/// +/// This type prevents the common indexing bug where instrument IDs are confused between: +/// - 1-based values (cbNSP1 = 1, used in API calls like cbGetProcInfo(cbNSP1, ...)) +/// - 0-based indices (packet header instrument field, array indices) +/// +/// Ground truth from upstream/cbproto/cbproto.h: +/// - cbNSP1 = 1 (first instrument in 1-based numbering) +/// - cbMAXOPEN = 4 (max number of instruments) +/// - cbMAXPROCS = 1 (processors per instrument) +/// - Packet header instrument field is 0-based (0, 1, 2, 3 for 4 instruments) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBPROTO_INSTRUMENT_ID_H +#define CBPROTO_INSTRUMENT_ID_H + +#include +#include + +#ifdef __cplusplus + +namespace cbproto { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Type-safe instrument identifier +/// +/// Provides explicit conversion methods to prevent 0-based/1-based indexing bugs. +/// +/// Usage: +/// @code +/// // From API constant (1-based) +/// InstrumentId id = InstrumentId::fromOneBased(cbNSP1); // id = 1 +/// uint8_t idx = id.toIndex(); // idx = 0 (for array access) +/// +/// // From packet header (0-based) +/// InstrumentId id = InstrumentId::fromPacketField(pkt->cbpkt_header.instrument); +/// uint8_t oneBased = id.toOneBased(); // For API calls +/// +/// // From array index (0-based) +/// InstrumentId id = InstrumentId::fromIndex(0); +/// uint8_t oneBased = id.toOneBased(); // 1 +/// @endcode +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// +class InstrumentId { +public: + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Construction from different representations + /// @{ + + /// @brief Create from 1-based ID (e.g., cbNSP1 = 1, cbNSP2 = 2) + /// @param id 1-based instrument ID (1-4 for cbMAXOPEN=4) + /// @return InstrumentId instance + static InstrumentId fromOneBased(uint8_t id) { + return InstrumentId(id); + } + + /// @brief Create from 0-based array index (0-3 for cbMAXOPEN=4) + /// @param idx 0-based array index + /// @return InstrumentId instance + static InstrumentId fromIndex(uint8_t idx) { + return InstrumentId(idx + 1); + } + + /// @brief Create from packet header instrument field (0-based) + /// @param pkt_inst Value from cbPKT_HEADER.instrument field + /// @return InstrumentId instance + static InstrumentId fromPacketField(uint8_t pkt_inst) { + return InstrumentId(pkt_inst + 1); + } + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Conversion to different representations + /// @{ + + /// @brief Convert to 1-based ID for API calls + /// @return 1-based instrument ID (1-4) + uint8_t toOneBased() const { + return m_id; + } + + /// @brief Convert to 0-based array index + /// @return 0-based index (0-3) + uint8_t toIndex() const { + return m_id - 1; + } + + /// @brief Convert to packet header instrument field value + /// @return 0-based value for packet header (0-3) + uint8_t toPacketField() const { + return m_id - 1; + } + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Validation + /// @{ + + /// @brief Check if ID is valid (1 <= id <= cbMAXOPEN) + /// @return true if valid, false otherwise + bool isValid() const { + return m_id >= 1 && m_id <= 4; // cbMAXOPEN = 4 + } + + /// @} + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Comparison operators + /// @{ + + bool operator==(const InstrumentId& other) const { return m_id == other.m_id; } + bool operator!=(const InstrumentId& other) const { return m_id != other.m_id; } + bool operator<(const InstrumentId& other) const { return m_id < other.m_id; } + bool operator<=(const InstrumentId& other) const { return m_id <= other.m_id; } + bool operator>(const InstrumentId& other) const { return m_id > other.m_id; } + bool operator>=(const InstrumentId& other) const { return m_id >= other.m_id; } + + /// @} + +private: + /// @brief Private constructor - use static factory methods + /// @param id 1-based instrument ID + explicit InstrumentId(uint8_t id) : m_id(id) {} + + uint8_t m_id; ///< Stored as 1-based ID internally +}; + +} // namespace cbproto + +#endif // __cplusplus + +#endif // CBPROTO_INSTRUMENT_ID_H diff --git a/src/cbproto/include/cbproto/packet_translator.h b/src/cbproto/include/cbproto/packet_translator.h new file mode 100644 index 00000000..f387d5af --- /dev/null +++ b/src/cbproto/include/cbproto/packet_translator.h @@ -0,0 +1,247 @@ +// +// Created by Chadwick Boulay on 2025-11-17. +// + +#ifndef CBPROTO_PACKET_TRANSLATOR_H +#define CBPROTO_PACKET_TRANSLATOR_H + +#include +#include // for size_t +#include // for uint8_t +#include // for std::memcpy + +// TODO: Finish the implementation of all the type-specific translation functions. + +namespace cbproto { + +typedef struct { + uint32_t time; ///< Ticks at 30 kHz + uint16_t chid; ///< Channel identifier + uint8_t type; ///< Packet type + uint8_t dlen; ///< Length of data field in 32-bit chunks +} cbPKT_HEADER_311; +constexpr size_t HEADER_SIZE_311 = sizeof(cbPKT_HEADER_311); + +typedef struct { + PROCTIME time; ///< Ticks at 30 kHz on legacy, or nanoseconds on Gemini + uint16_t chid; ///< Channel identifier + uint8_t type; ///< Packet type + uint16_t dlen; ///< Length of data field in 32-bit chunks + uint8_t instrument; ///< Instrument identifier + uint16_t reserved; ///< Reserved byte +} cbPKT_HEADER_400; +constexpr size_t HEADER_SIZE_400 = sizeof(cbPKT_HEADER_400); + +constexpr size_t HEADER_SIZE_410 = cbPKT_HEADER_SIZE; // Header unchanged since 4.1 + + +class PacketTranslator { +public: + // Public methods' args are pointers to the start of the entire packet (header + payload). + + static size_t translatePayload_311_to_current(const uint8_t* src, uint8_t* dest) { + // Header has already been translated, and we are guaranteed dest has enough space. + // Copy the payload bytes into the destination packet. + + const auto* src_payload = &src[HEADER_SIZE_311]; + auto& dest_header = *reinterpret_cast(dest); + + switch (dest_header.type) { + case cbPKTTYPE_NPLAYREP: + return translate_NPLAY_pre400_to_current(src_payload, reinterpret_cast(dest)); + case cbPKTTYPE_COMMENTREP: + return translate_COMMENT_pre400_to_current( + src_payload, reinterpret_cast(dest), *reinterpret_cast(src)); + case cbPKTTYPE_SYSPROTOCOLMONITOR: + // cbPKTTYPE_SYSPROTOCOLMONITOR == 0x01 == cbPKTTYPE_PREVREPLNC (pre-4.2) + // SYSPROTOCOLMONITOR translation takes priority; PREVREPLNC remap is N/A for 3.11 + return translate_SYSPROTOCOLMONITOR_pre410_to_current(src_payload, reinterpret_cast(dest)); + case cbPKTTYPE_CHANRESETREP: + return translate_CHANRESET_pre420_to_current(src_payload, reinterpret_cast(dest)); + default: + // CHANREP family (0x40-0x4F) — bitmask check, can't be a case label + if ((dest_header.type & 0xF0) == cbPKTTYPE_CHANREP) { + return translate_CHANINFO_pre410_to_current(src_payload, reinterpret_cast(dest)); + } + // TODO: cbPKT_DINP — needs chaninfo, unavailable here + break; + } + + // No explicit change. Do a memcpy and report the original dlen (already in dest header). + if (dest_header.dlen > 0) { + std::memcpy(&dest[cbPKT_HEADER_SIZE], + src_payload, + dest_header.dlen * 4); + } + return dest_header.dlen; + } + + static size_t translatePayload_400_to_current(const uint8_t* src, uint8_t* dest) { + // Header has already been translated, and we are guaranteed dest has enough space. + // Copy the payload bytes into the destination packet. + + auto& dest_header = *reinterpret_cast(dest); + const auto* src_payload = &src[HEADER_SIZE_400]; + + switch (dest_header.type) { + case cbPKTTYPE_SYSPROTOCOLMONITOR: + // cbPKTTYPE_SYSPROTOCOLMONITOR == 0x01; PREVREPLNC remap is N/A for 4.0 + return translate_SYSPROTOCOLMONITOR_pre410_to_current(src_payload, reinterpret_cast(dest)); + case cbPKTTYPE_CHANRESETREP: + return translate_CHANRESET_pre420_to_current(src_payload, reinterpret_cast(dest)); + default: + if ((dest_header.type & 0xF0) == cbPKTTYPE_CHANREP) { + return translate_CHANINFO_pre410_to_current(src_payload, reinterpret_cast(dest)); + } + break; + } + + // No explicit change. Do a memcpy and report the original dlen (already in dest header). + if (dest_header.dlen > 0) { + std::memcpy(&dest[cbPKT_HEADER_SIZE], + src_payload, + dest_header.dlen * 4); + } + return dest_header.dlen; + } + + static size_t translatePayload_410_to_current(const uint8_t* src, uint8_t* dest) { + // For 410 to current, we do not use an intermediate buffer; src and dest are the same! + const auto* src_payload = &src[HEADER_SIZE_410]; + auto& dest_header = *reinterpret_cast(dest); + switch (dest_header.type) { + case cbPKTTYPE_CHANRESETREP: + return translate_CHANRESET_pre420_to_current(src_payload, reinterpret_cast(dest)); + case 0x01: + dest_header.type = 0x04; + return dest_header.dlen; + default: + return dest_header.dlen; + } + } + + static size_t translatePayload_current_to_311(const cbPKT_GENERIC& src, uint8_t* dest) { + // Prepare pointers to specific sections that will be modified + auto& dest_header = *reinterpret_cast(dest); + auto* dest_payload = &dest[HEADER_SIZE_311]; + + switch (src.cbpkt_header.type) { + case cbPKTTYPE_NPLAYSET: + return translate_NPLAY_current_to_pre400( + *reinterpret_cast(&src), dest_payload); + case cbPKTTYPE_COMMENTSET: + return translate_COMMENT_current_to_pre400( + *reinterpret_cast(&src), dest_payload); + case cbPKTTYPE_SYSPROTOCOLMONITOR: + return translate_SYSPROTOCOLMONITOR_current_to_pre410( + *reinterpret_cast(&src), dest_payload); + case cbPKTTYPE_CHANRESET: + return translate_CHANRESET_current_to_pre420( + *reinterpret_cast(&src), dest_payload); + case cbPKTTYPE_PREVSETLNC: + dest_header.type = 0x81; + return dest_header.dlen; + default: + if ((src.cbpkt_header.type & 0xF0) == cbPKTTYPE_CHANSET) { + return translate_CHANINFO_current_to_pre410( + *reinterpret_cast(&src), dest_payload); + } + // TODO: cbPKT_DINP — needs chaninfo, unavailable here + break; + } + + // memcpy the payload bytes + const auto* src_bytes = reinterpret_cast(&src); + if (src.cbpkt_header.dlen > 0) { + std::memcpy(dest_payload, + &src_bytes[cbPKT_HEADER_SIZE], + src.cbpkt_header.dlen * 4); + } + return src.cbpkt_header.dlen; + } + + static size_t translatePayload_current_to_400(const cbPKT_GENERIC& src, uint8_t* dest) { + auto& dest_header = *reinterpret_cast(dest); + auto* dest_payload = &dest[HEADER_SIZE_400]; + + switch (src.cbpkt_header.type) { + case cbPKTTYPE_SYSPROTOCOLMONITOR: + return translate_SYSPROTOCOLMONITOR_current_to_pre410( + *reinterpret_cast(&src), dest_payload); + case cbPKTTYPE_CHANRESET: + return translate_CHANRESET_current_to_pre420( + *reinterpret_cast(&src), dest_payload); + case cbPKTTYPE_PREVSETLNC: + dest_header.type = 0x81; + return dest_header.dlen; + default: + if ((src.cbpkt_header.type & 0xF0) == cbPKTTYPE_CHANSET) { + return translate_CHANINFO_current_to_pre410( + *reinterpret_cast(&src), dest_payload); + } + break; + } + + // memcpy the payload bytes and report the original dlen. + const auto* src_bytes = reinterpret_cast(&src); + if (src.cbpkt_header.dlen > 0) { + std::memcpy(dest_payload, + &src_bytes[cbPKT_HEADER_SIZE], + src.cbpkt_header.dlen * 4); + } + return src.cbpkt_header.dlen; + } + + static size_t translatePayload_current_to_410(const cbPKT_GENERIC& src, uint8_t* dest) { + // We already copied the entire packet upstream. Here we need to adjust payload only. + auto& dest_header = *reinterpret_cast(dest); + auto* dest_payload = &dest[HEADER_SIZE_410]; + switch (src.cbpkt_header.type) { + case cbPKTTYPE_CHANRESET: + return translate_CHANRESET_current_to_pre420( + *reinterpret_cast(&src), dest_payload); + case cbPKTTYPE_PREVSETLNC: + dest_header.type = 0x81; + return dest_header.dlen; + default: + return src.cbpkt_header.dlen; + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /// @name Packet-Specific Translation Functions (Public for Unit Testing) + /// @{ + /// + /// These methods expect a pointer to the start of the non-current (src/dest) payload + /// and to the specific current (dest/src) packet. This is because these methods are reused + /// across non-current protocol versions so we cannot know the header size, whereas the public + /// translatePayload_* methods are only used for a specific protocol version and know the header size. + /// + /// Made public to enable direct unit testing of translation logic. + /// @} + + static size_t translate_DINP_pre400_to_current(const uint8_t* src_payload, cbPKT_DINP* dest); + static size_t translate_DINP_current_to_pre400(const cbPKT_DINP &src, uint8_t* dest_payload); + + static size_t translate_NPLAY_pre400_to_current(const uint8_t* src_payload, cbPKT_NPLAY* dest); + static size_t translate_NPLAY_current_to_pre400(const cbPKT_NPLAY &src, uint8_t* dest_payload); + + static size_t translate_COMMENT_pre400_to_current(const uint8_t* src_payload, cbPKT_COMMENT* dest, uint32_t hdr_timestamp); + static size_t translate_COMMENT_current_to_pre400(const cbPKT_COMMENT &src, uint8_t* dest_payload); + + static size_t translate_SYSPROTOCOLMONITOR_pre410_to_current(const uint8_t* src_payload, cbPKT_SYSPROTOCOLMONITOR* dest); + static size_t translate_SYSPROTOCOLMONITOR_current_to_pre410(const cbPKT_SYSPROTOCOLMONITOR &src, uint8_t* dest_payload); + + static size_t translate_CHANINFO_pre410_to_current(const uint8_t* src_payload, cbPKT_CHANINFO* dest); + static size_t translate_CHANINFO_current_to_pre410(const cbPKT_CHANINFO &pkt, uint8_t* dest_payload); + + static size_t translate_CHANRESET_pre420_to_current(const uint8_t* src_payload, cbPKT_CHANRESET* dest); + static size_t translate_CHANRESET_current_to_pre420(const cbPKT_CHANRESET &pkt, uint8_t* dest_payload); + +private: + // No private members currently +}; + +} // namespace cbproto + +#endif //CBPROTO_PACKET_TRANSLATOR_H diff --git a/src/cbproto/include/cbproto/types.h b/src/cbproto/include/cbproto/types.h new file mode 100644 index 00000000..c9736dec --- /dev/null +++ b/src/cbproto/include/cbproto/types.h @@ -0,0 +1,2246 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file types.h +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Core protocol types and constants +/// +/// This file contains the fundamental types and constants that define the Cerebus protocol. +/// These MUST match Blackrock's cbproto.h exactly to ensure compatibility with Central. +/// +/// DO NOT MODIFY unless updating from upstream protocol changes. +/// +/// Reference: cbproto.h (Protocol Version 4.2) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBPROTO_TYPES_H +#define CBPROTO_TYPES_H + +#include + +// Ensure tight packing for network protocol structures +#pragma pack(push, 1) + +#ifdef __cplusplus +extern "C" { +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Protocol Version +/// @{ + +#define cbVERSION_MAJOR 4 +#define cbVERSION_MINOR 2 + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Time Type +/// @{ + +/// @brief Processor time type +/// Protocol 4.0+ uses 64-bit timestamps +/// Protocol 3.x uses 32-bit timestamps (compile with CBPROTO_311) +#ifdef CBPROTO_311 +typedef uint32_t PROCTIME; +#else +typedef uint64_t PROCTIME; +#endif + +/// @brief Analog-to-digital data type +/// Used for continuous data samples in cbPKT_GROUP +typedef int16_t A2D_DATA; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Maximum Entity Ranges +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 237-262 +/// These define the maximum number of instruments, channels, etc. +/// @{ + +#define cbNSP1 1 ///< First instrument ID (1-based) +#define cbNSP2 2 ///< Second instrument ID (1-based) +#define cbNSP3 3 ///< Third instrument ID (1-based) +#define cbNSP4 4 ///< Fourth instrument ID (1-based) + +#define cbMAXOPEN 4 ///< Maximum number of open cbhwlib's (instruments) +#define cbMAXPROCS 1 ///< Number of processors per NSP + +#define cbNUM_FE_CHANS 256 ///< Front-end channels per NSP + +#define cbRAWGROUP 6 ///< Group number for raw data feed +#define cbMAXGROUPS 8 ///< Number of sample rate groups +#define cbMAXFILTS 32 ///< Maximum number of filters +#define cbFIRST_DIGITAL_FILTER 13 ///< (0-based) filter number, must be less than cbMAXFILTS +#define cbNUM_DIGITAL_FILTERS 4 ///< Number of custom digital filters +#define cbMAXHOOPS 4 ///< Maximum number of hoops for spike sorting +#define cbMAXSITES 4 ///< Maximum number of electrodes in an n-trode group +#define cbMAXSITEPLOTS ((cbMAXSITES - 1) * cbMAXSITES / 2) ///< Combination of 2 out of n +#define cbMAXUNITS 5 ///< Maximum number of sorted units per channel +#define cbMAXNTRODES (cbNUM_ANALOG_CHANS / 2) ///< Maximum n-trodes (stereotrode minimum) +#define cbMAX_PNTS 128 ///< Maximum spike waveform points +#define cbMAXVIDEOSOURCE 1 ///< Maximum number of video sources +#define cbMAXTRACKOBJ 20 ///< Maximum number of trackable objects +#define cbMAX_AOUT_TRIGGER 5 ///< Maximum number of per-channel (analog output, or digital output) triggers + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Channel Counts +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 252-262 +/// Note: Some channel counts depend on cbMAXPROCS +/// @{ + +#define cbNUM_ANAIN_CHANS (16 * cbMAXPROCS) ///< Analog input channels +#define cbNUM_ANALOG_CHANS (cbNUM_FE_CHANS + cbNUM_ANAIN_CHANS) ///< Total analog inputs +#define cbNUM_ANAOUT_CHANS (4 * cbMAXPROCS) ///< Analog output channels +#define cbNUM_AUDOUT_CHANS (2 * cbMAXPROCS) ///< Audio output channels +#define cbNUM_ANALOGOUT_CHANS (cbNUM_ANAOUT_CHANS + cbNUM_AUDOUT_CHANS) ///< Total analog outputs +#define cbNUM_DIGIN_CHANS (1 * cbMAXPROCS) ///< Digital input channels +#define cbNUM_SERIAL_CHANS (1 * cbMAXPROCS) ///< Serial input channels +#define cbNUM_DIGOUT_CHANS (4 * cbMAXPROCS) ///< Digital output channels + +/// @brief Number of analog/audio output channels with gain +/// This is the number of AOUT channels with gain. Conveniently, the 4 Analog Outputs +/// and the 2 Audio Outputs are right next to each other in the channel numbering sequence. +#define AOUT_NUM_GAIN_CHANS (cbNUM_ANAOUT_CHANS + cbNUM_AUDOUT_CHANS) + +/// @brief Total number of channels +#define cbMAXCHANS (cbNUM_ANALOG_CHANS + cbNUM_ANALOGOUT_CHANS + \ + cbNUM_DIGIN_CHANS + cbNUM_SERIAL_CHANS + cbNUM_DIGOUT_CHANS) + +#define cbFIRST_FE_CHAN 0 ///< First Front end channel (0-based) + +// Bank definitions - if any channel types exceed cbCHAN_PER_BANK, banks must be increased +#define cbCHAN_PER_BANK 32 ///< Channels per bank +#define cbNUM_FE_BANKS (cbNUM_FE_CHANS / cbCHAN_PER_BANK) ///< Front end banks +#define cbNUM_ANAIN_BANKS 1 ///< Analog Input banks +#define cbNUM_ANAOUT_BANKS 1 ///< Analog Output banks +#define cbNUM_AUDOUT_BANKS 1 ///< Audio Output banks +#define cbNUM_DIGIN_BANKS 1 ///< Digital Input banks +#define cbNUM_SERIAL_BANKS 1 ///< Serial Input banks +#define cbNUM_DIGOUT_BANKS 1 ///< Digital Output banks + +#define cbMAXBANKS (cbNUM_FE_BANKS + cbNUM_ANAIN_BANKS + cbNUM_ANAOUT_BANKS + \ + cbNUM_AUDOUT_BANKS + cbNUM_DIGIN_BANKS + cbNUM_SERIAL_BANKS + \ + cbNUM_DIGOUT_BANKS) + +#define SCALE_LNC_COUNT 17 +#define SCALE_CONTINUOUS_COUNT 17 +#define SCALE_SPIKE_COUNT 23 + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Network Configuration +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 193-211 +/// @{ + +#define cbNET_UDP_ADDR_INST "192.168.137.1" ///< Cerebus default address +#define cbNET_UDP_ADDR_CNT "192.168.137.128" ///< Gemini NSP default control address +#define cbNET_UDP_ADDR_BCAST "192.168.137.255" ///< NSP default broadcast address +#define cbNET_UDP_PORT_BCAST 51002 ///< Neuroflow Data Port +#define cbNET_UDP_PORT_CNT 51001 ///< Neuroflow Control Port + +// Maximum UDP datagram size used to transport cerebus packets, taken from MTU size +#define cbCER_UDP_SIZE_MAX 58080 ///< Note that multiple packets may reside in one udp datagram as aggregate + +// Gemini network configuration +#define cbNET_TCP_PORT_GEMINI 51005 ///< Neuroflow Data Port +#define cbNET_TCP_ADDR_GEMINI_HUB "192.168.137.200" ///< NSP default control address + +#define cbNET_UDP_ADDR_HOST "192.168.137.199" ///< Cerebus (central) default address +#define cbNET_UDP_ADDR_GEMINI_NSP "192.168.137.128" ///< NSP default control address +#define cbNET_UDP_ADDR_GEMINI_HUB "192.168.137.200" ///< HUB default control address +#define cbNET_UDP_ADDR_GEMINI_HUB2 "192.168.137.201" ///< HUB2 default control address +#define cbNET_UDP_ADDR_GEMINI_HUB3 "192.168.137.202" ///< HUB3 default control address +#define cbNET_UDP_PORT_GEMINI_NSP 51001 ///< Gemini NSP Port +#define cbNET_UDP_PORT_GEMINI_HUB 51002 ///< Gemini HUB Port +#define cbNET_UDP_PORT_GEMINI_HUB2 51003 ///< Gemini HUB2 Port +#define cbNET_UDP_PORT_GEMINI_HUB3 51004 ///< Gemini HUB3 Port + +// Protocol types +#define PROTOCOL_UDP 0 ///< UDP protocol +#define PROTOCOL_TCP 1 ///< TCP protocol + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name String Length Constants +/// @{ + +#define cbLEN_STR_UNIT 8 ///< Length of unit string +#define cbLEN_STR_LABEL 16 ///< Length of label string +#define cbLEN_STR_FILT_LABEL 16 ///< Length of filter label string +#define cbLEN_STR_IDENT 64 ///< Length of identity string +#define cbLEN_STR_COMMENT 256 ///< Length of comment string +#define cbMAX_COMMENT 128 ///< Maximum comment length (must be multiple of 4) + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Library Result Definitions +/// @{ + +typedef unsigned int cbRESULT; + +#define cbRESULT_OK 0 ///< Function executed normally +#define cbRESULT_NOLIBRARY 1 ///< The library was not properly initialized +#define cbRESULT_NOCENTRALAPP 2 ///< Unable to access the central application +#define cbRESULT_LIBINITERROR 3 ///< Error attempting to initialize library error +#define cbRESULT_MEMORYUNAVAIL 4 ///< Not enough memory available to complete the operation +#define cbRESULT_INVALIDADDRESS 5 ///< Invalid Processor or Bank address +#define cbRESULT_INVALIDCHANNEL 6 ///< Invalid channel ID passed to function +#define cbRESULT_INVALIDFUNCTION 7 ///< Channel exists, but requested function is not available +#define cbRESULT_NOINTERNALCHAN 8 ///< No internal channels available to connect hardware stream +#define cbRESULT_HARDWAREOFFLINE 9 ///< Hardware is offline or unavailable +#define cbRESULT_DATASTREAMING 10 ///< Hardware is streaming data and cannot be configured +#define cbRESULT_NONEWDATA 11 ///< There is no new data to be read in +#define cbRESULT_DATALOST 12 ///< The Central App incoming data buffer has wrapped +#define cbRESULT_INVALIDNTRODE 13 ///< Invalid NTrode number passed to function +#define cbRESULT_BUFRECALLOCERR 14 ///< Receive buffer could not be allocated +#define cbRESULT_BUFGXMTALLOCERR 15 ///< Global transmit buffer could not be allocated +#define cbRESULT_BUFLXMTALLOCERR 16 ///< Local transmit buffer could not be allocated +#define cbRESULT_BUFCFGALLOCERR 17 ///< Configuration buffer could not be allocated +#define cbRESULT_BUFPCSTATALLOCERR 18 ///< PC status buffer could not be allocated +#define cbRESULT_BUFSPKALLOCERR 19 ///< Spike cache buffer could not be allocated +#define cbRESULT_EVSIGERR 20 ///< Couldn't create shared event signal +#define cbRESULT_SOCKERR 21 ///< Generic socket creation error +#define cbRESULT_SOCKOPTERR 22 ///< Socket option error (possibly permission issue) +#define cbRESULT_SOCKMEMERR 23 ///< Socket memory assignment error +#define cbRESULT_INSTINVALID 24 ///< Invalid range or instrument address +#define cbRESULT_SOCKBIND 25 ///< Cannot bind to any address (possibly no Instrument network) +#define cbRESULT_SYSLOCK 26 ///< Cannot (un)lock the system resources (possibly resource busy) + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Packet Header +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 376-383 +/// Every packet contains this header (must be a multiple of uint32_t) +/// @{ + +/// @brief Cerebus packet header data structure +typedef struct { + PROCTIME time; ///< System clock timestamp + uint16_t chid; ///< Channel identifier + uint16_t type; ///< Packet type + uint16_t dlen; ///< Length of data field in 32-bit chunks + uint8_t instrument; ///< Instrument number (0-based in packet, despite cbNSP1=1!) + uint8_t reserved; ///< Reserved for future use +} cbPKT_HEADER; + +#define cbPKT_MAX_SIZE 1024 ///< Maximum packet size in bytes +#define cbPKT_HEADER_SIZE sizeof(cbPKT_HEADER) ///< Packet header size in bytes +#define cbPKT_HEADER_32SIZE (cbPKT_HEADER_SIZE / 4) ///< Packet header size in uint32_t's + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Generic Packet +/// +/// Base packet structure for all packet types +/// @{ + +/// @brief Generic packet structure +typedef struct { + cbPKT_HEADER cbpkt_header; ///< Packet header + union { + uint8_t data_u8[cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE]; ///< Data as uint8_t array + uint16_t data_u16[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE) / 2]; ///< Data as uint16_t array + uint32_t data_u32[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE) / 4]; ///< Data as uint32_t array + }; +} cbPKT_GENERIC; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Old Packet Header and Generic (for CCF file compatibility) +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 385-420 +/// @{ + +/// @brief Old Cerebus packet header data structure +/// +/// This is used to read old CCF files +typedef struct { + uint32_t time; ///< system clock timestamp + uint16_t chid; ///< channel identifier + uint8_t type; ///< packet type + uint8_t dlen; ///< length of data field in 32-bit chunks +} cbPKT_HEADER_OLD; + +#define cbPKT_HEADER_SIZE_OLD sizeof(cbPKT_HEADER_OLD) ///< define the size of the old packet header in bytes + +/// @brief Old Generic Cerebus packet data structure (1024 bytes total) +/// +/// This is used to read old CCF files +typedef struct { + cbPKT_HEADER_OLD cbpkt_header; + + uint32_t data[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE_OLD) / 4]; ///< data buffer (up to 1016 bytes) +} cbPKT_GENERIC_OLD; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Dependent Structures +/// +/// These structures are used within packet structures +/// @{ + +// Filter type flags (used in cbFILTDESC hptype/lptype fields) +#define cbFILTTYPE_PHYSICAL 0x0001 +#define cbFILTTYPE_DIGITAL 0x0002 +#define cbFILTTYPE_ADAPTIVE 0x0004 +#define cbFILTTYPE_NONLINEAR 0x0008 +#define cbFILTTYPE_BUTTERWORTH 0x0100 +#define cbFILTTYPE_CHEBYCHEV 0x0200 +#define cbFILTTYPE_BESSEL 0x0400 +#define cbFILTTYPE_ELLIPTICAL 0x0800 + +/// @brief Filter description structure +/// +/// Filter description used in cbPKT_CHANINFO +typedef struct { + char label[cbLEN_STR_FILT_LABEL]; + uint32_t hpfreq; ///< high-pass corner frequency in milliHertz + uint32_t hporder; ///< high-pass filter order + uint32_t hptype; ///< high-pass filter type + uint32_t lpfreq; ///< low-pass frequency in milliHertz + uint32_t lporder; ///< low-pass filter order + uint32_t lptype; ///< low-pass filter type +} cbFILTDESC; + +/// @brief Scaling structure +/// +/// Structure used in cbPKT_CHANINFO +typedef struct { + int16_t digmin; ///< digital value that cooresponds with the anamin value + int16_t digmax; ///< digital value that cooresponds with the anamax value + int32_t anamin; ///< the minimum analog value present in the signal + int32_t anamax; ///< the maximum analog value present in the signal + int32_t anagain; ///< the gain applied to the default analog values to get the analog values + char anaunit[cbLEN_STR_UNIT]; ///< the unit for the analog signal (eg, "uV" or "MPa") +} cbSCALING; + +/// @brief Amplitude Rejection structure +typedef struct { + uint32_t bEnabled; ///< BOOL implemented as uint32_t - for structure alignment at paragraph boundary + int16_t nAmplPos; ///< any spike that has a value above nAmplPos will be rejected + int16_t nAmplNeg; ///< any spike that has a value below nAmplNeg will be rejected +} cbAMPLITUDEREJECT; + +/// @brief Manual Unit Mapping structure +/// +/// Defines an ellipsoid for sorting. Used in cbPKT_CHANINFO and cbPKT_NTRODEINFO +typedef struct { + int16_t nOverride; ///< override to unit if in ellipsoid + int16_t afOrigin[3]; ///< ellipsoid origin + int16_t afShape[3][3]; ///< ellipsoid shape + int16_t aPhi; ///< + uint32_t bValid; ///< is this unit in use at this time? + ///< BOOL implemented as uint32_t - for structure alignment at paragraph boundary +} cbMANUALUNITMAPPING; + +/// @brief Hoop definition structure +/// +/// Defines the hoop used for sorting. There can be up to 5 hoops per unit. Used in cbPKT_CHANINFO +typedef struct { + uint16_t valid; ///< 0=undefined, 1 for valid + int16_t time; ///< time offset into spike window + int16_t min; ///< minimum value for the hoop window + int16_t max; ///< maximum value for the hoop window +} cbHOOP; + +/// @brief Adaptation type enumeration +/// +/// To control and keep track of how long an element of spike sorting has been adapting. +enum ADAPT_TYPE { + ADAPT_NEVER, ///< 0 - do not adapt at all + ADAPT_ALWAYS, ///< 1 - always adapt + ADAPT_TIMED ///< 2 - adapt if timer not timed out +}; + +/// @brief Adaptive Control structure +typedef struct { + uint32_t nMode; ///< 0-do not adapt at all, 1-always adapt, 2-adapt if timer not timed out + float fTimeOutMinutes; ///< how many minutes until time out + float fElapsedMinutes; ///< the amount of time that has elapsed +} cbAdaptControl; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Configuration Structures +/// +/// Non-packet structures used for configuration +/// @{ + +/// @brief Signal Processor Configuration Structure +typedef struct { + uint32_t idcode; ///< manufacturer part and rom ID code of the Signal Processor + char ident[cbLEN_STR_IDENT]; ///< ID string with the equipment name of the Signal Processor + uint32_t chanbase; ///< lowest channel identifier claimed by this processor + uint32_t chancount; ///< number of channel identifiers claimed by this processor + uint32_t bankcount; ///< number of signal banks supported by the processor + uint32_t groupcount; ///< number of sample groups supported by the processor + uint32_t filtcount; ///< number of digital filters supported by the processor + uint32_t sortcount; ///< number of channels supported for spike sorting (reserved for future) + uint32_t unitcount; ///< number of supported units for spike sorting (reserved for future) + uint32_t hoopcount; ///< number of supported hoops for spike sorting (reserved for future) + uint32_t reserved; ///< reserved for future use, set to 0 + uint32_t version; ///< current version of libraries +} cbPROCINFO; + +/// @brief Signal Bank Configuration Structure +typedef struct { + uint32_t idcode; ///< manufacturer part and rom ID code of the module addressed to this bank + char ident[cbLEN_STR_IDENT]; ///< ID string with the equipment name of the Signal Bank hardware module + char label[cbLEN_STR_LABEL]; ///< Label on the instrument for the signal bank, eg "Analog In" + uint32_t chanbase; ///< lowest channel identifier claimed by this bank + uint32_t chancount; ///< number of channel identifiers claimed by this bank +} cbBANKINFO; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Packet Type Constants +/// +/// Ground truth from upstream/cbproto/cbproto.h +/// These define all the packet types in the Cerebus protocol +/// @{ + +// System packets +#define cbPKTTYPE_SYSHEARTBEAT 0x00 ///< System heartbeat packet +#define cbPKTTYPE_SYSPROTOCOLMONITOR 0x01 ///< Protocol monitoring packet +#define cbPKTTYPE_PREVREPSTREAM 0x02 ///< Preview reply stream +#define cbPKTTYPE_PREVREP 0x03 ///< Preview reply +#define cbPKTTYPE_PREVREPLNC 0x04 ///< Preview reply LNC +#define cbPKTTYPE_REPCONFIGALL 0x08 ///< Response that NSP got your request +#define cbPKTTYPE_SYSREP 0x10 ///< System reply +#define cbPKTTYPE_SYSREPSPKLEN 0x11 ///< System reply spike length +#define cbPKTTYPE_SYSREPRUNLEV 0x12 ///< System reply runlevel + +// Processor, bank, and filter information packets +#define cbPKTTYPE_PROCREP 0x21 ///< Processor information reply +#define cbPKTTYPE_BANKREP 0x22 ///< Bank information reply +#define cbPKTTYPE_FILTREP 0x23 ///< Filter information reply +#define cbPKTTYPE_CHANRESETREP 0x24 ///< Channel reset reply +#define cbPKTTYPE_ADAPTFILTREP 0x25 ///< Adaptive filter reply +#define cbPKTTYPE_REFELECFILTREP 0x26 ///< Reference electrode filter reply +#define cbPKTTYPE_REPNTRODEINFO 0x27 ///< N-Trode information reply +#define cbPKTTYPE_LNCREP 0x28 ///< LNC reply +#define cbPKTTYPE_VIDEOSYNCHREP 0x29 ///< Video sync reply + +// Sample group and configuration packets +#define cbPKTTYPE_GROUPREP 0x30 ///< Sample group information reply +#define cbPKTTYPE_COMMENTREP 0x31 ///< Comment reply +#define cbPKTTYPE_NMREP 0x32 ///< NeuroMotive (NM) reply +#define cbPKTTYPE_WAVEFORMREP 0x33 ///< Waveform reply +#define cbPKTTYPE_STIMULATIONREP 0x34 ///< Stimulation reply + +// Channel information packets - Reply (0x40-0x4F) +#define cbPKTTYPE_CHANREP 0x40 ///< Channel information reply +#define cbPKTTYPE_CHANREPLABEL 0x41 ///< Channel label reply +#define cbPKTTYPE_CHANREPSCALE 0x42 ///< Channel scale reply +#define cbPKTTYPE_CHANREPDOUT 0x43 ///< Channel digital output reply +#define cbPKTTYPE_CHANREPDINP 0x44 ///< Channel digital input reply +#define cbPKTTYPE_CHANREPAOUT 0x45 ///< Channel analog output reply +#define cbPKTTYPE_CHANREPDISP 0x46 ///< Channel display reply +#define cbPKTTYPE_CHANREPAINP 0x47 ///< Channel analog input reply +#define cbPKTTYPE_CHANREPSMP 0x48 ///< Channel sampling reply +#define cbPKTTYPE_CHANREPSPK 0x49 ///< Channel spike reply +#define cbPKTTYPE_CHANREPSPKTHR 0x4A ///< Channel spike threshold reply +#define cbPKTTYPE_CHANREPSPKHPS 0x4B ///< Channel spike hoops reply +#define cbPKTTYPE_CHANREPUNITOVERRIDES 0x4C ///< Channel unit overrides reply +#define cbPKTTYPE_CHANREPNTRODEGROUP 0x4D ///< Channel n-trode group reply +#define cbPKTTYPE_CHANREPREJECTAMPLITUDE 0x4E ///< Channel reject amplitude reply +#define cbPKTTYPE_CHANREPAUTOTHRESHOLD 0x4F ///< Channel auto threshold reply + +// Spike sorting packets - Reply (0x50-0x5F) +#define cbPKTTYPE_SS_MODELALLREP 0x50 ///< Spike sorting model all reply +#define cbPKTTYPE_SS_MODELREP 0x51 ///< Spike sorting model reply +#define cbPKTTYPE_SS_DETECTREP 0x52 ///< Spike sorting detect reply +#define cbPKTTYPE_SS_ARTIF_REJECTREP 0x53 ///< Spike sorting artifact reject reply +#define cbPKTTYPE_SS_NOISE_BOUNDARYREP 0x54 ///< Spike sorting noise boundary reply +#define cbPKTTYPE_SS_STATISTICSREP 0x55 ///< Spike sorting statistics reply +#define cbPKTTYPE_SS_RESETREP 0x56 ///< Spike sorting reset reply +#define cbPKTTYPE_SS_STATUSREP 0x57 ///< Spike sorting status reply +#define cbPKTTYPE_SS_RESET_MODEL_REP 0x58 ///< Spike sorting reset model reply +#define cbPKTTYPE_SS_RECALCREP 0x59 ///< Spike sorting recalculate reply +#define cbPKTTYPE_FS_BASISREP 0x5B ///< Feature space basis reply +#define cbPKTTYPE_NPLAYREP 0x5C ///< NPlay reply +#define cbPKTTYPE_SET_DOUTREP 0x5D ///< Set digital output reply +#define cbPKTTYPE_TRIGGERREP 0x5E ///< Trigger reply +#define cbPKTTYPE_VIDEOTRACKREP 0x5F ///< Video track reply + +// File and configuration packets - Reply (0x60-0x6F) +#define cbPKTTYPE_REPFILECFG 0x61 ///< File configuration reply +#define cbPKTTYPE_REPUNITSELECTION 0x62 ///< Unit selection reply +#define cbPKTTYPE_LOGREP 0x63 ///< Log reply +#define cbPKTTYPE_REPPATIENTINFO 0x64 ///< Patient info reply +#define cbPKTTYPE_REPIMPEDANCE 0x65 ///< Impedance reply +#define cbPKTTYPE_REPINITIMPEDANCE 0x66 ///< Initial impedance reply +#define cbPKTTYPE_REPPOLL 0x67 ///< Poll reply +#define cbPKTTYPE_REPMAPFILE 0x68 ///< Map file reply + +// Update packets +#define cbPKTTYPE_UPDATEREP 0x71 ///< Update reply + +// Preview packets - Set +#define cbPKTTYPE_PREVSETSTREAM 0x82 ///< Preview set stream +#define cbPKTTYPE_PREVSET 0x83 ///< Preview set +#define cbPKTTYPE_PREVSETLNC 0x84 ///< Preview set LNC + +// System packets - Request (0x88-0x9F) +#define cbPKTTYPE_REQCONFIGALL 0x88 ///< Request for ALL configuration information +#define cbPKTTYPE_SYSSET 0x90 ///< System set +#define cbPKTTYPE_SYSSETSPKLEN 0x91 ///< System set spike length +#define cbPKTTYPE_SYSSETRUNLEV 0x92 ///< System set runlevel + +// Filter and configuration packets - Set (0xA0-0xAF) +#define cbPKTTYPE_FILTSET 0xA3 ///< Filter set +#define cbPKTTYPE_CHANRESET 0xA4 ///< Channel reset +#define cbPKTTYPE_ADAPTFILTSET 0xA5 ///< Adaptive filter set +#define cbPKTTYPE_REFELECFILTSET 0xA6 ///< Reference electrode filter set +#define cbPKTTYPE_SETNTRODEINFO 0xA7 ///< N-Trode information set +#define cbPKTTYPE_LNCSET 0xA8 ///< LNC set +#define cbPKTTYPE_VIDEOSYNCHSET 0xA9 ///< Video sync set + +// Sample group and waveform packets - Set (0xB0-0xBF) +#define cbPKTTYPE_GROUPSET 0xB0 ///< Sample group set +#define cbPKTTYPE_COMMENTSET 0xB1 ///< Comment set +#define cbPKTTYPE_NMSET 0xB2 ///< NeuroMotive set +#define cbPKTTYPE_WAVEFORMSET 0xB3 ///< Waveform set +#define cbPKTTYPE_STIMULATIONSET 0xB4 ///< Stimulation set + +// Channel information packets - Set (0xC0-0xCF) +#define cbPKTTYPE_CHANSET 0xC0 ///< Channel information set +#define cbPKTTYPE_CHANSETLABEL 0xC1 ///< Channel label set +#define cbPKTTYPE_CHANSETSCALE 0xC2 ///< Channel scale set +#define cbPKTTYPE_CHANSETDOUT 0xC3 ///< Channel digital output set +#define cbPKTTYPE_CHANSETDINP 0xC4 ///< Channel digital input set +#define cbPKTTYPE_CHANSETAOUT 0xC5 ///< Channel analog output set +#define cbPKTTYPE_CHANSETDISP 0xC6 ///< Channel display set +#define cbPKTTYPE_CHANSETAINP 0xC7 ///< Channel analog input set +#define cbPKTTYPE_CHANSETSMP 0xC8 ///< Channel sampling set +#define cbPKTTYPE_CHANSETSPK 0xC9 ///< Channel spike set +#define cbPKTTYPE_CHANSETSPKTHR 0xCA ///< Channel spike threshold set +#define cbPKTTYPE_CHANSETSPKHPS 0xCB ///< Channel spike hoops set +#define cbPKTTYPE_CHANSETUNITOVERRIDES 0xCC ///< Channel unit overrides set +#define cbPKTTYPE_CHANSETNTRODEGROUP 0xCD ///< Channel n-trode group set +#define cbPKTTYPE_CHANSETREJECTAMPLITUDE 0xCE ///< Channel reject amplitude set +#define cbPKTTYPE_CHANSETAUTOTHRESHOLD 0xCF ///< Channel auto threshold set + +// Spike sorting packets - Set (0xD0-0xDF) +#define cbPKTTYPE_SS_MODELALLSET 0xD0 ///< Spike sorting model all set +#define cbPKTTYPE_SS_MODELSET 0xD1 ///< Spike sorting model set +#define cbPKTTYPE_SS_DETECTSET 0xD2 ///< Spike sorting detect set +#define cbPKTTYPE_SS_ARTIF_REJECTSET 0xD3 ///< Spike sorting artifact reject set +#define cbPKTTYPE_SS_NOISE_BOUNDARYSET 0xD4 ///< Spike sorting noise boundary set +#define cbPKTTYPE_SS_STATISTICSSET 0xD5 ///< Spike sorting statistics set +#define cbPKTTYPE_SS_RESETSET 0xD6 ///< Spike sorting reset set +#define cbPKTTYPE_SS_STATUSSET 0xD7 ///< Spike sorting status set +#define cbPKTTYPE_SS_RESET_MODEL_SET 0xD8 ///< Spike sorting reset model set +#define cbPKTTYPE_SS_RECALCSET 0xD9 ///< Spike sorting recalculate set +#define cbPKTTYPE_FS_BASISSET 0xDB ///< Feature space basis set +#define cbPKTTYPE_NPLAYSET 0xDC ///< NPlay set +#define cbPKTTYPE_SET_DOUTSET 0xDD ///< Set digital output set +#define cbPKTTYPE_TRIGGERSET 0xDE ///< Trigger set +#define cbPKTTYPE_VIDEOTRACKSET 0xDF ///< Video track set + +// Packet type masks and conversion constants +#define cbPKTTYPE_MASKED_REFLECTED 0xE0 ///< Masked reflected packet type +#define cbPKTTYPE_COMPARE_MASK_REFLECTED 0xF0 ///< Compare mask for reflected packets +#define cbPKTTYPE_REFLECTED_CONVERSION_MASK 0x7F ///< Reflected conversion mask + +// File and configuration packets - Set (0xE0-0xEF) +#define cbPKTTYPE_SETFILECFG 0xE1 ///< File configuration set +#define cbPKTTYPE_SETUNITSELECTION 0xE2 ///< Unit selection set +#define cbPKTTYPE_LOGSET 0xE3 ///< Log set +#define cbPKTTYPE_SETPATIENTINFO 0xE4 ///< Patient info set +#define cbPKTTYPE_SETIMPEDANCE 0xE5 ///< Impedance set +#define cbPKTTYPE_SETINITIMPEDANCE 0xE6 ///< Initial impedance set +#define cbPKTTYPE_SETPOLL 0xE7 ///< Poll set +#define cbPKTTYPE_SETMAPFILE 0xE8 ///< Map file set + +// Update packets - Set +#define cbPKTTYPE_UPDATESET 0xF1 ///< Update set + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Channel Constants +/// @{ + +#define cbPKTCHAN_CONFIGURATION 0x8000 ///< Channel # to mean configuration + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Channel Capability Flags +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 554-562 +/// These flags define the capabilities of each channel +/// @{ + +#define cbCHAN_EXISTS 0x00000001 ///< Channel id is allocated +#define cbCHAN_CONNECTED 0x00000002 ///< Channel is connected and mapped and ready to use +#define cbCHAN_ISOLATED 0x00000004 ///< Channel is electrically isolated +#define cbCHAN_AINP 0x00000100 ///< Channel has analog input capabilities +#define cbCHAN_AOUT 0x00000200 ///< Channel has analog output capabilities +#define cbCHAN_DINP 0x00000400 ///< Channel has digital input capabilities +#define cbCHAN_DOUT 0x00000800 ///< Channel has digital output capabilities +#define cbCHAN_GYRO 0x00001000 ///< Channel has gyroscope/accelerometer/magnetometer/temperature capabilities + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Digital Input Capability and Option Flags +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 569-590 +/// @{ + +#define cbDINP_SERIALMASK 0x000000FF ///< Bit mask used to detect RS232 Serial Baud Rates +#define cbDINP_BAUD2400 0x00000001 ///< RS232 Serial Port operates at 2400 (n-8-1) +#define cbDINP_BAUD9600 0x00000002 ///< RS232 Serial Port operates at 9600 (n-8-1) +#define cbDINP_BAUD19200 0x00000004 ///< RS232 Serial Port operates at 19200 (n-8-1) +#define cbDINP_BAUD38400 0x00000008 ///< RS232 Serial Port operates at 38400 (n-8-1) +#define cbDINP_BAUD57600 0x00000010 ///< RS232 Serial Port operates at 57600 (n-8-1) +#define cbDINP_BAUD115200 0x00000020 ///< RS232 Serial Port operates at 115200 (n-8-1) +#define cbDINP_1BIT 0x00000100 ///< Port has a single input bit (eg single BNC input) +#define cbDINP_8BIT 0x00000200 ///< Port has 8 input bits +#define cbDINP_16BIT 0x00000400 ///< Port has 16 input bits +#define cbDINP_32BIT 0x00000800 ///< Port has 32 input bits +#define cbDINP_ANYBIT 0x00001000 ///< Capture the port value when any bit changes. +#define cbDINP_WRDSTRB 0x00002000 ///< Capture the port when a word-write line is strobed +#define cbDINP_PKTCHAR 0x00004000 ///< Capture packets using an End of Packet Character +#define cbDINP_PKTSTRB 0x00008000 ///< Capture packets using an End of Packet Logic Input +#define cbDINP_MONITOR 0x00010000 ///< Port controls other ports or system events +#define cbDINP_REDGE 0x00020000 ///< Capture the port value when any bit changes lo-2-hi (rising edge) +#define cbDINP_FEDGE 0x00040000 ///< Capture the port value when any bit changes hi-2-lo (falling edge) +#define cbDINP_STRBANY 0x00080000 ///< Capture packets using 8-bit strobe/8-bit any Input +#define cbDINP_STRBRIS 0x00100000 ///< Capture packets using 8-bit strobe/8-bit rising edge Input +#define cbDINP_STRBFAL 0x00200000 ///< Capture packets using 8-bit strobe/8-bit falling edge Input +#define cbDINP_MASK (cbDINP_ANYBIT | cbDINP_WRDSTRB | cbDINP_PKTCHAR | cbDINP_PKTSTRB | cbDINP_MONITOR | cbDINP_REDGE | cbDINP_FEDGE | cbDINP_STRBANY | cbDINP_STRBRIS | cbDINP_STRBFAL) + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Digital Output Capability and Option Flags +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 599-630 +/// @{ + +#define cbDOUT_SERIALMASK 0x000000FF ///< Port operates as an RS232 Serial Connection +#define cbDOUT_BAUD2400 0x00000001 ///< Serial Port operates at 2400 (n-8-1) +#define cbDOUT_BAUD9600 0x00000002 ///< Serial Port operates at 9600 (n-8-1) +#define cbDOUT_BAUD19200 0x00000004 ///< Serial Port operates at 19200 (n-8-1) +#define cbDOUT_BAUD38400 0x00000008 ///< Serial Port operates at 38400 (n-8-1) +#define cbDOUT_BAUD57600 0x00000010 ///< Serial Port operates at 57600 (n-8-1) +#define cbDOUT_BAUD115200 0x00000020 ///< Serial Port operates at 115200 (n-8-1) +#define cbDOUT_1BIT 0x00000100 ///< Port has a single output bit (eg single BNC output) +#define cbDOUT_8BIT 0x00000200 ///< Port has 8 output bits +#define cbDOUT_16BIT 0x00000400 ///< Port has 16 output bits +#define cbDOUT_32BIT 0x00000800 ///< Port has 32 output bits +#define cbDOUT_VALUE 0x00010000 ///< Port can be manually configured +#define cbDOUT_TRACK 0x00020000 ///< Port should track the most recently selected channel +#define cbDOUT_FREQUENCY 0x00040000 ///< Port can output a frequency +#define cbDOUT_TRIGGERED 0x00080000 ///< Port can be triggered +#define cbDOUT_MONITOR_UNIT0 0x01000000 ///< Can monitor unit 0 = UNCLASSIFIED +#define cbDOUT_MONITOR_UNIT1 0x02000000 ///< Can monitor unit 1 +#define cbDOUT_MONITOR_UNIT2 0x04000000 ///< Can monitor unit 2 +#define cbDOUT_MONITOR_UNIT3 0x08000000 ///< Can monitor unit 3 +#define cbDOUT_MONITOR_UNIT4 0x10000000 ///< Can monitor unit 4 +#define cbDOUT_MONITOR_UNIT5 0x20000000 ///< Can monitor unit 5 +#define cbDOUT_MONITOR_UNIT_ALL 0x3F000000 ///< Can monitor ALL units +#define cbDOUT_MONITOR_SHIFT_TO_FIRST_UNIT 24 ///< This tells us how many bit places to get to unit 1 + +// Trigger types for Digital Output channels +#define cbDOUT_TRIGGER_NONE 0 ///< instant software trigger +#define cbDOUT_TRIGGER_DINPRISING 1 ///< digital input rising edge trigger +#define cbDOUT_TRIGGER_DINPFALLING 2 ///< digital input falling edge trigger +#define cbDOUT_TRIGGER_SPIKEUNIT 3 ///< spike unit +#define cbDOUT_TRIGGER_NM 4 ///< comment RGBA color (A being big byte) +#define cbDOUT_TRIGGER_RECORDINGSTART 5 ///< recording start trigger +#define cbDOUT_TRIGGER_EXTENSION 6 ///< extension trigger + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Analog Input Capability and Option Flags +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 696-723 +/// @{ + +#define cbAINP_RAWPREVIEW 0x00000001 ///< Generate scrolling preview data for the raw channel +#define cbAINP_LNC 0x00000002 ///< Line Noise Cancellation +#define cbAINP_LNCPREVIEW 0x00000004 ///< Retrieve the LNC correction waveform +#define cbAINP_SMPSTREAM 0x00000010 ///< stream the analog input stream directly to disk +#define cbAINP_SMPFILTER 0x00000020 ///< Digitally filter the analog input stream +#define cbAINP_RAWSTREAM 0x00000040 ///< Raw data stream available +#define cbAINP_SPKSTREAM 0x00000100 ///< Spike Stream is available +#define cbAINP_SPKFILTER 0x00000200 ///< Selectable Filters +#define cbAINP_SPKPREVIEW 0x00000400 ///< Generate scrolling preview of the spike channel +#define cbAINP_SPKPROC 0x00000800 ///< Channel is able to do online spike processing +#define cbAINP_OFFSET_CORRECT_CAP 0x00001000 ///< Offset correction mode (0-disabled 1-enabled) + +#define cbAINP_LNC_OFF 0x00000000 ///< Line Noise Cancellation disabled +#define cbAINP_LNC_RUN_HARD 0x00000001 ///< Hardware-based LNC running and adapting according to the adaptation const +#define cbAINP_LNC_RUN_SOFT 0x00000002 ///< Software-based LNC running and adapting according to the adaptation const +#define cbAINP_LNC_HOLD 0x00000004 ///< LNC running, but not adapting +#define cbAINP_LNC_MASK 0x00000007 ///< Mask for LNC Flags +#define cbAINP_REFELEC_LFPSPK 0x00000010 ///< Apply reference electrode to LFP & Spike +#define cbAINP_REFELEC_SPK 0x00000020 ///< Apply reference electrode to Spikes only +#define cbAINP_REFELEC_MASK 0x00000030 ///< Mask for Reference Electrode flags +#define cbAINP_RAWSTREAM_ENABLED 0x00000040 ///< Raw data stream enabled +#define cbAINP_OFFSET_CORRECT 0x00000100 ///< Offset correction mode (0-disabled 1-enabled) + +// Preview request packet identifiers +#define cbAINPPREV_LNC 0x81 +#define cbAINPPREV_STREAM 0x82 +#define cbAINPPREV_ALL 0x83 + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Analog Input Spike Processing Flags +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 728-749 +/// @{ + +#define cbAINPSPK_EXTRACT 0x00000001 ///< Time-stamp and packet to first superthreshold peak +#define cbAINPSPK_REJART 0x00000002 ///< Reject around clipped signals on multiple channels +#define cbAINPSPK_REJCLIP 0x00000004 ///< Reject clipped signals on the channel +#define cbAINPSPK_ALIGNPK 0x00000008 ///< +#define cbAINPSPK_REJAMPL 0x00000010 ///< Reject based on amplitude +#define cbAINPSPK_THRLEVEL 0x00000100 ///< Analog level threshold detection +#define cbAINPSPK_THRENERGY 0x00000200 ///< Energy threshold detection +#define cbAINPSPK_THRAUTO 0x00000400 ///< Auto threshold detection +#define cbAINPSPK_SPREADSORT 0x00001000 ///< Enable Auto spread Sorting +#define cbAINPSPK_CORRSORT 0x00002000 ///< Enable Auto Histogram Correlation Sorting +#define cbAINPSPK_PEAKMAJSORT 0x00004000 ///< Enable Auto Histogram Peak Major Sorting +#define cbAINPSPK_PEAKFISHSORT 0x00008000 ///< Enable Auto Histogram Peak Fisher Sorting +#define cbAINPSPK_HOOPSORT 0x00010000 ///< Enable Manual Hoop Sorting +#define cbAINPSPK_PCAMANSORT 0x00020000 ///< Enable Manual PCA Sorting +#define cbAINPSPK_PCAKMEANSORT 0x00040000 ///< Enable K-means PCA Sorting +#define cbAINPSPK_PCAEMSORT 0x00080000 ///< Enable EM-clustering PCA Sorting +#define cbAINPSPK_PCADBSORT 0x00100000 ///< Enable DBSCAN PCA Sorting +#define cbAINPSPK_AUTOSORT (cbAINPSPK_SPREADSORT | cbAINPSPK_CORRSORT | cbAINPSPK_PEAKMAJSORT | cbAINPSPK_PEAKFISHSORT) ///< old auto sorting methods +#define cbAINPSPK_NOSORT 0x00000000 ///< No sorting +#define cbAINPSPK_PCAAUTOSORT (cbAINPSPK_PCAKMEANSORT | cbAINPSPK_PCAEMSORT | cbAINPSPK_PCADBSORT) ///< All PCA sorting auto algorithms +#define cbAINPSPK_PCASORT (cbAINPSPK_PCAMANSORT | cbAINPSPK_PCAAUTOSORT) ///< All PCA sorting algorithms +#define cbAINPSPK_ALLSORT (cbAINPSPK_AUTOSORT | cbAINPSPK_HOOPSORT | cbAINPSPK_PCASORT) ///< All sorting algorithms + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Analog Output Capability Flags +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 768-778 +/// @{ + +#define cbAOUT_AUDIO 0x00000001 ///< Channel is physically optimized for audio output +#define cbAOUT_SCALE 0x00000002 ///< Output a static value +#define cbAOUT_TRACK 0x00000004 ///< Output a static value +#define cbAOUT_STATIC 0x00000008 ///< Output a static value +#define cbAOUT_MONITORRAW 0x00000010 ///< Monitor an analog signal line - RAW data +#define cbAOUT_MONITORLNC 0x00000020 ///< Monitor an analog signal line - Line Noise Cancelation +#define cbAOUT_MONITORSMP 0x00000040 ///< Monitor an analog signal line - Continuous +#define cbAOUT_MONITORSPK 0x00000080 ///< Monitor an analog signal line - spike +#define cbAOUT_STIMULATE 0x00000100 ///< Stimulation waveform functions are available. +#define cbAOUT_WAVEFORM 0x00000200 ///< Custom Waveform +#define cbAOUT_EXTENSION 0x00000400 ///< Output Waveform from Extension + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Runlevel Constants +/// +/// Ground truth from upstream/cbproto/cbproto.h +/// These define the system runlevel states +/// @{ + +#define cbRUNLEVEL_UPDATE 78 +#define cbRUNLEVEL_STARTUP 10 +#define cbRUNLEVEL_HARDRESET 20 +#define cbRUNLEVEL_STANDBY 30 +#define cbRUNLEVEL_RESET 40 +#define cbRUNLEVEL_RUNNING 50 +#define cbRUNLEVEL_STRESSED 60 +#define cbRUNLEVEL_ERROR 70 +#define cbRUNLEVEL_SHUTDOWN 80 + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Runflags Constants +/// +/// These define the system runflags +/// @{ +#define cbRUNFLAGS_NONE 0 +#define cbRUNFLAGS_LOCK 1 // Lock recording after reset +/// @} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name System Packet Structures +/// +/// Ground truth from upstream/cbproto/cbproto.h +/// @{ + +#define cbPKTTYPE_SYSHEARTBEAT 0x00 +#define cbPKTDLEN_SYSHEARTBEAT ((sizeof(cbPKT_SYSHEARTBEAT)/4) - cbPKT_HEADER_32SIZE) +#define HEARTBEAT_MS 10 + +/// @brief PKT Set:N/A Rep:0x00 - System Heartbeat packet +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 903-914 +/// This is sent every 10ms +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header +} cbPKT_SYSHEARTBEAT; + +/// @brief PKT Set:0x92 Rep:0x12 - System info +/// +/// Contains system information including the runlevel +typedef struct { + cbPKT_HEADER cbpkt_header; ///< Packet header + + uint32_t sysfreq; ///< System sampling clock frequency in Hz + uint32_t spikelen; ///< The length of the spike events + uint32_t spikepre; ///< Spike pre-trigger samples + uint32_t resetque; ///< The channel for the reset to que on + uint32_t runlevel; ///< System runlevel + uint32_t runflags; ///< Lock recording after reset +} cbPKT_SYSINFO; + +#define cbPKTDLEN_SYSINFO ((sizeof(cbPKT_SYSINFO)/4) - cbPKT_HEADER_32SIZE) + +/// @brief Old system info packet +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1086-1099 +/// Used for backward compatibility with old CCF files +#define cbPKTDLEN_OLDSYSINFO ((sizeof(cbPKT_OLDSYSINFO)/4) - 2) + +typedef struct { + uint32_t time; ///< system clock timestamp + uint16_t chid; ///< 0x8000 + uint8_t type; ///< PKTTYPE_SYS* + uint8_t dlen; ///< cbPKT_OLDSYSINFODLEN + + uint32_t sysfreq; ///< System clock frequency in Hz + uint32_t spikelen; ///< The length of the spike events + uint32_t spikepre; ///< Spike pre-trigger samples + uint32_t resetque; ///< The channel for the reset to que on + uint32_t runlevel; ///< System runlevel + uint32_t runflags; +} cbPKT_OLDSYSINFO; + +/// @brief PKT Set:N/A Rep:0x01 - System protocol monitor +/// +/// Packets are sent via UDP. This packet is sent by the NSP periodically (approximately every 10ms) +/// telling the client how many packets have been sent since the last cbPKT_SYSPROTOCOLMONITOR. +/// The client can compare this with the number of packets it has received to detect packet loss. +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1048-1055 +/// +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t sentpkts; ///< Packets sent since last cbPKT_SYSPROTOCOLMONITOR (or 0 if timestamp=0) + ///< The cbPKT_SYSPROTOCOLMONITOR packets are counted as well so this must be >= 1 + uint32_t counter; ///< Counter of cbPKT_SYSPROTOCOLMONITOR packets sent since beginning of NSP time +} cbPKT_SYSPROTOCOLMONITOR; + +#define cbPKTDLEN_SYSPROTOCOLMONITOR ((sizeof(cbPKT_SYSPROTOCOLMONITOR)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xB1 Rep:0x31 - Comment annotation packet +/// +/// This packet injects a comment into the data stream which gets recorded in the file and displayed on Raster. +typedef struct { + cbPKT_HEADER cbpkt_header; //!< packet header + + struct { + uint8_t charset; //!< Character set (0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI) + uint8_t reserved[3]; //!< Reserved (must be 0) + } info; + PROCTIME timeStarted; //!< Start time of when the user started typing the comment + uint32_t rgba; //!< rgba to color the comment + char comment[cbMAX_COMMENT]; //!< Comment +} cbPKT_COMMENT; + +#define cbPKTDLEN_COMMENT ((sizeof(cbPKT_COMMENT)/4) - cbPKT_HEADER_32SIZE) +#define cbPKTDLEN_COMMENTSHORT (cbPKTDLEN_COMMENT - ((sizeof(uint8_t)*cbMAX_COMMENT)/4)) + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Preview Packets +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 2039-2084 +/// @{ + +// PCA collection states +#define cbPCA_START_COLLECTION 0 ///< start collecting samples +#define cbPCA_START_BASIS 1 ///< start basis calculation +#define cbPCA_MANUAL_LAST_SAMPLE 2 ///< the manual-only PCA, samples at zero, calculates PCA basis at 1 and stops at 2 + +// Stream preview flags +#define cbSTREAMPREV_NONE 0x00000000 +#define cbSTREAMPREV_PCABASIS_NONEMPTY 0x00000001 + +#define cbPKTTYPE_PREVREPSTREAM 0x02 +#define cbPKTDLEN_PREVREPSTREAM ((sizeof(cbPKT_STREAMPREV)/4) - cbPKT_HEADER_32SIZE) + +/// @brief Preview packet +/// +/// Sends preview of various data points. This is sent every 10ms +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + int16_t rawmin; ///< minimum raw channel value over last preview period + int16_t rawmax; ///< maximum raw channel value over last preview period + int16_t smpmin; ///< minimum sample channel value over last preview period + int16_t smpmax; ///< maximum sample channel value over last preview period + int16_t spkmin; ///< minimum spike channel value over last preview period + int16_t spkmax; ///< maximum spike channel value over last preview period + uint32_t spkmos; ///< mean of squares + uint32_t eventflag; ///< flag to detail the units that happend in the last sample period + int16_t envmin; ///< minimum envelope channel value over the last preview period + int16_t envmax; ///< maximum envelope channel value over the last preview period + int32_t spkthrlevel; ///< preview of spike threshold level + uint32_t nWaveNum; ///< this tracks the number of waveforms collected in the WCM for each channel + uint32_t nSampleRows; ///< tracks number of sample vectors of waves + uint32_t nFlags; ///< cbSTREAMPREV_* +} cbPKT_STREAMPREV; + +#define cbPKTTYPE_PREVREPLNC 0x04 +#define cbPKTDLEN_PREVREPLNC ((sizeof(cbPKT_LNCPREV)/4) - cbPKT_HEADER_32SIZE) + +/// @brief Preview packet - Line Noise preview +/// +/// Sends a preview of the line noise waveform. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t freq; ///< Estimated line noise frequency * 1000 (zero means not valid) + int16_t wave[300]; ///< lnc cancellation waveform (downsampled by 2) +} cbPKT_LNCPREV; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Additional Configuration Packets +/// @{ + +/// @brief PKT Set:N/A Rep:0x21 - Info about the processor +/// +/// Includes information about the counts of various features of the processor +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t proc; ///< index of the bank + uint32_t idcode; ///< manufacturer part and rom ID code of the Signal Processor + char ident[cbLEN_STR_IDENT]; ///< ID string with the equipment name of the Signal Processor + uint32_t chanbase; ///< lowest channel number of channel id range claimed by this processor + uint32_t chancount; ///< number of channel identifiers claimed by this processor + uint32_t bankcount; ///< number of signal banks supported by the processor + uint32_t groupcount; ///< number of sample groups supported by the processor + uint32_t filtcount; ///< number of digital filters supported by the processor + uint32_t sortcount; ///< number of channels supported for spike sorting (reserved for future) + uint32_t unitcount; ///< number of supported units for spike sorting (reserved for future) + uint32_t hoopcount; ///< number of supported units for spike sorting (reserved for future) + uint32_t reserved; ///< reserved for future use, set to 0 + uint32_t version; ///< current version of libraries +} cbPKT_PROCINFO; + +#define cbPKTDLEN_PROCINFO ((sizeof(cbPKT_PROCINFO)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:N/A Rep:0x22 - Information about the banks in the processor +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t proc; ///< the address of the processor on which the bank resides + uint32_t bank; ///< the address of the bank reported by the packet + uint32_t idcode; ///< manufacturer part and rom ID code of the module addressed to this bank + char ident[cbLEN_STR_IDENT]; ///< ID string with the equipment name of the Signal Bank hardware module + char label[cbLEN_STR_LABEL]; ///< Label on the instrument for the signal bank, eg "Analog In" + uint32_t chanbase; ///< lowest channel number of channel id range claimed by this bank + uint32_t chancount; ///< number of channel identifiers claimed by this bank +} cbPKT_BANKINFO; + +#define cbPKTDLEN_BANKINFO ((sizeof(cbPKT_BANKINFO)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xA3 Rep:0x23 - Filter Information Packet +/// +/// Describes the filters contained in the NSP including the filter coefficients +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t proc; ///< + uint32_t filt; ///< + char label[cbLEN_STR_FILT_LABEL]; // name of the filter + uint32_t hpfreq; ///< high-pass corner frequency in milliHertz + uint32_t hporder; ///< high-pass filter order + uint32_t hptype; ///< high-pass filter type + uint32_t lpfreq; ///< low-pass frequency in milliHertz + uint32_t lporder; ///< low-pass filter order + uint32_t lptype; ///< low-pass filter type + ///< These are for sending the NSP filter info, otherwise the NSP has this stuff in NSPDefaults.c + double gain; ///< filter gain + double sos1a1; ///< filter coefficient + double sos1a2; ///< filter coefficient + double sos1b1; ///< filter coefficient + double sos1b2; ///< filter coefficient + double sos2a1; ///< filter coefficient + double sos2a2; ///< filter coefficient + double sos2b1; ///< filter coefficient + double sos2b2; ///< filter coefficient +} cbPKT_FILTINFO; + +#define cbPKTDLEN_FILTINFO ((sizeof(cbPKT_FILTINFO)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xB0 Rep:0x30 - Sample Group (GROUP) Information Packets +/// +/// Contains information including the name and list of channels for each sample group. The cbPKT_GROUP packet transmits +/// the data for each group based on the list contained here. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t proc; ///< processor number + uint32_t group; ///< group number + char label[cbLEN_STR_LABEL]; ///< sampling group label + uint32_t period; ///< sampling period for the group + uint32_t length; ///< number of channels in the list + uint16_t list[cbNUM_ANALOG_CHANS]; ///< variable length list. The max size is the total number of analog channels +} cbPKT_GROUPINFO; + +#define cbPKTDLEN_GROUPINFO ((sizeof(cbPKT_GROUPINFO)/4) - cbPKT_HEADER_32SIZE) +#define cbPKTDLEN_GROUPINFOSHORT (8) // basic length without list + +/// @brief PKT Set:0xCx Rep:0x4x - Channel Information +/// +/// This contains the details for each channel within the system. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t chan; ///< actual channel id of the channel being configured + uint32_t proc; ///< the address of the processor on which the channel resides + uint32_t bank; ///< the address of the bank on which the channel resides + uint32_t term; ///< the terminal number of the channel within it's bank + uint32_t chancaps; ///< general channel capablities (given by cbCHAN_* flags) + uint32_t doutcaps; ///< digital output capablities (composed of cbDOUT_* flags) + uint32_t dinpcaps; ///< digital input capablities (composed of cbDINP_* flags) + uint32_t aoutcaps; ///< analog output capablities (composed of cbAOUT_* flags) + uint32_t ainpcaps; ///< analog input capablities (composed of cbAINP_* flags) + uint32_t spkcaps; ///< spike processing capabilities + cbSCALING physcalin; ///< physical channel scaling information + cbFILTDESC phyfiltin; ///< physical channel filter definition + cbSCALING physcalout; ///< physical channel scaling information + cbFILTDESC phyfiltout; ///< physical channel filter definition + char label[cbLEN_STR_LABEL]; ///< Label of the channel (null terminated if <16 characters) + uint32_t userflags; ///< User flags for the channel state + int32_t position[4]; ///< reserved for future position information + cbSCALING scalin; ///< user-defined scaling information for AINP + cbSCALING scalout; ///< user-defined scaling information for AOUT + uint32_t doutopts; ///< digital output options (composed of cbDOUT_* flags) + uint32_t dinpopts; ///< digital input options (composed of cbDINP_* flags) + uint32_t aoutopts; ///< analog output options + uint32_t eopchar; ///< digital input capablities (given by cbDINP_* flags) + union { + struct { // separate system channel to instrument specific channel number + uint16_t moninst; ///< instrument of channel to monitor + uint16_t monchan; ///< channel to monitor + int32_t outvalue; ///< output value + }; + struct { // used for digout timed output + uint16_t lowsamples; ///< number of samples to set low for timed output + uint16_t highsamples; ///< number of samples to set high for timed output + int32_t offset; ///< number of samples to offset the transitions for timed output + }; + }; + uint8_t trigtype; ///< trigger type (see cbDOUT_TRIGGER_*) + uint8_t reserved[2]; ///< 2 bytes reserved + uint8_t triginst; ///< instrument of the trigger channel + uint16_t trigchan; ///< trigger channel + uint16_t trigval; ///< trigger value + uint32_t ainpopts; ///< analog input options (composed of cbAINP* flags) + uint32_t lncrate; ///< line noise cancellation filter adaptation rate + uint32_t smpfilter; ///< continuous-time pathway filter id + uint32_t smpgroup; ///< continuous-time pathway sample group + int32_t smpdispmin; ///< continuous-time pathway display factor + int32_t smpdispmax; ///< continuous-time pathway display factor + uint32_t spkfilter; ///< spike pathway filter id + int32_t spkdispmax; ///< spike pathway display factor + int32_t lncdispmax; ///< Line Noise pathway display factor + uint32_t spkopts; ///< spike processing options + int32_t spkthrlevel; ///< spike threshold level + int32_t spkthrlimit; ///< + uint32_t spkgroup; ///< NTrodeGroup this electrode belongs to - 0 is single unit, non-0 indicates a multi-trode grouping + int16_t amplrejpos; ///< Amplitude rejection positive value + int16_t amplrejneg; ///< Amplitude rejection negative value + uint32_t refelecchan; ///< Software reference electrode channel + cbMANUALUNITMAPPING unitmapping[cbMAXUNITS]; ///< manual unit mapping + cbHOOP spkhoops[cbMAXUNITS][cbMAXHOOPS]; ///< spike hoop sorting set +} cbPKT_CHANINFO; + +#define cbPKTDLEN_CHANINFO ((sizeof(cbPKT_CHANINFO)/4) - cbPKT_HEADER_32SIZE) +#define cbPKTDLEN_CHANINFOSHORT (cbPKTDLEN_CHANINFO - ((sizeof(cbHOOP)*cbMAXUNITS*cbMAXHOOPS)/4)) + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Spike Data Packets +/// @{ + +#define cbPKTDLEN_SPK ((sizeof(cbPKT_SPK)/4) - cbPKT_HEADER_32SIZE) +#define cbPKTDLEN_SPKSHORT (cbPKTDLEN_SPK - ((sizeof(int16_t)*cbMAX_PNTS)/4)) + +/// @brief Data packet - Spike waveform data +/// +/// Detected spikes are sent through this packet. The spike waveform may or may not be sent depending +/// on the dlen contained in the header. The waveform can be anywhere from 30 samples to 128 samples +/// based on user configuration. The default spike length is 48 samples. cbpkt_header.chid is the +/// channel number of the spike. cbpkt_header.type is the sorted unit number (0=unsorted, 255=noise). +typedef struct { + cbPKT_HEADER cbpkt_header; ///< in the header for this packet, the type is used as the unit number + + float fPattern[3]; ///< values of the pattern space (Normal uses only 2, PCA uses third) + int16_t nPeak; ///< highest datapoint of the waveform + int16_t nValley; ///< lowest datapoint of the waveform + + int16_t wave[cbMAX_PNTS]; ///< datapoints of each sample of the waveform. Room for all possible points collected + ///< wave must be the last item in the structure because it can be variable length to a max of cbMAX_PNTS +} cbPKT_SPK; + +/// @brief Gyro Data packet - Gyro input data value. +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 890-901 +/// This packet is sent when gyro data has changed. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint8_t gyroscope[4]; ///< X, Y, Z values read from the gyroscope, last byte set to zero + uint8_t accelerometer[4]; ///< X, Y, Z values read from the accelerometer, last byte set to zero + uint8_t magnetometer[4]; ///< X, Y, Z values read from the magnetometer, last byte set to zero + uint16_t temperature; ///< temperature data + uint16_t reserved; ///< set to zero +} cbPKT_GYRO; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Continuous Data Packets +/// @{ + +/// @brief Data packet - Sample Group data packet +/// +/// This packet contains each sample for the specified group. The group is specified in the type member of the +/// header. Groups are currently 1=500S/s, 2=1kS/s, 3=2kS/s, 4=10kS/s, 5=30kS/s, 6=raw (30kS/s no filter). The +/// list of channels associated with each group is transmitted using the cbPKT_GROUPINFO when the list of channels +/// changes. cbpkt_header.chid is always zero. cbpkt_header.type is the group number +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + A2D_DATA data[cbNUM_ANALOG_CHANS]; ///< variable length address list +} cbPKT_GROUP; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Digital Input/Output Data Packets +/// @{ + +#define DINP_EVENT_ANYBIT 0x00000001 ///< Digital input event: any bit changed +#define DINP_EVENT_STROBE 0x00000002 ///< Digital input event: strobe detected + +/// @brief Data packet - Digital input data value +/// +/// This packet is sent when a digital input value has met the criteria set in Hardware Configuration. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + uint32_t valueRead; ///< data read from the digital input port + uint32_t bitsChanged; ///< bits that have changed from the last packet sent + uint32_t eventType; ///< type of event, eg DINP_EVENT_ANYBIT, DINP_EVENT_STROBE +} cbPKT_DINP; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Unit Selection +/// @{ + +// Unit selection masks +enum +{ + UNIT_UNCLASS_MASK = 0x01, // mask to use to say unclassified units are selected + UNIT_1_MASK = 0x02, // mask to use to say unit 1 is selected + UNIT_2_MASK = 0x04, // mask to use to say unit 2 is selected + UNIT_3_MASK = 0x08, // mask to use to say unit 3 is selected + UNIT_4_MASK = 0x10, // mask to use to say unit 4 is selected + UNIT_5_MASK = 0x20, // mask to use to say unit 5 is selected + CONTINUOUS_MASK = 0x40, // mask to use to say the continuous signal is selected + + UNIT_ALL_MASK = UNIT_UNCLASS_MASK | + UNIT_1_MASK | // This means the channel is completely selected + UNIT_2_MASK | + UNIT_3_MASK | + UNIT_4_MASK | + UNIT_5_MASK | + CONTINUOUS_MASK | + 0xFF80, // this is here to select all digital input bits in raster when expanded +}; + +#define cbPKTDLEN_UNITSELECTION ((sizeof(cbPKT_UNIT_SELECTION) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief Unit Selection +/// +/// Packet which says that these channels are now selected +typedef struct +{ + cbPKT_HEADER cbpkt_header; ///< packet header + + int32_t lastchan; ///< Which channel was clicked last. + uint16_t abyUnitSelections[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE - sizeof(int32_t))]; ///< one for each channel, channels are 0 based here, shows units selected +} cbPKT_UNIT_SELECTION; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Channel Reset +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1264-1308 +/// @{ + +#define cbPKTTYPE_CHANRESETREP 0x24 ///< NSP->PC response...ignore all values +#define cbPKTTYPE_CHANRESET 0xA4 ///< PC->NSP request +#define cbPKTDLEN_CHANRESET ((sizeof(cbPKT_CHANRESET) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xA4 Rep:0x24 - Channel reset packet +/// +/// This resets various aspects of a channel. For each member, 0 doesn't change the value, any non-zero value resets +/// the property to factory defaults +/// This is currently not used in the system. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t chan; ///< actual channel id of the channel being configured + + // For all of the values that follow + // 0 = NOT change value; nonzero = reset to factory defaults + + uint8_t label; ///< Channel label + uint8_t userflags; ///< User flags for the channel state + uint8_t position; ///< reserved for future position information + uint8_t scalin; ///< user-defined scaling information + uint8_t scalout; ///< user-defined scaling information + uint8_t doutopts; ///< digital output options (composed of cbDOUT_* flags) + uint8_t dinpopts; ///< digital input options (composed of cbDINP_* flags) + uint8_t aoutopts; ///< analog output options + uint8_t eopchar; ///< the end of packet character + uint8_t moninst; ///< instrument number of channel to monitor + uint8_t monchan; ///< channel to monitor + uint8_t outvalue; ///< output value + uint8_t ainpopts; ///< analog input options (composed of cbAINP_* flags) + uint8_t lncrate; ///< line noise cancellation filter adaptation rate + uint8_t smpfilter; ///< continuous-time pathway filter id + uint8_t smpgroup; ///< continuous-time pathway sample group + uint8_t smpdispmin; ///< continuous-time pathway display factor + uint8_t smpdispmax; ///< continuous-time pathway display factor + uint8_t spkfilter; ///< spike pathway filter id + uint8_t spkdispmax; ///< spike pathway display factor + uint8_t lncdispmax; ///< Line Noise pathway display factor + uint8_t spkopts; ///< spike processing options + uint8_t spkthrlevel; ///< spike threshold level + uint8_t spkthrlimit; ///< + uint8_t spkgroup; ///< NTrodeGroup this electrode belongs to - 0 is single unit, non-0 indicates a multi-trode grouping + uint8_t spkhoops; ///< spike hoop sorting set +} cbPKT_CHANRESET; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Adaptive Filtering +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1310-1333 +/// @{ + +#define cbPKTTYPE_ADAPTFILTREP 0x25 ///< NSP->PC response +#define cbPKTTYPE_ADAPTFILTSET 0xA5 ///< PC->NSP request +#define cbPKTDLEN_ADAPTFILTINFO ((sizeof(cbPKT_ADAPTFILTINFO) / 4) - cbPKT_HEADER_32SIZE) + +// Adaptive filter settings +#define ADAPT_FILT_DISABLED 0 +#define ADAPT_FILT_ALL 1 +#define ADAPT_FILT_SPIKES 2 + +/// @brief PKT Set:0xA5 Rep:0x25 - Adaptive filtering +/// +/// This sets the parameters for the adaptive filtering. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t chan; ///< Ignored + + uint32_t nMode; ///< 0=disabled, 1=filter continuous & spikes, 2=filter spikes + float dLearningRate; ///< speed at which adaptation happens. Very small. e.g. 5e-12 + uint32_t nRefChan1; ///< The first reference channel (1 based). + uint32_t nRefChan2; ///< The second reference channel (1 based). + +} cbPKT_ADAPTFILTINFO; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Reference Electrode Filtering +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1335-1355 +/// @{ + +#define cbPKTTYPE_REFELECFILTREP 0x26 ///< NSP->PC response +#define cbPKTTYPE_REFELECFILTSET 0xA6 ///< PC->NSP request +#define cbPKTDLEN_REFELECFILTINFO ((sizeof(cbPKT_REFELECFILTINFO) / 4) - cbPKT_HEADER_32SIZE) + +// Reference electrode filter settings +#define REFELEC_FILT_DISABLED 0 +#define REFELEC_FILT_ALL 1 +#define REFELEC_FILT_SPIKES 2 + +/// @brief PKT Set:0xA6 Rep:0x26 - Reference Electrode Information. +/// +/// This configures a channel to be referenced by another channel. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t chan; ///< Ignored + + uint32_t nMode; ///< 0=disabled, 1=filter continuous & spikes, 2=filter spikes + uint32_t nRefChan; ///< The reference channel (1 based). +} cbPKT_REFELECFILTINFO; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Line Noise Cancellation +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1910-1926 +/// @{ + +#define cbPKTTYPE_LNCREP 0x28 ///< NSP->PC response +#define cbPKTTYPE_LNCSET 0xA8 ///< PC->NSP request +#define cbPKTDLEN_LNC ((sizeof(cbPKT_LNC) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xA8 Rep:0x28 - Line Noise Cancellation +/// +/// This packet holds the Line Noise Cancellation parameters +typedef struct +{ + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t lncFreq; ///< Nominal line noise frequency to be canceled (in Hz) + uint32_t lncRefChan; ///< Reference channel for lnc synch (1-based) + uint32_t lncGlobalMode; ///< reserved +} cbPKT_LNC; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Video Synchronization +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1113-1128 +/// @{ + +#define cbPKTTYPE_VIDEOSYNCHREP 0x29 ///< NSP->PC response +#define cbPKTTYPE_VIDEOSYNCHSET 0xA9 ///< PC->NSP request +#define cbPKTDLEN_VIDEOSYNCH ((sizeof(cbPKT_VIDEOSYNCH)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xA9 Rep:0x29 - Video/external synchronization packet. +/// +/// This packet comes from NeuroMotive through network or RS232 +/// then is transmitted to the Central after spike or LFP packets to stamp them. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint16_t split; ///< file split number of the video file + uint32_t frame; ///< frame number in last video + uint32_t etime; ///< capture elapsed time (in milliseconds) + uint16_t id; ///< video source id +} cbPKT_VIDEOSYNCH; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name File Configuration +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1550-1584 +/// @{ + +// file config options +#define cbFILECFG_OPT_NONE 0x00000000 ///< Launch File dialog, set file info, start or stop recording +#define cbFILECFG_OPT_KEEPALIVE 0x00000001 ///< Keep-alive message +#define cbFILECFG_OPT_REC 0x00000002 ///< Recording is in progress +#define cbFILECFG_OPT_STOP 0x00000003 ///< Recording stopped +#define cbFILECFG_OPT_NMREC 0x00000004 ///< NeuroMotive recording status +#define cbFILECFG_OPT_CLOSE 0x00000005 ///< Close file application +#define cbFILECFG_OPT_SYNCH 0x00000006 ///< Recording datetime +#define cbFILECFG_OPT_OPEN 0x00000007 ///< Launch File dialog, do not set or do anything +#define cbFILECFG_OPT_TIMEOUT 0x00000008 ///< Keep alive not received so it timed out +#define cbFILECFG_OPT_PAUSE 0x00000009 ///< Recording paused + +// file save configuration packet +#define cbPKTTYPE_REPFILECFG 0x61 +#define cbPKTTYPE_SETFILECFG 0xE1 +#define cbPKTDLEN_FILECFG ((sizeof(cbPKT_FILECFG)/4) - cbPKT_HEADER_32SIZE) +#define cbPKTDLEN_FILECFGSHORT (cbPKTDLEN_FILECFG - ((sizeof(char)*3*cbLEN_STR_COMMENT)/4)) ///< used for keep-alive messages + +/// @brief PKT Set:0xE1 Rep:0x61 - File configuration packet +/// +/// File recording can be started or stopped externally using this packet. It also contains a timeout mechanism to notify +/// if file isn't still recording. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t options; ///< cbFILECFG_OPT_* + uint32_t duration; + uint32_t recording; ///< If cbFILECFG_OPT_NONE this option starts/stops recording remotely + uint32_t extctrl; ///< If cbFILECFG_OPT_REC this is split number (0 for non-TOC) + ///< If cbFILECFG_OPT_STOP this is error code (0 means no error) + + char username[cbLEN_STR_COMMENT]; ///< name of computer issuing the packet + union { + char filename[cbLEN_STR_COMMENT]; ///< filename to record to + char datetime[cbLEN_STR_COMMENT]; ///< + }; + char comment[cbLEN_STR_COMMENT]; ///< comment to include in the file +} cbPKT_FILECFG; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Log Packet +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1008-1037 +/// @{ + +// Log modes +#define cbLOG_MODE_NONE 0 ///< Normal log +#define cbLOG_MODE_CRITICAL 1 ///< Critical log +#define cbLOG_MODE_RPC 2 ///< PC->NSP: Remote Procedure Call (RPC) +#define cbLOG_MODE_PLUGINFO 3 ///< NSP->PC: Plugin information +#define cbLOG_MODE_RPC_RES 4 ///< NSP->PC: Remote Procedure Call Results +#define cbLOG_MODE_PLUGINERR 5 ///< NSP->PC: Plugin error information +#define cbLOG_MODE_RPC_END 6 ///< NSP->PC: Last RPC packet +#define cbLOG_MODE_RPC_KILL 7 ///< PC->NSP: terminate last RPC +#define cbLOG_MODE_RPC_INPUT 8 ///< PC->NSP: RPC command input +#define cbLOG_MODE_UPLOAD_RES 9 ///< NSP->PC: Upload result +#define cbLOG_MODE_ENDPLUGIN 10 ///< PC->NSP: Signal the plugin to end +#define cbLOG_MODE_NSP_REBOOT 11 ///< PC->NSP: Reboot the NSP + +#define cbMAX_LOG 130 ///< Maximum log description +#define cbPKTTYPE_LOGREP 0x63 ///< NPLAY->PC response +#define cbPKTTYPE_LOGSET 0xE3 ///< PC->NPLAY request +#define cbPKTDLEN_LOG ((sizeof(cbPKT_LOG)/4) - cbPKT_HEADER_32SIZE) +#define cbPKTDLEN_LOGSHORT (cbPKTDLEN_LOG - ((sizeof(char)*cbMAX_LOG)/4)) ///< All but description + +/// @brief PKT Set:0xE3 Rep:0x63 - Log packet +/// +/// Similar to the comment packet but used for internal NSP events and extension communication. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint16_t mode; ///< log mode (cbLOG_MODE_*) + char name[cbLEN_STR_LABEL]; ///< Logger source name (Computer name, Plugin name, ...) + char desc[cbMAX_LOG]; ///< description of the change (will fill the rest of the packet) +} cbPKT_LOG; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Patient Information +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1586-1605 +/// @{ + +#define cbMAX_PATIENTSTRING 128 ///< Maximum patient string length + +#define cbPKTTYPE_REPPATIENTINFO 0x64 +#define cbPKTTYPE_SETPATIENTINFO 0xE4 +#define cbPKTDLEN_PATIENTINFO ((sizeof(cbPKT_PATIENTINFO)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xE4 Rep:0x64 - Patient information packet. +/// +/// This can be used to externally set the patient information of a file. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + char ID[cbMAX_PATIENTSTRING]; ///< Patient identification + char firstname[cbMAX_PATIENTSTRING]; ///< Patient first name + char lastname[cbMAX_PATIENTSTRING]; ///< Patient last name + uint32_t DOBMonth; ///< Patient birth month + uint32_t DOBDay; ///< Patient birth day + uint32_t DOBYear; ///< Patient birth year +} cbPKT_PATIENTINFO; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Impedance Packets (Deprecated) +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1607-1659 +/// @{ + +#define cbPKTTYPE_REPIMPEDANCE 0x65 +#define cbPKTTYPE_SETIMPEDANCE 0xE5 +#define cbPKTDLEN_IMPEDANCE ((sizeof(cbPKT_IMPEDANCE)/4) - cbPKT_HEADER_32SIZE) + +/// @brief *Deprecated* Send impedance data +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + float data[(cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE) / sizeof(float)]; ///< variable length address list +} cbPKT_IMPEDANCE; + +#define cbPKTTYPE_REPINITIMPEDANCE 0x66 +#define cbPKTTYPE_SETINITIMPEDANCE 0xE6 +#define cbPKTDLEN_INITIMPEDANCE ((sizeof(cbPKT_INITIMPEDANCE)/4) - cbPKT_HEADER_32SIZE) + +/// @brief *Deprecated* Initiate impedance calculations +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t initiate; ///< on set call -> 1 to start autoimpedance + ///< on response -> 1 initiated +} cbPKT_INITIMPEDANCE; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Poll Packet (Deprecated) +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1619-1645 +/// @{ + +// Poll packet command +#define cbPOLL_MODE_NONE 0 ///< no command (parameters) +#define cbPOLL_MODE_APPSTATUS 1 ///< Poll or response to poll about the status of an application + +// Poll packet status flags +#define cbPOLL_FLAG_NONE 0 ///< no flag (parameters) +#define cbPOLL_FLAG_RESPONSE 1 ///< Response to the query + +// Extra information +#define cbPOLL_EXT_NONE 0 ///< No extra information +#define cbPOLL_EXT_EXISTS 1 ///< App exists +#define cbPOLL_EXT_RUNNING 2 ///< App is running + +#define cbPKTTYPE_REPPOLL 0x67 +#define cbPKTTYPE_SETPOLL 0xE7 +#define cbPKTDLEN_POLL ((sizeof(cbPKT_POLL)/4) - cbPKT_HEADER_32SIZE) + +/// @brief *Deprecated* Poll for packet mechanism +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t mode; ///< any of cbPOLL_MODE_* commands + uint32_t flags; ///< any of the cbPOLL_FLAG_* status + uint32_t extra; ///< Extra parameters depending on flags and mode + char appname[32]; ///< name of program to apply command specified by mode + char username[256]; ///< return your computername + uint32_t res[32]; ///< reserved for the future +} cbPKT_POLL; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Map File Configuration +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1661-1673 +/// @{ + +#define cbPKTTYPE_REPMAPFILE 0x68 +#define cbPKTTYPE_SETMAPFILE 0xE8 +#define cbPKTDLEN_MAPFILE ((sizeof(cbPKT_MAPFILE)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xE8 Rep:0x68 - Map file +/// +/// Sets the mapfile for applications that use a mapfile so they all display similarly. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + char filename[512]; ///< filename of the mapfile to use +} cbPKT_MAPFILE; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Spike Sorting Packets +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1690-1849 +/// @{ + +// Spike sorting algorithm identifiers +#define cbAUTOALG_NONE 0 ///< No sorting +#define cbAUTOALG_SPREAD 1 ///< Auto spread +#define cbAUTOALG_HIST_CORR_MAJ 2 ///< Auto Hist Correlation +#define cbAUTOALG_HIST_PEAK_COUNT_MAJ 3 ///< Auto Hist Peak Maj +#define cbAUTOALG_HIST_PEAK_COUNT_FISH 4 ///< Auto Hist Peak Fish +#define cbAUTOALG_PCA 5 ///< Manual PCA +#define cbAUTOALG_HOOPS 6 ///< Manual Hoops +#define cbAUTOALG_PCA_KMEANS 7 ///< K-means PCA +#define cbAUTOALG_PCA_EM 8 ///< EM-clustering PCA +#define cbAUTOALG_PCA_DBSCAN 9 ///< DBSCAN PCA + +// Spike sorting mode commands +#define cbAUTOALG_MODE_SETTING 0 ///< Change the settings and leave sorting the same (PC->NSP request) +#define cbAUTOALG_MODE_APPLY 1 ///< Change settings and apply this sorting to all channels (PC->NSP request) + +// SS Model All constants +#define cbPKTTYPE_SS_MODELALLREP 0x50 ///< NSP->PC response +#define cbPKTTYPE_SS_MODELALLSET 0xD0 ///< PC->NSP request +#define cbPKTDLEN_SS_MODELALLSET ((sizeof(cbPKT_SS_MODELALLSET) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD0 Rep:0x50 - Get the spike sorting model for all channels (Histogram Peak Count) +/// +/// This packet says, "Give me all of the model". In response, you will get a series of cbPKTTYPE_MODELREP +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header +} cbPKT_SS_MODELALLSET; + +// SS Model constants +#define cbPKTTYPE_SS_MODELREP 0x51 ///< NSP->PC response +#define cbPKTTYPE_SS_MODELSET 0xD1 ///< PC->NSP request +#define cbPKTDLEN_SS_MODELSET ((sizeof(cbPKT_SS_MODELSET) / 4) - cbPKT_HEADER_32SIZE) +#define MAX_REPEL_POINTS 3 + +/// @brief PKT Set:0xD1 Rep:0x51 - Get the spike sorting model for a single channel (Histogram Peak Count) +/// +/// The system replys with the model of a specific channel. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t chan; ///< actual channel id of the channel being configured (0 based) + uint32_t unit_number; ///< unit label (0 based, 0 is noise cluster) + uint32_t valid; ///< 1 = valid unit, 0 = not a unit, in other words just deleted when NSP -> PC + uint32_t inverted; ///< 0 = not inverted, 1 = inverted + + // Block statistics (change from block to block) + int32_t num_samples; ///< non-zero value means that the block stats are valid + float mu_x[2]; + float Sigma_x[2][2]; + float determinant_Sigma_x; + ///// Only needed if we are using a Bayesian classification model + float Sigma_x_inv[2][2]; + float log_determinant_Sigma_x; + ///// + float subcluster_spread_factor_numerator; + float subcluster_spread_factor_denominator; + float mu_e; + float sigma_e_squared; +} cbPKT_SS_MODELSET; + +// SS Detect constants +#define cbPKTTYPE_SS_DETECTREP 0x52 ///< NSP->PC response +#define cbPKTTYPE_SS_DETECTSET 0xD2 ///< PC->NSP request +#define cbPKTDLEN_SS_DETECT ((sizeof(cbPKT_SS_DETECT) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD2 Rep:0x52 - Auto threshold parameters +/// +/// Set the auto threshold parameters +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + float fThreshold; ///< current detection threshold + float fMultiplier; ///< multiplier +} cbPKT_SS_DETECT; + +// SS Artifact Reject constants +#define cbPKTTYPE_SS_ARTIF_REJECTREP 0x53 ///< NSP->PC response +#define cbPKTTYPE_SS_ARTIF_REJECTSET 0xD3 ///< PC->NSP request +#define cbPKTDLEN_SS_ARTIF_REJECT ((sizeof(cbPKT_SS_ARTIF_REJECT) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD3 Rep:0x53 - Artifact reject +/// +/// Sets the artifact rejection parameters. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t nMaxSimulChans; ///< how many channels can fire exactly at the same time??? + uint32_t nRefractoryCount; ///< for how many samples (30 kHz) is a neuron refractory, so can't re-trigger +} cbPKT_SS_ARTIF_REJECT; + +// SS Noise Boundary constants +#define cbPKTTYPE_SS_NOISE_BOUNDARYREP 0x54 ///< NSP->PC response +#define cbPKTTYPE_SS_NOISE_BOUNDARYSET 0xD4 ///< PC->NSP request +#define cbPKTDLEN_SS_NOISE_BOUNDARY ((sizeof(cbPKT_SS_NOISE_BOUNDARY) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD4 Rep:0x54 - Noise boundary +/// +/// Sets the noise boundary parameters +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t chan; ///< which channel we belong to + float afc[3]; ///< the center of the ellipsoid + float afS[3][3]; ///< an array of the axes for the ellipsoid +} cbPKT_SS_NOISE_BOUNDARY; + +// SS Statistics constants +#define cbPKTTYPE_SS_STATISTICSREP 0x55 ///< NSP->PC response +#define cbPKTTYPE_SS_STATISTICSSET 0xD5 ///< PC->NSP request +#define cbPKTDLEN_SS_STATISTICS ((sizeof(cbPKT_SS_STATISTICS) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD5 Rep:0x55 - Spike sourting statistics (Histogram peak count) +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t nUpdateSpikes; ///< update rate in spike counts + + uint32_t nAutoalg; ///< sorting algorithm (0=none 1=spread, 2=hist_corr_maj, 3=hist_peak_count_maj, 4=hist_peak_count_maj_fisher, 5=pca, 6=hoops) + uint32_t nMode; ///< cbAUTOALG_MODE_SETTING, + + float fMinClusterPairSpreadFactor; ///< larger number = more apt to combine 2 clusters into 1 + float fMaxSubclusterSpreadFactor; ///< larger number = less apt to split because of 2 clusers + + float fMinClusterHistCorrMajMeasure; ///< larger number = more apt to split 1 cluster into 2 + float fMaxClusterPairHistCorrMajMeasure; ///< larger number = less apt to combine 2 clusters into 1 + + float fClusterHistValleyPercentage; ///< larger number = less apt to split nearby clusters + float fClusterHistClosePeakPercentage; ///< larger number = less apt to split nearby clusters + float fClusterHistMinPeakPercentage; ///< larger number = less apt to split separated clusters + + uint32_t nWaveBasisSize; ///< number of wave to collect to calculate the basis, + ///< must be greater than spike length + uint32_t nWaveSampleSize; ///< number of samples sorted with the same basis before re-calculating the basis + ///< 0=manual re-calculation + ///< nWaveBasisSize * nWaveSampleSize is the number of waves/spikes to run against + ///< the same PCA basis before next +} cbPKT_SS_STATISTICS; + +// SS Status constants +#define cbPKTTYPE_SS_STATUSREP 0x57 ///< NSP->PC response +#define cbPKTTYPE_SS_STATUSSET 0xD7 ///< PC->NSP request +#define cbPKTDLEN_SS_STATUS ((sizeof(cbPKT_SS_STATUS) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD7 Rep:0x57 - Spike sorting status (Histogram peak count) +/// +/// This packet contains the status of the automatic spike sorting. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + cbAdaptControl cntlUnitStats; ///< + cbAdaptControl cntlNumUnits; ///< +} cbPKT_SS_STATUS; + +// SS Reset constants +#define cbPKTTYPE_SS_RESETREP 0x56 ///< NSP->PC response +#define cbPKTTYPE_SS_RESETSET 0xD6 ///< PC->NSP request +#define cbPKTDLEN_SS_RESET ((sizeof(cbPKT_SS_RESET) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD6 Rep:0x56 - Spike sorting reset +/// +/// Send this packet to the NSP to tell it to reset all spike sorting to default values +typedef struct +{ + cbPKT_HEADER cbpkt_header; ///< packet header +} cbPKT_SS_RESET; + +// SS Reset Model constants +#define cbPKTTYPE_SS_RESET_MODEL_REP 0x58 ///< NSP->PC response +#define cbPKTTYPE_SS_RESET_MODEL_SET 0xD8 ///< PC->NSP request +#define cbPKTDLEN_SS_RESET_MODEL ((sizeof(cbPKT_SS_RESET_MODEL) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD8 Rep:0x58 - Spike sorting reset model +/// +/// Send this packet to the NSP to tell it to reset all spike sorting models +typedef struct +{ + cbPKT_HEADER cbpkt_header; ///< packet header +} cbPKT_SS_RESET_MODEL; + +// Feature space commands and status changes +#define cbPCA_RECALC_START 0 ///< PC ->NSP start recalculation +#define cbPCA_RECALC_STOPPED 1 ///< NSP->PC finished recalculation +#define cbPCA_COLLECTION_STARTED 2 ///< NSP->PC waveform collection started +#define cbBASIS_CHANGE 3 ///< Change the basis of feature space +#define cbUNDO_BASIS_CHANGE 4 +#define cbREDO_BASIS_CHANGE 5 +#define cbINVALIDATE_BASIS 6 + +// SS Recalc constants +#define cbPKTTYPE_SS_RECALCREP 0x59 ///< NSP->PC response +#define cbPKTTYPE_SS_RECALCSET 0xD9 ///< PC->NSP request +#define cbPKTDLEN_SS_RECALC ((sizeof(cbPKT_SS_RECALC) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xD9 Rep:0x59 - Spike Sorting recalculate PCA +/// +/// Send this packet to the NSP to tell it to re calculate all PCA Basis Vectors and Values +typedef struct +{ + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t chan; ///< 1 based channel we want to recalc (0 = All channels) + uint32_t mode; ///< cbPCA_RECALC_START -> Start PCA basis, cbPCA_RECALC_STOPPED-> PCA basis stopped, cbPCA_COLLECTION_STARTED -> PCA waveform collection started +} cbPKT_SS_RECALC; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Feature Space Basis +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1889-1909 +/// @{ + +#define cbPKTTYPE_FS_BASISREP 0x5B ///< NSP->PC response +#define cbPKTTYPE_FS_BASISSET 0xDB ///< PC->NSP request +#define cbPKTDLEN_FS_BASIS ((sizeof(cbPKT_FS_BASIS) / 4) - cbPKT_HEADER_32SIZE) +#define cbPKTDLEN_FS_BASISSHORT (cbPKTDLEN_FS_BASIS - ((sizeof(float)* cbMAX_PNTS * 3)/4)) + +/// @brief PKT Set:0xDB Rep:0x5B - Feature Space Basis +/// +/// This packet holds the calculated basis of the feature space from NSP to Central +/// Or it has the previous basis retrieved and transmitted by central to NSP +typedef struct +{ + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t chan; ///< 1-based channel number + uint32_t mode; ///< cbBASIS_CHANGE, cbUNDO_BASIS_CHANGE, cbREDO_BASIS_CHANGE, cbINVALIDATE_BASIS ... + uint32_t fs; ///< Feature space: cbAUTOALG_PCA + /// basis must be the last item in the structure because it can be variable length to a max of cbMAX_PNTS + float basis[cbMAX_PNTS][3]; ///< Room for all possible points collected +} cbPKT_FS_BASIS; + +/// @} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Video and Tracking (NeuroMotive) +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 493-511 +/// @{ + +/// @brief NeuroMotive video source +typedef struct { + char name[cbLEN_STR_LABEL]; ///< filename of the video file + float fps; ///< nominal record fps +} cbVIDEOSOURCE; + +// Track object types +#define cbTRACKOBJ_TYPE_UNDEFINED 0 ///< Undefined track object type +#define cbTRACKOBJ_TYPE_2DMARKERS 1 ///< 2D marker tracking +#define cbTRACKOBJ_TYPE_2DBLOB 2 ///< 2D blob tracking +#define cbTRACKOBJ_TYPE_3DMARKERS 3 ///< 3D marker tracking +#define cbTRACKOBJ_TYPE_2DBOUNDARY 4 ///< 2D boundary tracking +#define cbTRACKOBJ_TYPE_1DSIZE 5 ///< 1D size tracking + +/// @brief Track object structure for NeuroMotive +typedef struct { + char name[cbLEN_STR_LABEL]; ///< name of the object + uint16_t type; ///< trackable type (cbTRACKOBJ_TYPE_*) + uint16_t pointCount; ///< maximum number of points +} cbTRACKOBJ; + +// NeuroMotive status +#define cbNM_STATUS_IDLE 0 ///< NeuroMotive is idle +#define cbNM_STATUS_EXIT 1 ///< NeuroMotive is exiting +#define cbNM_STATUS_REC 2 ///< NeuroMotive is recording +#define cbNM_STATUS_PLAY 3 ///< NeuroMotive is playing video file +#define cbNM_STATUS_CAP 4 ///< NeuroMotive is capturing from camera +#define cbNM_STATUS_STOP 5 ///< NeuroMotive is stopping +#define cbNM_STATUS_PAUSED 6 ///< NeuroMotive is paused +#define cbNM_STATUS_COUNT 7 ///< This is the count of status options + +// NeuroMotive commands and status changes (cbPKT_NM.mode) +#define cbNM_MODE_NONE 0 ///< No command +#define cbNM_MODE_CONFIG 1 ///< Ask NeuroMotive for configuration +#define cbNM_MODE_SETVIDEOSOURCE 2 ///< Configure video source +#define cbNM_MODE_SETTRACKABLE 3 ///< Configure trackable +#define cbNM_MODE_STATUS 4 ///< NeuroMotive status reporting (cbNM_STATUS_*) +#define cbNM_MODE_TSCOUNT 5 ///< Timestamp count (value is the period with 0 to disable this mode) +#define cbNM_MODE_SYNCHCLOCK 6 ///< Start (or stop) synchronization clock (fps*1000 specified by value, zero fps to stop capture) +#define cbNM_MODE_ASYNCHCLOCK 7 ///< Asynchronous clock + +#define cbNM_FLAG_NONE 0 ///< No flags + +#define cbPKTTYPE_NMREP 0x32 ///< NSP->PC response +#define cbPKTTYPE_NMSET 0xB2 ///< PC->NSP request +#define cbPKTDLEN_NM ((sizeof(cbPKT_NM)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xB2 Rep:0x32 - NeuroMotive packet structure +/// +/// Used for video tracking and NeuroMotive configuration +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t mode; ///< cbNM_MODE_* command to NeuroMotive or Central + uint32_t flags; ///< cbNM_FLAG_* status of NeuroMotive + uint32_t value; ///< value assigned to this mode + union { + uint32_t opt[cbLEN_STR_LABEL / 4]; ///< Additional options for this mode + char name[cbLEN_STR_LABEL]; ///< name associated with this mode + }; +} cbPKT_NM; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name N-Trode Information +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1357-1375 +/// @{ + +// N-Trode feature space modes +enum cbNTRODEINFO_FS_MODE { + cbNTRODEINFO_FS_PEAK, ///< Feature space based on peak + cbNTRODEINFO_FS_VALLEY, ///< Feature space based on valley + cbNTRODEINFO_FS_AMPLITUDE, ///< Feature space based on amplitude + cbNTRODEINFO_FS_COUNT ///< Number of feature space modes +}; + +#define cbPKTTYPE_REPNTRODEINFO 0x27 ///< NSP->PC response +#define cbPKTTYPE_SETNTRODEINFO 0xA7 ///< PC->NSP request +#define cbPKTDLEN_NTRODEINFO ((sizeof(cbPKT_NTRODEINFO) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xA7 Rep:0x27 - N-Trode information packets +/// +/// Sets information about an N-Trode. The user can change the name, number of sites, sites (channels) +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t ntrode; ///< ntrode with which we are working (1-based) + char label[cbLEN_STR_LABEL]; ///< Label of the Ntrode (null terminated if < 16 characters) + cbMANUALUNITMAPPING ellipses[cbMAXSITEPLOTS][cbMAXUNITS]; ///< unit mapping + uint16_t nSite; ///< number channels in this NTrode ( 0 <= nSite <= cbMAXSITES) + uint16_t fs; ///< NTrode feature space cbNTRODEINFO_FS_* + uint16_t nChan[cbMAXSITES]; ///< group of channels in this NTrode +} cbPKT_NTRODEINFO; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Analog Output Waveform +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1943-2008 +/// @{ + +#define cbMAX_WAVEFORM_PHASES ((cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE - 24) / 4) ///< Maximum number of phases in a waveform + +/// @brief Analog output waveform data +/// +/// Contains the parameters to define a waveform for Analog Output channels +typedef struct +{ + int16_t offset; ///< DC offset + union { + struct { + uint16_t sineFrequency; ///< sine wave Hz + int16_t sineAmplitude; ///< sine amplitude + }; + struct { + uint16_t seq; ///< Wave sequence number (for file playback) + uint16_t seqTotal; ///< total number of sequences + uint16_t phases; ///< Number of valid phases in this wave (maximum is cbMAX_WAVEFORM_PHASES) + uint16_t duration[cbMAX_WAVEFORM_PHASES]; ///< array of durations for each phase + int16_t amplitude[cbMAX_WAVEFORM_PHASES]; ///< array of amplitude for each phase + }; + }; +} cbWaveformData; + +// Signal generator waveform type +#define cbWAVEFORM_MODE_NONE 0 ///< waveform is disabled +#define cbWAVEFORM_MODE_PARAMETERS 1 ///< waveform is a repeated sequence +#define cbWAVEFORM_MODE_SINE 2 ///< waveform is a sinusoids + +// Signal generator waveform trigger type +#define cbWAVEFORM_TRIGGER_NONE 0 ///< instant software trigger +#define cbWAVEFORM_TRIGGER_DINPREG 1 ///< digital input rising edge trigger +#define cbWAVEFORM_TRIGGER_DINPFEG 2 ///< digital input falling edge trigger +#define cbWAVEFORM_TRIGGER_SPIKEUNIT 3 ///< spike unit +#define cbWAVEFORM_TRIGGER_COMMENTCOLOR 4 ///< comment RGBA color (A being big byte) +#define cbWAVEFORM_TRIGGER_RECORDINGSTART 5 ///< recording start trigger +#define cbWAVEFORM_TRIGGER_EXTENSION 6 ///< extension trigger + +// AOUT signal generator waveform data +#define cbPKTTYPE_WAVEFORMREP 0x33 ///< NSP->PC response +#define cbPKTTYPE_WAVEFORMSET 0xB3 ///< PC->NSP request +#define cbPKTDLEN_WAVEFORM ((sizeof(cbPKT_AOUT_WAVEFORM)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xB3 Rep:0x33 - AOUT waveform +/// +/// This sets a user defined waveform for one or multiple Analog & Audio Output channels. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint16_t chan; ///< which analog output/audio output channel (1-based, will equal chan from GetDoutCaps) + + /// Each file may contain multiple sequences. + /// Each sequence consists of phases + /// Each phase is defined by amplitude and duration + + /// Waveform parameter information + uint16_t mode; ///< Can be any of cbWAVEFORM_MODE_* + uint32_t repeats; ///< Number of repeats (0 means forever) + uint8_t trig; ///< Can be any of cbWAVEFORM_TRIGGER_* + uint8_t trigInst; ///< Instrument the trigChan belongs + uint16_t trigChan; ///< Depends on trig: + /// for cbWAVEFORM_TRIGGER_DINP* 1-based trigChan (1-16) is digin1, (17-32) is digin2, ... + /// for cbWAVEFORM_TRIGGER_SPIKEUNIT 1-based trigChan (1-156) is channel number + /// for cbWAVEFORM_TRIGGER_COMMENTCOLOR trigChan is A->B in A->B->G->R + uint16_t trigValue; ///< Trigger value (spike unit, G-R comment color, ...) + uint8_t trigNum; ///< trigger number (0-based) (can be up to cbMAX_AOUT_TRIGGER-1) + uint8_t active; ///< status of trigger + cbWaveformData wave; ///< Actual waveform data +} cbPKT_AOUT_WAVEFORM; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Stimulation +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 2010-2022 +/// @{ + +#define cbPKTTYPE_STIMULATIONREP 0x34 ///< NSP->PC response +#define cbPKTTYPE_STIMULATIONSET 0xB4 ///< PC->NSP request +#define cbPKTDLEN_STIMULATION ((sizeof(cbPKT_STIMULATION)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xB4 Rep:0x34 - Stimulation command +/// +/// This sets a user defined stimulation for stim/record headstages +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint8_t commandBytes[40]; ///< series of bytes to control stimulation +} cbPKT_STIMULATION; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name nPlay Configuration +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 916-965 +/// @{ + +// Audio commands "val" +#define cbAUDIO_CMD_NONE 0 ///< PC->NPLAY query audio status + +// nPlay file version (first byte NSx version, second byte NEV version) +#define cbNPLAY_FILE_NS21 1 ///< NSX 2.1 file +#define cbNPLAY_FILE_NS22 2 ///< NSX 2.2 file +#define cbNPLAY_FILE_NS30 3 ///< NSX 3.0 file +#define cbNPLAY_FILE_NEV21 (1 << 8) ///< Nev 2.1 file +#define cbNPLAY_FILE_NEV22 (2 << 8) ///< Nev 2.2 file +#define cbNPLAY_FILE_NEV23 (3 << 8) ///< Nev 2.3 file +#define cbNPLAY_FILE_NEV30 (4 << 8) ///< Nev 3.0 file + +// nPlay commands and status changes (cbPKT_NPLAY.mode) +#define cbNPLAY_FNAME_LEN (cbPKT_MAX_SIZE - cbPKT_HEADER_SIZE - 40) ///< length of the file name (with terminating null) +#define cbNPLAY_MODE_NONE 0 ///< no command (parameters) +#define cbNPLAY_MODE_PAUSE 1 ///< PC->NPLAY pause if "val" is non-zero, un-pause otherwise +#define cbNPLAY_MODE_SEEK 2 ///< PC->NPLAY seek to time "val" +#define cbNPLAY_MODE_CONFIG 3 ///< PC<->NPLAY request full config +#define cbNPLAY_MODE_OPEN 4 ///< PC->NPLAY open new file in "val" for playback +#define cbNPLAY_MODE_PATH 5 ///< PC->NPLAY use the directory path in fname +#define cbNPLAY_MODE_CONFIGMAIN 6 ///< PC<->NPLAY request main config packet +#define cbNPLAY_MODE_STEP 7 ///< PC<->NPLAY run "val" procTime steps and pause, then send cbNPLAY_FLAG_STEPPED +#define cbNPLAY_MODE_SINGLE 8 ///< PC->NPLAY single mode if "val" is non-zero, wrap otherwise +#define cbNPLAY_MODE_RESET 9 ///< PC->NPLAY reset nPlay +#define cbNPLAY_MODE_NEVRESORT 10 ///< PC->NPLAY resort NEV if "val" is non-zero, do not if otherwise +#define cbNPLAY_MODE_AUDIO_CMD 11 ///< PC->NPLAY perform audio command in "val" (cbAUDIO_CMD_*), with option "opt" + +#define cbNPLAY_FLAG_NONE 0x00 ///< no flag +#define cbNPLAY_FLAG_CONF 0x01 ///< NPLAY->PC config packet ("val" is "fname" file index) +#define cbNPLAY_FLAG_MAIN (0x02 | cbNPLAY_FLAG_CONF) ///< NPLAY->PC main config packet ("val" is file version) +#define cbNPLAY_FLAG_DONE 0x02 ///< NPLAY->PC step command done + +// nPlay configuration packet(sent on restart together with config packet) +#define cbPKTTYPE_NPLAYREP 0x5C ///< NPLAY->PC response +#define cbPKTTYPE_NPLAYSET 0xDC ///< PC->NPLAY request +#define cbPKTDLEN_NPLAY ((sizeof(cbPKT_NPLAY)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xDC Rep:0x5C - nPlay configuration packet +/// +/// Sent on restart together with config packet +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + union { + PROCTIME ftime; ///< the total time of the file. + PROCTIME opt; ///< optional value + }; + PROCTIME stime; ///< start time + PROCTIME etime; ///< stime < end time < ftime + PROCTIME val; ///< Used for current time to traverse, file index, file version, ... + uint16_t mode; ///< cbNPLAY_MODE_* command to nPlay + uint16_t flags; ///< cbNPLAY_FLAG_* status of nPlay + float speed; ///< positive means fast forward, negative means rewind, 0 means go as fast as you can. + char fname[cbNPLAY_FNAME_LEN]; ///< This is a String with the file name. +} cbPKT_NPLAY; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Set Digital Output +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 1928-1941 +/// @{ + +#define cbPKTTYPE_SET_DOUTREP 0x5D ///< NSP->PC response +#define cbPKTTYPE_SET_DOUTSET 0xDD ///< PC->NSP request +#define cbPKTDLEN_SET_DOUT ((sizeof(cbPKT_SET_DOUT) / 4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xDD Rep:0x5D - Set Digital Output +/// +/// Allows setting the digital output value if not assigned set to monitor a channel or timed waveform or triggered +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint16_t chan; ///< which digital output channel (1 based, will equal chan from GetDoutCaps) + uint16_t value; ///< Which value to set? zero = 0; non-zero = 1 (output is 1 bit) +} cbPKT_SET_DOUT; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Trigger and Video Tracking +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 967-1005 +/// @{ + +#define cbTRIGGER_MODE_UNDEFINED 0 +#define cbTRIGGER_MODE_BUTTONPRESS 1 ///< Patient button press event +#define cbTRIGGER_MODE_EVENTRESET 2 ///< event reset + +#define cbPKTTYPE_TRIGGERREP 0x5E ///< NPLAY->PC response +#define cbPKTTYPE_TRIGGERSET 0xDE ///< PC->NPLAY request +#define cbPKTDLEN_TRIGGER ((sizeof(cbPKT_TRIGGER)/4) - cbPKT_HEADER_32SIZE) + +/// @brief PKT Set:0xDE Rep:0x5E - Trigger Packet used for Cervello system +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint32_t mode; ///< cbTRIGGER_MODE_* +} cbPKT_TRIGGER; + +#define cbMAX_TRACKCOORDS (128) ///< Maximum number of coordinates (must be an even number) +#define cbPKTTYPE_VIDEOTRACKREP 0x5F ///< NPLAY->PC response +#define cbPKTTYPE_VIDEOTRACKSET 0xDF ///< PC->NPLAY request +#define cbPKTDLEN_VIDEOTRACK ((sizeof(cbPKT_VIDEOTRACK)/4) - cbPKT_HEADER_32SIZE) +#define cbPKTDLEN_VIDEOTRACKSHORT (cbPKTDLEN_VIDEOTRACK - ((sizeof(uint16_t)*cbMAX_TRACKCOORDS)/4)) + +/// @brief PKT Set:0xDF Rep:0x5F - Video tracking event packet +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + uint16_t parentID; ///< parent ID + uint16_t nodeID; ///< node ID (cross-referenced in the TrackObj header) + uint16_t nodeCount; ///< Children count + uint16_t pointCount; ///< number of points at this node + ///< this must be the last item in the structure because it can be variable length to a max of cbMAX_TRACKCOORDS + union { + uint16_t coords[cbMAX_TRACKCOORDS]; + uint32_t sizes[cbMAX_TRACKCOORDS / 2]; + }; +} cbPKT_VIDEOTRACK; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Configuration Tables (Central/cbhwlib only) +/// +/// Ground truth from upstream/cbhwlib/cbhwlib.h lines 906-931 +/// These are used in shared memory structures for Central application +/// @{ + +/// @brief Color table for Central application +/// +/// Used for display configuration in Central +typedef struct { + uint32_t winrsvd[48]; ///< Reserved for Windows + uint32_t dispback; ///< Display background color + uint32_t dispgridmaj; ///< Display major grid color + uint32_t dispgridmin; ///< Display minor grid color + uint32_t disptext; ///< Display text color + uint32_t dispwave; ///< Display waveform color + uint32_t dispwavewarn; ///< Display waveform warning color + uint32_t dispwaveclip; ///< Display waveform clipping color + uint32_t dispthresh; ///< Display threshold color + uint32_t dispmultunit; ///< Display multi-unit color + uint32_t dispunit[16]; ///< Display unit colors (0 = unclassified) + uint32_t dispnoise; ///< Display noise color + uint32_t dispchansel[3]; ///< Display channel selection colors + uint32_t disptemp[5]; ///< Display temporary colors + uint32_t disprsvd[14]; ///< Reserved display colors +} cbCOLORTABLE; + +/// @brief Option table for Central application +/// +/// Used for configuration options in Central +typedef struct { + float fRMSAutoThresholdDistance; ///< multiplier to use for autothresholding when using RMS to guess noise + uint32_t reserved[31]; ///< Reserved for future use +} cbOPTIONTABLE; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Firmware Update Packets +/// +/// Ground truth from upstream/cbproto/cbproto.h lines 800-838 +/// @{ + +#define cbRUNLEVEL_UPDATE 78 +#define cbPKTTYPE_UPDATESET 0xF1 +#define cbPKTTYPE_UPDATEREP 0x71 +#define cbPKTDLEN_UPDATE (sizeof(cbPKT_UPDATE)/4)-2 + +/// @brief PKT Set:0xF1 Rep:0x71 - Update Packet +/// +/// Update the firmware of the NSP. This will copy data received into files in a temporary location and if +/// completed, on reboot will copy the files to the proper location to run. +typedef struct { + cbPKT_HEADER cbpkt_header; ///< packet header + + char filename[64]; ///< filename to be updated + uint32_t blockseq; ///< sequence of the current block + uint32_t blockend; ///< last block of the current file + uint32_t blocksiz; ///< block size of the current block + uint8_t block[512]; ///< block data +} cbPKT_UPDATE; + +#define cbPKTDLEN_UPDATE_OLD (sizeof(cbPKT_UPDATE_OLD)/4)-2 + +/// @brief PKT Set:0xF1 Rep:0x71 - Old Update Packet +/// +/// Update the firmware of the NSP. This will copy data received into files in a temporary location and if +/// completed, on reboot will copy the files to the proper location to run. +/// +/// Since the NSP needs to work with old versions of the firmware, this packet retains the old header format. +typedef struct { + uint32_t time; ///< system clock timestamp + uint16_t chan; ///< channel identifier + uint8_t type; ///< packet type + uint8_t dlen; ///< length of data field in 32-bit chunks + char filename[64]; ///< filename to be updated + uint32_t blockseq; ///< sequence of the current block + uint32_t blockend; ///< last block of the current file + uint32_t blocksiz; ///< block size of the current block + uint8_t block[512]; ///< block data +} cbPKT_UPDATE_OLD; + +/// @} + +#ifdef __cplusplus +} +#endif + +#pragma pack(pop) + +#endif // CBPROTO_TYPES_H diff --git a/src/cbproto/src/packet_translator.cpp b/src/cbproto/src/packet_translator.cpp new file mode 100644 index 00000000..b71ca7af --- /dev/null +++ b/src/cbproto/src/packet_translator.cpp @@ -0,0 +1,191 @@ +// +// Created by Chadwick Boulay on 2025-11-17. +// + +#include + +size_t cbproto::PacketTranslator::translate_DINP_pre400_to_current(const uint8_t* src_payload, cbPKT_DINP* dest) { + // 3.11 -> 4.0: Eliminated data array and added new fields: + // uint32_t valueRead; + // uint32_t bitsChanged; + // uint32_t eventType; + // memcpy the 3 quadlets + memcpy(&dest->valueRead, src_payload, 12); + dest->cbpkt_header.dlen = 3; + return dest->cbpkt_header.dlen; +} + +size_t cbproto::PacketTranslator::translate_DINP_current_to_pre400(const cbPKT_DINP &src, uint8_t* dest_payload) { + // memcpy the 3 quadlets + memcpy(dest_payload, &src.valueRead, 12); + return 3; +} + +size_t cbproto::PacketTranslator::translate_NPLAY_pre400_to_current(const uint8_t* src_payload, cbPKT_NPLAY* dest) { + // ftime, stime, etime, val fields all changed from uint32_t to PROCTIME (uint64_t). + dest->ftime = static_cast(*reinterpret_cast(src_payload)) * 1000000000/30000; + dest->stime = static_cast(*reinterpret_cast(src_payload + 4)) * 1000000000/30000; + dest->etime = static_cast(*reinterpret_cast(src_payload + 8)) * 1000000000/30000; + dest->val = static_cast(*reinterpret_cast(src_payload + 12)) * 1000000000/30000; + // memcpy the remaining dlen - 4 quadlets from src to dest. + memcpy( + &(dest->mode), + src_payload + 4 + 4 + 4 + 4, + dest->cbpkt_header.dlen * 4 - 4 - 4 - 4 - 4 + // dest dlen is the same as src dlen at this point. + ); + // Add 1 quadlet per timestamp. + dest->cbpkt_header.dlen += 4; + return dest->cbpkt_header.dlen; +} + +size_t cbproto::PacketTranslator::translate_NPLAY_current_to_pre400(const cbPKT_NPLAY &src, uint8_t* dest_payload) { + // ftime, stime, etime, val fields must be narrowed from PROCTIME (uint64_t) to uint32_t. + *reinterpret_cast(dest_payload) = static_cast(src.ftime * 30000 / 1000000000); + *reinterpret_cast(dest_payload + 4) = static_cast(src.stime * 30000 / 1000000000); + *reinterpret_cast(dest_payload + 8) = static_cast(src.etime * 30000 / 1000000000); + *reinterpret_cast(dest_payload + 12) = static_cast(src.val * 30000 / 1000000000); + // Copy the rest of the payload + memcpy( + dest_payload + 4 + 4 + 4 + 4, + &(src.mode), + src.cbpkt_header.dlen * 4 - 8 - 8 - 8 - 8 + ); + // dlen decrease: 4 fields shrink from PROCTIME (8 bytes) to uint32_t (4 bytes) = 16 bytes = 4 quadlets + return src.cbpkt_header.dlen - 4; +} + +size_t cbproto::PacketTranslator::translate_COMMENT_pre400_to_current(const uint8_t* src_payload, cbPKT_COMMENT* dest, const uint32_t hdr_timestamp) { + // cbPKT_COMMENT's 2nd field is a `info` struct with fields: + // * In 3.11: `uint8_t type;`, `uint8_t flags`, and `uint8_t reserved[2];` + // * In 4.0: `uint8_t charset;` and `uint8_t reserved[3];` + // --> No change in size. + // Immediately after `info`, we have: + // * In 3.11: `uint32_t data;` -- can be rgba if flags is 0x00, or timeStarted if flags is 0x01 + // * In 4.0: `PROCTIME timeStarted;`, `uint32_t rgba;` + // --> Inserted 8 bytes! + // auto src_info_type = src_payload[0]; + const auto src_info_flags = src_payload[1]; + const auto src_data = *reinterpret_cast(&src_payload[4]); + dest->info.charset = 0; + if (src_info_flags) { + dest->timeStarted = static_cast(src_data) * 1000000000/30000; + } else { + dest->rgba = src_data; + dest->timeStarted = static_cast(hdr_timestamp) * 1000000000/30000; + } + // Finally, the `comment` char array + memcpy( + &(dest->comment), + src_payload + 1 + 1 + 2 + 4, + dest->cbpkt_header.dlen * 4 - 1 - 1 - 2 - 4 + ); + // The new dlen is just the old + 2 quadlets (8 bytes) for timeStarted. + dest->cbpkt_header.dlen += 2; + return dest->cbpkt_header.dlen; +} + +size_t cbproto::PacketTranslator::translate_COMMENT_current_to_pre400(const cbPKT_COMMENT &src, uint8_t* dest_payload) { + // dest_payload[0] = ??; // type -- Is this related to modern charset? + dest_payload[1] = 0x01; // flags -- we always set to timeStarted + // dest.data = timeStarted: + *reinterpret_cast(&dest_payload[4]) = static_cast(src.timeStarted * 30000 / 1000000000); + // Copy char array from src.comment to dest.comment. + memcpy( + dest_payload + 1 + 1 + 2 + 4, + &(src.comment), + src.cbpkt_header.dlen * 4 - 4 - 8 - 4 + ); + // Removal of timeStarted field. + return src.cbpkt_header.dlen - 2; +} + +size_t cbproto::PacketTranslator::translate_SYSPROTOCOLMONITOR_pre410_to_current(const uint8_t* src_payload, cbPKT_SYSPROTOCOLMONITOR* dest) { + dest->sentpkts = *reinterpret_cast(src_payload); + // 4.1 added uint32_t counter field at the end of the payload. + // If we are coming from protocol 3.11, we can use its header.time field because that was device ticks at 30 kHz. + // TODO: This only works for 3.11 -> current. What about 4.0 -> current? + dest->counter = dest->cbpkt_header.time; + dest->cbpkt_header.dlen += 1; + return dest->cbpkt_header.dlen; +} + +size_t cbproto::PacketTranslator::translate_SYSPROTOCOLMONITOR_current_to_pre410(const cbPKT_SYSPROTOCOLMONITOR &src, uint8_t* dest_payload) { + *reinterpret_cast(dest_payload) = src.sentpkts; + // Ignore .counter field and drop it from dlen. + return src.cbpkt_header.dlen - 1; +} + +size_t cbproto::PacketTranslator::translate_CHANINFO_pre410_to_current(const uint8_t* src_payload, cbPKT_CHANINFO* dest) { + // Copy everything up to and including eopchar -- unchanged + constexpr size_t payload_to_union = offsetof(cbPKT_CHANINFO, eopchar) + sizeof(dest->eopchar) - cbPKT_HEADER_SIZE; + std::memcpy(&dest->chan, src_payload, payload_to_union); + size_t src_offset = payload_to_union; + // Narrow 3.11's uint32_t monsource to 4.1's uint16_t moninst, set uint16_t monchan to 0. + dest->moninst = static_cast(*reinterpret_cast(&src_payload[src_offset])); + src_offset += 4; + dest->monchan = 0; // New field; set to 0. + // outvalue and trigtype are unchanged + dest->outvalue = *reinterpret_cast(&src_payload[src_offset]); + src_offset += 4; + dest->trigtype = src_payload[src_offset]; + src_offset += 1; + // New fields: + dest->reserved[0] = 0; + dest->reserved[1] = 0; + dest->triginst = 0; + // memcpy rest. Copy all remaining bytes from source payload. + std::memcpy(&dest->trigchan, + &src_payload[src_offset], + dest->cbpkt_header.dlen * 4 - src_offset); + dest->cbpkt_header.dlen += 1; // Actually 3/4 of a quadlet, rounded up. + return dest->cbpkt_header.dlen; +} + +size_t cbproto::PacketTranslator::translate_CHANINFO_current_to_pre410(const cbPKT_CHANINFO &pkt, uint8_t* dest_payload) { + // Copy everything up to and including eopchar -- unchanged + constexpr size_t payload_to_union = offsetof(cbPKT_CHANINFO, eopchar) + sizeof(pkt.eopchar) - cbPKT_HEADER_SIZE; + memcpy(dest_payload, &pkt.chan, payload_to_union); + size_t dest_offset = payload_to_union; + // Expand 4.1's uint16_t moninst to 3.11's uint32_t monsource; ignore uint16_t monchan. + *reinterpret_cast(&dest_payload[dest_offset]) = static_cast(pkt.moninst); + dest_offset += 4; + // outvalue is unchanged + *reinterpret_cast(&dest_payload[dest_offset]) = pkt.outvalue; + dest_offset += 4; + // trigtype is unchanged + dest_payload[dest_offset] = pkt.trigtype; + dest_offset += 1; + // Skip reserved[0], reserved[1], triginst -- not present in 3.11 + // memcpy rest. Copy all remaining fields from trigchan onwards. + // Size = remaining destination space = (result_dlen * 4) - dest_offset + size_t result_dlen = pkt.cbpkt_header.dlen - 1; + std::memcpy(&dest_payload[dest_offset], &pkt.trigchan, + result_dlen * 4 - dest_offset); + return result_dlen; +} + +size_t cbproto::PacketTranslator::translate_CHANRESET_pre420_to_current(const uint8_t* src_payload, cbPKT_CHANRESET* dest) { + // In 4.2, cbPKT_CHANRESET renamed uint8_t monsource to uint8_t moninst and inserted uint8_t monchan. + // First, we copy everything from outvalue (after monchan) onward. + // Second, we copy everything up to and including monsource. + // Note: We go backwards because our src and dest might be the same memory depending on who called this function. + constexpr size_t payload_to_moninst = offsetof(cbPKT_CHANRESET, moninst) + sizeof(dest->moninst) - cbPKT_HEADER_SIZE; + constexpr size_t outvalue_to_end = sizeof(cbPKT_CHANRESET) - offsetof(cbPKT_CHANRESET, outvalue); + std::memcpy(&dest->outvalue, &src_payload[payload_to_moninst], outvalue_to_end); + std::memcpy(&dest->chan, src_payload, payload_to_moninst); + // Payload from 29 bytes to 30 bytes, or 7.25 to 7.5 quadlets. dlen will stay truncated at 7. + return dest->cbpkt_header.dlen; +} + +size_t cbproto::PacketTranslator::translate_CHANRESET_current_to_pre420(const cbPKT_CHANRESET &pkt, uint8_t* dest_payload) { + // In 4.2, cbPKT_CHANRESET renamed uint8_t monsource to uint8_t moninst and inserted uint8_t monchan. + // First, we copy everything from outvalue (after monchan) onward. + // Second, we copy everything up to and including monsource. + // Note: We go backwards because our src and dest might be the same memory depending on who called this function. + constexpr size_t payload_to_moninst = offsetof(cbPKT_CHANRESET, moninst) + sizeof(pkt.moninst) - cbPKT_HEADER_SIZE; + constexpr size_t outvalue_to_end = sizeof(cbPKT_CHANRESET) - offsetof(cbPKT_CHANRESET, outvalue); + std::memcpy(&dest_payload[payload_to_moninst], &pkt.outvalue, outvalue_to_end); + std::memcpy(dest_payload, &pkt.chan, payload_to_moninst); + return pkt.cbpkt_header.dlen; +} diff --git a/src/cbsdk/CMakeLists.txt b/src/cbsdk/CMakeLists.txt new file mode 100644 index 00000000..4fa88639 --- /dev/null +++ b/src/cbsdk/CMakeLists.txt @@ -0,0 +1,105 @@ +# cbsdk - SDK Public API +# Orchestrates cbdev + cbshm to provide clean public C API + +project(cbsdk + DESCRIPTION "CereLink SDK" + LANGUAGES CXX C +) + +# Library sources +set(CBSDK_SOURCES + src/sdk_session.cpp + src/cbsdk.cpp + src/cmp_parser.cpp +) + +# Build as STATIC library +add_library(cbsdk STATIC ${CBSDK_SOURCES}) + +target_include_directories(cbsdk + BEFORE PUBLIC + $ + $ +) + +# Dependencies +target_link_libraries(cbsdk + PUBLIC + cbproto + cbutil + cbshm + cbdev + PRIVATE + $ +) + +# Inject version from top-level project into cbsdk_get_version() +target_compile_definitions(cbsdk PRIVATE + CBSDK_VERSION_STRING="${CBSDK_VERSION_MAJOR}.${CBSDK_VERSION_MINOR}.${CBSDK_VERSION_PATCH}" +) + +# C++17 for implementation, C compatible API +target_compile_features(cbsdk PUBLIC cxx_std_17) + +# Platform-specific libraries +if(WIN32) + target_link_libraries(cbsdk PRIVATE wsock32 ws2_32) +elseif(APPLE) + target_link_libraries(cbsdk PRIVATE pthread) +else() + target_link_libraries(cbsdk PRIVATE pthread) +endif() + +# Shared library for FFI bindings (Python, C#, Matlab) +option(CBSDK_BUILD_SHARED "Build cbsdk as a shared library" OFF) +if(CBSDK_BUILD_SHARED) + add_library(cbsdk_shared SHARED ${CBSDK_SOURCES}) + + target_include_directories(cbsdk_shared + BEFORE PUBLIC + $ + $ + ) + + target_link_libraries(cbsdk_shared + PRIVATE + cbproto cbutil cbshm cbdev ccfutils + ) + + target_compile_features(cbsdk_shared PUBLIC cxx_std_17) + target_compile_definitions(cbsdk_shared PRIVATE + CBSDK_SHARED CBSDK_EXPORTS + CBSDK_VERSION_STRING="${CBSDK_VERSION_MAJOR}.${CBSDK_VERSION_MINOR}.${CBSDK_VERSION_PATCH}" + ) + set_target_properties(cbsdk_shared PROPERTIES OUTPUT_NAME cbsdk) + + if(WIN32) + target_link_libraries(cbsdk_shared PRIVATE wsock32 ws2_32) + else() + target_link_libraries(cbsdk_shared PRIVATE pthread) + endif() + + # Static-link MinGW runtime to eliminate DLL dependencies (libstdc++, libgcc, libwinpthread) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_link_options(cbsdk_shared PRIVATE -static-libstdc++ -static-libgcc) + endif() + + install(TARGETS cbsdk_shared + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ) +endif() + +# Installation +install(TARGETS cbsdk + EXPORT CBSDKTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +# TODO: Install public headers when they exist +# install( +# DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +# ) diff --git a/src/cbsdk/ContinuousData.cpp b/src/cbsdk/ContinuousData.cpp deleted file mode 100644 index 87037bca..00000000 --- a/src/cbsdk/ContinuousData.cpp +++ /dev/null @@ -1,326 +0,0 @@ -#include "ContinuousData.h" -#include -#include -#include - -GroupContinuousData::GroupContinuousData() : - m_size(0), m_write_index(0), m_write_start_index(0), - m_read_end_index(0), m_num_channels(0), - m_channel_ids(nullptr), m_timestamps(nullptr), m_channel_data(nullptr) -{ -} - -GroupContinuousData::~GroupContinuousData() -{ - cleanup(); -} - -bool GroupContinuousData::needsReallocation(const uint16_t* chan_ids, uint32_t n_chans) const -{ - if (m_channel_data == nullptr) - return true; // First packet for this group - - if (m_num_channels != n_chans) - return true; // Channel count changed - - // Same count, but check if actual channels changed - for (uint32_t i = 0; i < n_chans; ++i) - { - if (m_channel_ids[i] != chan_ids[i]) - return true; - } - - return false; // No change needed -} - -bool GroupContinuousData::allocate(uint32_t buffer_size, uint32_t n_chans, const uint16_t* chan_ids) -{ - if (m_num_channels != n_chans || m_size != buffer_size) - { - // Channel count or buffer size changed - need to reallocate - // Clean up old allocation - if (m_channel_data) - { - delete[] m_channel_data; - m_channel_data = nullptr; - } - if (m_channel_ids) - { - delete[] m_channel_ids; - m_channel_ids = nullptr; - } - if (m_timestamps) - { - delete[] m_timestamps; - m_timestamps = nullptr; - } - - // Allocate new arrays with exact size needed - try { - // Single contiguous allocation: [buffer_size * n_chans] - m_channel_data = new int16_t[buffer_size * n_chans]; - std::fill_n(m_channel_data, buffer_size * n_chans, static_cast(0)); - - m_channel_ids = new uint16_t[n_chans]; - - m_timestamps = new PROCTIME[buffer_size]; - std::fill_n(m_timestamps, buffer_size, static_cast(0)); - - m_num_channels = n_chans; - m_size = buffer_size; - } catch (...) { - // Allocation failed - cleanup partial allocation - if (m_channel_data) - { - delete[] m_channel_data; - m_channel_data = nullptr; - } - if (m_channel_ids) - { - delete[] m_channel_ids; - m_channel_ids = nullptr; - } - if (m_timestamps) - { - delete[] m_timestamps; - m_timestamps = nullptr; - } - m_num_channels = 0; - m_size = 0; - return false; - } - - // Reset indices on reallocation (lose any buffered data) - m_write_index = 0; - m_write_start_index = 0; - } - - // Update channel list - memcpy(m_channel_ids, chan_ids, n_chans * sizeof(uint16_t)); - - return true; -} - -bool GroupContinuousData::writeSample(PROCTIME timestamp, const int16_t* data, uint32_t n_chans) -{ - if (!m_channel_data || n_chans != m_num_channels) - return false; // Not allocated or channel count mismatch - - // Store timestamp for this sample - m_timestamps[m_write_index] = timestamp; - - // Store data for all channels in one memcpy - // Contiguous layout: data for sample at write_index starts at [write_index * num_channels] - memcpy(&m_channel_data[m_write_index * m_num_channels], data, n_chans * sizeof(int16_t)); - - // Advance write index (circular buffer) - const uint32_t next_write_index = (m_write_index + 1) % m_size; - - // Check for buffer overflow - bool overflow = false; - if (next_write_index == m_write_start_index) - { - // Buffer is full - overwrite oldest data - overflow = true; - m_write_start_index = (m_write_start_index + 1) % m_size; - } - - m_write_index = next_write_index; - return overflow; -} - -void GroupContinuousData::reset() -{ - if (m_size && m_timestamps) - std::fill_n(m_timestamps, m_size, static_cast(0)); - - if (m_channel_data && m_size && m_num_channels) - { - // Fill entire contiguous block - std::fill_n(m_channel_data, m_size * m_num_channels, static_cast(0)); - } - - m_write_index = 0; - m_write_start_index = 0; - m_read_end_index = 0; -} - -void GroupContinuousData::cleanup() -{ - if (m_timestamps) - { - delete[] m_timestamps; - m_timestamps = nullptr; - } - - if (m_channel_data) - { - delete[] m_channel_data; - m_channel_data = nullptr; - } - - if (m_channel_ids) - { - delete[] m_channel_ids; - m_channel_ids = nullptr; - } - - m_num_channels = 0; - m_size = 0; -} - -// ContinuousData methods implementation - -bool ContinuousData::writeSampleThreadSafe(uint32_t group_idx, PROCTIME timestamp, - const int16_t* data, uint32_t n_chans, - const uint16_t* chan_ids) -{ - if (group_idx >= cbMAXGROUPS) - return false; - - auto& grp = groups[group_idx]; - std::lock_guard lock(grp.m_mutex); - - // Check if we need to allocate or reallocate - if (grp.needsReallocation(chan_ids, n_chans)) - { - // Use existing size if already allocated, otherwise use default - const uint32_t buffer_size = grp.getSize() ? grp.getSize() : default_size; - if (!grp.allocate(buffer_size, n_chans, chan_ids)) - { - return false; // Allocation failed - } - } - - // Write sample to ring buffer (returns true if overflow occurred) - return grp.writeSample(timestamp, data, n_chans); -} - -bool ContinuousData::snapshotForReading(uint32_t group_idx, GroupSnapshot& snapshot) -{ - if (group_idx >= cbMAXGROUPS) - return false; - - auto& grp = groups[group_idx]; - std::lock_guard lock(grp.m_mutex); - - snapshot.is_allocated = grp.isAllocated(); - if (!snapshot.is_allocated) - { - snapshot.num_samples = 0; - snapshot.num_channels = 0; - snapshot.buffer_size = 0; - snapshot.read_start_index = 0; - snapshot.read_end_index = 0; - return true; // Success, but no data - } - - // Take snapshot of current state - snapshot.read_end_index = grp.getWriteIndex(); - snapshot.read_start_index = grp.getWriteStartIndex(); - snapshot.num_channels = grp.getNumChannels(); - snapshot.buffer_size = grp.getSize(); - - // Calculate available samples - auto num_avail = static_cast(snapshot.read_end_index - snapshot.read_start_index); - if (num_avail < 0) - num_avail += static_cast(snapshot.buffer_size); // Wrapped around - - snapshot.num_samples = static_cast(num_avail); - - // Update the read_end_index in the group (this is the snapshot point) - grp.setReadEndIndex(snapshot.read_end_index); - - return true; -} - -bool ContinuousData::readSamples(uint32_t group_idx, int16_t* output_samples, - PROCTIME* output_timestamps, uint32_t& num_samples, - bool bSeek) -{ - if (group_idx >= cbMAXGROUPS) - return false; - - if (!output_samples || !output_timestamps) - return false; - - auto& grp = groups[group_idx]; - std::lock_guard lock(grp.m_mutex); - - if (!grp.isAllocated()) - { - num_samples = 0; - return true; // Success, but no data - } - - // Get current read pointers - const uint32_t read_start_index = grp.getWriteStartIndex(); - const uint32_t read_end_index = grp.getWriteIndex(); - - // Calculate available samples - auto num_avail = static_cast(read_end_index - read_start_index); - if (num_avail < 0) - num_avail += static_cast(grp.getSize()); // Wrapped around - - // Don't read more than requested or available - num_samples = std::min(static_cast(num_avail), num_samples); - - if (num_samples == 0) - return true; // Success, but no data to read - - // Get pointers to internal data - const int16_t* channel_data = grp.getChannelData(); - const PROCTIME* timestamps = grp.getTimestamps(); - const uint32_t num_channels = grp.getNumChannels(); - const uint32_t buffer_size = grp.getSize(); - - // Check if we wrap around the ring buffer - const bool wraps = (read_start_index + num_samples) > buffer_size; - - if (!wraps) - { - // No wraparound - copy everything in bulk - memcpy(output_timestamps, - ×tamps[read_start_index], - num_samples * sizeof(PROCTIME)); - - memcpy(output_samples, - &channel_data[read_start_index * num_channels], - num_samples * num_channels * sizeof(int16_t)); - } - else - { - // Wraparound case - copy in two chunks - const uint32_t first_chunk_size = buffer_size - read_start_index; - const uint32_t second_chunk_size = num_samples - first_chunk_size; - - // First chunk of timestamps - memcpy(output_timestamps, - ×tamps[read_start_index], - first_chunk_size * sizeof(PROCTIME)); - - // Second chunk of timestamps - memcpy(&output_timestamps[first_chunk_size], - timestamps, - second_chunk_size * sizeof(PROCTIME)); - - // First chunk of sample data - memcpy(output_samples, - &channel_data[read_start_index * num_channels], - first_chunk_size * num_channels * sizeof(int16_t)); - - // Second chunk of sample data - memcpy(&output_samples[first_chunk_size * num_channels], - channel_data, - second_chunk_size * num_channels * sizeof(int16_t)); - } - - // Update write_start_index if consuming data (bSeek) - if (bSeek) - { - const uint32_t new_start = (read_start_index + num_samples) % grp.getSize(); - grp.setWriteStartIndex(new_start); - } - - return true; -} \ No newline at end of file diff --git a/src/cbsdk/ContinuousData.h b/src/cbsdk/ContinuousData.h deleted file mode 100644 index 8d720368..00000000 --- a/src/cbsdk/ContinuousData.h +++ /dev/null @@ -1,194 +0,0 @@ -////////////////////////////////////////////////////////////////////// -/** -* \file ContinuousData.h -* \brief Continuous data classes for per-group allocation (internal use) -* -* This header contains the refactored continuous data classes that use -* per-group allocation instead of per-channel allocation. This reduces memory -* usage and improves cache locality. -* -* These classes are internal to the SDK implementation and are not part of -* the public API. -*/ - -#ifndef CONTINUOUSDATA_H_INCLUDED -#define CONTINUOUSDATA_H_INCLUDED - -#include "../../include/cerelink/cbhwlib.h" -#include - - -/// Class to store continuous data for a single sample group -class GroupContinuousData -{ -public: - /// Constructor - initializes all fields to safe defaults - GroupContinuousData(); - - // Allow ContinuousData to access private mutex for multi-group operations - friend class ContinuousData; - - /// Destructor - ~GroupContinuousData(); - - // Disable copy (to avoid accidental deep copy issues) - GroupContinuousData(const GroupContinuousData&) = delete; - GroupContinuousData& operator=(const GroupContinuousData&) = delete; - - /// Check if reallocation is needed for new channel configuration - /// \param chan_ids Array of channel IDs from packet - /// \param n_chans Number of channels in packet - /// \return true if reallocation/update needed - [[nodiscard]] bool needsReallocation(const uint16_t* chan_ids, uint32_t n_chans) const; - - /// Allocate or reallocate buffers for this group - /// \param buffer_size Number of samples to buffer - /// \param n_chans Number of channels - /// \param chan_ids Array of channel IDs (1-based) - /// \return true if allocation succeeded - [[nodiscard]] bool allocate(uint32_t buffer_size, uint32_t n_chans, const uint16_t* chan_ids); - - /// Write a sample to the ring buffer - /// \param timestamp Timestamp for this sample - /// \param data Pointer to channel data (nChans elements) - /// \param n_chans Number of channels in data - /// \return true if buffer overflowed (oldest data was overwritten) - [[nodiscard]] bool writeSample(PROCTIME timestamp, const int16_t* data, uint32_t n_chans); - - /// Reset ring buffer indices and zero data (preserves allocation) - void reset(); - - /// Cleanup - deallocates all memory and resets to constructor state - void cleanup(); - - // Getters for read access - [[nodiscard]] uint32_t getSize() const { return m_size; } - [[nodiscard]] uint32_t getWriteIndex() const { return m_write_index; } - [[nodiscard]] uint32_t getWriteStartIndex() const { return m_write_start_index; } - [[nodiscard]] uint32_t getReadEndIndex() const { return m_read_end_index; } - [[nodiscard]] uint32_t getNumChannels() const { return m_num_channels; } - [[nodiscard]] const uint16_t* getChannelIds() const { return m_channel_ids; } - [[nodiscard]] const PROCTIME* getTimestamps() const { return m_timestamps; } - [[nodiscard]] const int16_t* getChannelData() const { return m_channel_data; } - [[nodiscard]] bool isAllocated() const { return m_channel_data != nullptr; } - - // Setters for write index management (used by SdkGetTrialData) - void setWriteStartIndex(const uint32_t index) { m_write_start_index = index; } - void setReadEndIndex(const uint32_t index) { m_read_end_index = index; } - void setWriteIndex(const uint32_t index) { m_write_index = index; } - -private: - // Buffer configuration - uint32_t m_size; ///< Buffer size for this group (samples) - - // Ring buffer management - uint32_t m_write_index; ///< Next write position in ring buffer - uint32_t m_write_start_index; ///< Where reading starts (oldest unread sample) - uint32_t m_read_end_index; ///< Last safe read position (snapshot for readers) - - // Dynamic channel management - uint32_t m_num_channels; ///< Number of channels in this group - uint16_t* m_channel_ids; ///< Array of channel IDs (1-based, size = num_channels) - - // Data storage (contiguous layout: [samples * channels]) - // Single contiguous allocation for [size][num_channels] layout enables bulk memcpy. - // Access: m_channel_data[sample_idx * m_num_channels + channel_idx] - PROCTIME* m_timestamps; ///< [size] - timestamp for each sample - int16_t* m_channel_data; ///< [size * num_channels] - contiguous data block - - mutable std::mutex m_mutex; ///< Mutex for thread-safe access to this group -}; - - -/// Structure to hold snapshot of group state for reading -struct GroupSnapshot -{ - uint32_t read_start_index; ///< Start of available data - uint32_t read_end_index; ///< End of available data - uint32_t num_samples; ///< Number of samples available - uint32_t num_channels; ///< Number of channels in group - uint32_t buffer_size; ///< Total buffer size - bool is_allocated; ///< Whether group is allocated -}; - -/// Class to store all continuous data organized by sample groups -class ContinuousData -{ -public: - ContinuousData() : default_size(0) {} - - uint32_t default_size; ///< Default buffer size (cbSdk_CONTINUOUS_DATA_SAMPLES) - GroupContinuousData groups[cbMAXGROUPS]; ///< One group per sample rate (0-7) - - /// Thread-safe write of a sample to a group with automatic reallocation - /// \param group_idx Group index (0-based, 0-7) - /// \param timestamp Timestamp for this sample - /// \param data Pointer to channel data - /// \param n_chans Number of channels in data - /// \param chan_ids Array of channel IDs (1-based) - /// \return true if buffer overflowed (oldest data was overwritten) - [[nodiscard]] bool writeSampleThreadSafe(uint32_t group_idx, PROCTIME timestamp, - const int16_t* data, uint32_t n_chans, - const uint16_t* chan_ids); - - /// Thread-safe snapshot of group state for reading - /// \param group_idx Group index (0-based, 0-7) - /// \param snapshot Output structure to fill with snapshot data - /// \return true if successful, false if group_idx invalid - [[nodiscard]] bool snapshotForReading(uint32_t group_idx, GroupSnapshot& snapshot); - - /// Thread-safe read of samples from a group - /// \param group_idx Group index (0-based, 0-7) - /// \param output_samples Output buffer for sample data [num_samples * num_channels] - /// \param output_timestamps Output buffer for timestamps [num_samples] - /// \param num_samples Number of samples to read (in/out - updated with actual read count) - /// \param bSeek If true, advance read pointer; if false, just peek at data - /// \return true if successful, false if group not allocated or invalid parameters - [[nodiscard]] bool readSamples(uint32_t group_idx, int16_t* output_samples, - PROCTIME* output_timestamps, uint32_t& num_samples, - bool bSeek); - - /// Helper: Find channel index within a group - /// \param group_idx Group index (0-7) - /// \param channel_id Channel ID to find (1-based) - /// \return Channel index within group (0-based), or -1 if not found - [[nodiscard]] int32_t findChannelInGroup(const uint32_t group_idx, const uint16_t channel_id) const - { - if (group_idx >= cbMAXGROUPS) - return -1; - - const auto& grp = groups[group_idx]; - const uint16_t* chan_ids = grp.getChannelIds(); - if (!chan_ids) - return -1; - - for (uint32_t i = 0; i < grp.getNumChannels(); ++i) - { - if (chan_ids[i] == channel_id) - return static_cast(i); - } - return -1; - } - - /// Reset all groups (preserves allocations) - void reset() - { - for (auto& grp : groups) - { - std::lock_guard lock(grp.m_mutex); - grp.reset(); - } - } - - /// Cleanup all groups (deallocates all memory) - void cleanup() - { - for (auto& grp : groups) - { - std::lock_guard lock(grp.m_mutex); - grp.cleanup(); - } - } -}; - -#endif // CONTINUOUSDATA_H_INCLUDED diff --git a/src/cbsdk/EventData.cpp b/src/cbsdk/EventData.cpp deleted file mode 100644 index c76b5d56..00000000 --- a/src/cbsdk/EventData.cpp +++ /dev/null @@ -1,257 +0,0 @@ -#include "EventData.h" -#include -#include -#include - -EventData::EventData() : - m_size(0), - m_timestamps(nullptr), - m_channels(nullptr), - m_units(nullptr), - m_waveform_data(nullptr), - m_write_index(0), - m_write_start_index(0) -{ -} - -EventData::~EventData() -{ - cleanup(); -} - -bool EventData::allocate(const uint32_t buffer_size) -{ - // Allocate buffer_size + 1 internally to hide the "one empty slot" ring buffer detail - // This allows users to store exactly buffer_size events - const uint32_t internal_size = buffer_size + 1; - - if (m_size == internal_size && m_timestamps != nullptr) - { - // Already allocated with same size - just reset - reset(); - return true; - } - - // Clean up old allocation if size changed - if (m_size != internal_size) - { - cleanup(); - } - - try { - // Allocate flat arrays - m_timestamps = new PROCTIME[internal_size]; - std::fill_n(m_timestamps, internal_size, static_cast(0)); - - m_channels = new uint16_t[internal_size]; - std::fill_n(m_channels, internal_size, static_cast(0)); - - m_units = new uint16_t[internal_size]; - std::fill_n(m_units, internal_size, static_cast(0)); - - m_size = internal_size; - m_write_index = 0; - m_write_start_index = 0; - m_waveform_data = nullptr; // Managed externally - - return true; - - } catch (...) { - // Allocation failed - cleanup partial allocation - if (m_timestamps) - { - delete[] m_timestamps; - m_timestamps = nullptr; - } - if (m_channels) - { - delete[] m_channels; - m_channels = nullptr; - } - if (m_units) - { - delete[] m_units; - m_units = nullptr; - } - m_size = 0; - return false; - } -} - -bool EventData::writeEvent(const uint16_t channel, const PROCTIME timestamp, const uint16_t unit) -{ - if (channel == 0 || channel > cbMAXCHANS) - return false; // Invalid channel - - if (!m_timestamps) - return false; // Not allocated - - // Store event data - m_timestamps[m_write_index] = timestamp; - m_channels[m_write_index] = channel; - m_units[m_write_index] = unit; - - // Advance write index (circular buffer) - const uint32_t next_write_index = (m_write_index + 1) % m_size; - - // Check for buffer overflow - bool overflow = false; - if (next_write_index == m_write_start_index) - { - // Buffer is full - overwrite oldest data - overflow = true; - m_write_start_index = (m_write_start_index + 1) % m_size; - } - - m_write_index = next_write_index; - return overflow; -} - -void EventData::reset() -{ - if (m_size) - { - if (m_timestamps) - std::fill_n(m_timestamps, m_size, static_cast(0)); - - if (m_channels) - std::fill_n(m_channels, m_size, static_cast(0)); - - if (m_units) - std::fill_n(m_units, m_size, static_cast(0)); - - m_write_index = 0; - m_write_start_index = 0; - } - - m_waveform_data = nullptr; // Managed externally, don't delete -} - -void EventData::cleanup() -{ - if (m_timestamps) - { - delete[] m_timestamps; - m_timestamps = nullptr; - } - - if (m_channels) - { - delete[] m_channels; - m_channels = nullptr; - } - - if (m_units) - { - delete[] m_units; - m_units = nullptr; - } - - m_waveform_data = nullptr; // Managed externally, don't delete - m_size = 0; - m_write_index = 0; - m_write_start_index = 0; -} - -uint32_t EventData::getNumEvents() const -{ - if (!m_timestamps) - return 0; - - // Calculate number of events in ring buffer - int32_t num_events = m_write_index - m_write_start_index; - if (num_events < 0) - num_events += m_size; - - return static_cast(num_events); -} - -void EventData::setWriteStartIndex(uint32_t index) -{ - // Assert catches bugs in debug builds - assert((m_size == 0 || index < m_size) && "setWriteStartIndex: index out of bounds"); - - // Defensive check in release builds - if (m_size > 0 && index >= m_size) - return; - - m_write_start_index = index; -} - -void EventData::setWriteIndex(uint32_t index) -{ - // Assert catches bugs in debug builds - assert((m_size == 0 || index < m_size) && "setWriteIndex: index out of bounds"); - - // Defensive check in release builds - if (m_size > 0 && index >= m_size) - return; - - m_write_index = index; -} - -uint32_t EventData::readEvents(PROCTIME* output_timestamps, - uint16_t* output_channels, - uint16_t* output_units, - uint32_t max_events, - bool bSeek) -{ - if (!m_timestamps) - return 0; // Not allocated - - const uint32_t available = getNumEvents(); - if (available == 0) - return 0; - - const uint32_t num_to_read = std::min(available, max_events); - - // Read from ring buffer using bulk memory copies - uint32_t read_index = m_write_start_index; - const uint32_t end_index = read_index + num_to_read; - - if (end_index <= m_size) - { - // No wraparound - single bulk copy per array - if (output_timestamps) - std::memcpy(output_timestamps, &m_timestamps[read_index], num_to_read * sizeof(PROCTIME)); - if (output_channels) - std::memcpy(output_channels, &m_channels[read_index], num_to_read * sizeof(uint16_t)); - if (output_units) - std::memcpy(output_units, &m_units[read_index], num_to_read * sizeof(uint16_t)); - - read_index = end_index % m_size; - } - else - { - // Wraparound - two bulk copies per array - const uint32_t first_chunk = m_size - read_index; - const uint32_t second_chunk = num_to_read - first_chunk; - - // Copy first chunk (from read_index to end of buffer) - if (output_timestamps) - { - std::memcpy(output_timestamps, &m_timestamps[read_index], first_chunk * sizeof(PROCTIME)); - std::memcpy(&output_timestamps[first_chunk], m_timestamps, second_chunk * sizeof(PROCTIME)); - } - if (output_channels) - { - std::memcpy(output_channels, &m_channels[read_index], first_chunk * sizeof(uint16_t)); - std::memcpy(&output_channels[first_chunk], m_channels, second_chunk * sizeof(uint16_t)); - } - if (output_units) - { - std::memcpy(output_units, &m_units[read_index], first_chunk * sizeof(uint16_t)); - std::memcpy(&output_units[first_chunk], m_units, second_chunk * sizeof(uint16_t)); - } - - read_index = second_chunk; - } - - // Update read position if seeking - if (bSeek) - { - m_write_start_index = read_index; - } - - return num_to_read; -} diff --git a/src/cbsdk/EventData.h b/src/cbsdk/EventData.h deleted file mode 100644 index e30d75d8..00000000 --- a/src/cbsdk/EventData.h +++ /dev/null @@ -1,103 +0,0 @@ -////////////////////////////////////////////////////////////////////// -/** -* \file EventData.h -* \brief Event data class for spike/event storage (internal use) -* -* This header contains the refactored event data class that stores -* spike and digital input event data as a flat time-series of events. -* -* This class is internal to the SDK implementation and is not part of -* the public API. -*/ - -#ifndef EVENTDATA_H_INCLUDED -#define EVENTDATA_H_INCLUDED - -#include "../../include/cerelink/cbhwlib.h" -#include - - -/// Class to store event data (spikes, digital inputs) as a time-series -class EventData -{ -public: - /// Constructor - initializes all fields to safe defaults - EventData(); - - /// Destructor - ~EventData(); - - // Disable copy (to avoid accidental deep copy issues) - EventData(const EventData&) = delete; - EventData& operator=(const EventData&) = delete; - - /// Allocate or reallocate buffers for event storage - /// \param buffer_size Total number of events to buffer (across all channels) - /// \return true if allocation succeeded - [[nodiscard]] bool allocate(uint32_t buffer_size); - - /// Write an event to the ring buffer - /// \param channel Channel number (1-based) - /// \param timestamp Timestamp for this event - /// \param unit Unit classification (0-5 for units, 255 for noise) or digital data - /// \return true if buffer overflowed (oldest data was overwritten) - [[nodiscard]] bool writeEvent(uint16_t channel, PROCTIME timestamp, uint16_t unit); - - /// Reset ring buffer indices and zero data (preserves allocation) - void reset(); - - /// Cleanup - deallocates all memory and resets to constructor state - void cleanup(); - - // Getters for read access - [[nodiscard]] uint32_t getSize() const { return m_size > 0 ? m_size - 1 : 0; } // Return usable capacity - [[nodiscard]] uint32_t getWriteIndex() const { return m_write_index; } - [[nodiscard]] uint32_t getWriteStartIndex() const { return m_write_start_index; } - [[nodiscard]] uint32_t getNumEvents() const; // Number of events currently in buffer - - [[nodiscard]] const PROCTIME* getTimestamps() const { return m_timestamps; } - [[nodiscard]] const uint16_t* getChannels() const { return m_channels; } - [[nodiscard]] const uint16_t* getUnits() const { return m_units; } - - [[nodiscard]] int16_t* getWaveformData() { return m_waveform_data; } - [[nodiscard]] const int16_t* getWaveformData() const { return m_waveform_data; } - [[nodiscard]] bool isAllocated() const { return m_timestamps != nullptr; } - - // Setters for write index management - void setWriteStartIndex(uint32_t index); - void setWriteIndex(uint32_t index); - - /// Read all events from the ring buffer - /// \param output_timestamps Output array for timestamps - /// \param output_channels Output array for channel IDs - /// \param output_units Output array for units/digital data - /// \param max_events Maximum number of events to read - /// \param bSeek If true, advance read position - /// \return Actual number of events read - [[nodiscard]] uint32_t readEvents(PROCTIME* output_timestamps, - uint16_t* output_channels, - uint16_t* output_units, - uint32_t max_events, - bool bSeek); - - // Public member for external access control - mutable std::mutex m_mutex; ///< Mutex for thread-safe access - -private: - // Buffer configuration - uint32_t m_size; ///< Total buffer capacity (events across all channels) - - // Flat time-series storage - PROCTIME* m_timestamps; ///< [size] - timestamp for each event - uint16_t* m_channels; ///< [size] - channel ID (1-based) for each event - uint16_t* m_units; ///< [size] - unit classification or digital data for each event - - // Shared waveform buffer (allocated on demand, managed externally) - int16_t* m_waveform_data; ///< Buffer with maximum size [size][cbMAX_PNTS] - - // Ring buffer management (single buffer for all channels) - uint32_t m_write_index; ///< Next index location to write data - uint32_t m_write_start_index; ///< Index location that reading can begin -}; - -#endif // EVENTDATA_H_INCLUDED diff --git a/src/cbsdk/README.md b/src/cbsdk/README.md new file mode 100644 index 00000000..12a93432 --- /dev/null +++ b/src/cbsdk/README.md @@ -0,0 +1,131 @@ +# cbsdk_v2 - New SDK Public API + +**Status:** Phase 4 - Not Started + +## Purpose + +Public C API that orchestrates cbdev + cbshm to provide a clean, stable interface for users. + +**Key Goal:** Hide all multi-instrument and indexing complexity from users! + +## Core Functionality + +1. **Session Management** + - `cbSdkOpen_v2()` / `cbSdkClose_v2()` + - Automatic mode detection (standalone vs. client) + - Device name resolution ("Hub1" → IP address) + +2. **Orchestration** + - Manages lifecycle of cbshm + cbdev + - Routes packets: device → shmem → user callbacks + - Handles mode-specific logic internally + +3. **Configuration API** + - `cbSdkGetProcInfo_v2()` - uses first active instrument + - `cbSdkGetChanInfo_v2()` / `cbSdkSetChanInfo_v2()` + - All config operations abstracted from multi-instrument complexity + +4. **Callback System** + - Register user callbacks for packets + - Thread-safe callback invocation + - Flexible callback management + +## Key Design Decisions + +- **C API:** Public interface is pure C for ABI stability +- **C++ Implementation:** Internal SdkSession class in C++ +- **Hide Complexity:** Users never see InstrumentId, indexing, or mode details +- **Stable API:** Can evolve cbshm/cbdev without breaking users + +## Current Status + +- [x] Directory structure created +- [x] CMake integration added (placeholder) +- [ ] SdkSession class (C++) designed +- [ ] C API wrappers implemented +- [ ] Mode detection logic implemented +- [ ] Packet routing implemented +- [ ] Callback system implemented +- [ ] Device name resolution implemented +- [ ] Integration tests written + +## API Preview + +### C API (Public) + +```c +// cbsdk_v2.h +typedef uint32_t cbSdkHandle; + +typedef struct { + cbSdkConnectionType conType; // DEFAULT, CENTRAL, STANDALONE + const char* deviceAddress; // Optional: override default + uint16_t deviceRecvPort; // Optional: override default + uint16_t deviceSendPort; // Optional: override default +} cbSdkConnectionInfo; + +// Open session +cbSdkResult cbSdkOpen_v2( + uint32_t instance, + const cbSdkConnectionInfo* pConnection, + cbSdkHandle* pHandle +); + +// Close session +cbSdkResult cbSdkClose_v2(cbSdkHandle handle); + +// Get config (simplified - no instrument IDs!) +cbSdkResult cbSdkGetProcInfo_v2( + cbSdkHandle handle, + cbPROCINFO* pInfo +); + +cbSdkResult cbSdkGetChanInfo_v2( + cbSdkHandle handle, + uint16_t channel, + cbCHANINFO* pInfo +); +``` + +### C++ Implementation (Internal) + +```cpp +// sdk_session.cpp +class SdkSession { +public: + cbSdkResult open(const cbSdkConnectionInfo* pConn) { + // 1. Determine mode (standalone vs. client) + // 2. Open cbshm + // 3. If standalone: open cbdev and start receive thread + // 4. Register packet callback: cbdev → cbshm → user + } + + cbSdkResult getProcInfo(cbPROCINFO* pInfo) { + // Uses cbshm::getFirstProcInfo() + // User never knows about multi-instrument complexity! + } + +private: + cbshm::ShmemSession m_shmem; + cbdev::DeviceSession m_device; +}; +``` + +## Migration from Old API + +Users will transition from: +```c +cbSdkOpen(INST, conType, con); // Old API +``` + +To: +```c +cbSdkOpen_v2(instance, &conInfo, &handle); // New API +``` + +During Phase 5, we'll provide migration guide and compatibility shims. + +## References + +- Design document: `docs/refactor_plan.md` (Phase 4) +- Current SDK: `src/cbsdk/cbsdk.cpp`, `include/cerelink/cbsdk.h` diff --git a/src/cbsdk/SdkApp.h b/src/cbsdk/SdkApp.h deleted file mode 100644 index 1d2acb91..00000000 --- a/src/cbsdk/SdkApp.h +++ /dev/null @@ -1,250 +0,0 @@ -/* =STS=> SdkApp.h[5022].aa11 submit SMID:12 */ -////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2012 - 2017 Blackrock Microsystems -// -// $Workfile: SdkApp.h $ -// $Archive: /Cerebus/Human/WindowsApps/cbmex/SdkApp.h $ -// $Revision: 1 $ -// $Date: 4/29/12 1:21p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////// -/** -* \file SdkApp.h -* \brief Cerebus SDK - This header file is distributed as part of the SDK. -*/ -#ifndef SDKAPP_H_INCLUDED -#define SDKAPP_H_INCLUDED - -#include "InstNetwork.h" -#include "../include/cerelink/cbsdk.h" -#include "../include/cerelink/CCFUtils.h" -#include "ContinuousData.h" -#include "EventData.h" -#include -#include - -/// Wrapper class for SDK Qt application -class SdkApp : public InstNetwork, public InstNetwork::Listener -{ -public: - SdkApp(); - ~SdkApp() override; -public: - // Override the Listener::ProcessIncomingPacket virtual function - void ProcessIncomingPacket(const cbPKT_GENERIC * pPkt) override; // Process incoming packets - uint32_t GetInstInfo() const {return m_instInfo;} - cbRESULT GetLastCbErr() const {return m_lastCbErr;} - void Open(uint32_t nInstance, int nInPort = cbNET_UDP_PORT_BCAST, int nOutPort = cbNET_UDP_PORT_CNT, - LPCSTR szInIP = cbNET_UDP_ADDR_INST, LPCSTR szOutIP = cbNET_UDP_ADDR_CNT, int nRecBufSize = NSP_REC_BUF_SIZE, int nRange = 0); -private: - void OnPktGroup(const cbPKT_GROUP * pkt) const; - void OnPktEvent(const cbPKT_GENERIC * pPkt); - void OnPktComment(const cbPKT_COMMENT * pPkt); - void OnPktLog(const cbPKT_LOG * pPkt); - void OnPktTrack(const cbPKT_VIDEOTRACK * pPkt); - - void LateBindCallback(cbSdkCallbackType callbackType); - void LinkFailureEvent(const cbSdkPktLostEvent & lost); - void InstInfoEvent(uint32_t instInfo); - cbSdkResult unsetTrialConfig(cbSdkTrialType type); - -public: - // --------------------------- - // All SDK functions come here - // --------------------------- - - void SdkAsynchCCF(ccf::ccfResult res, LPCSTR szFileName, cbStateCCF state, uint32_t nProgress); - cbSdkResult SdkGetVersion(cbSdkVersion *version) const; - cbSdkResult SdkReadCCF(cbSdkCCF * pData, const char * szFileName, bool bConvert, bool bSend, bool bThreaded); // From file or device if szFileName is null - cbSdkResult SdkFetchCCF(cbSdkCCF * pData) const; // from device only - cbSdkResult SdkWriteCCF(cbSdkCCF * pData, const char * szFileName, bool bThreaded); // to file or device if szFileName is null - cbSdkResult SdkSendCCF(cbSdkCCF * pData, bool bAutosort = false); // to device only - cbSdkResult SdkOpen(uint32_t nInstance, cbSdkConnectionType conType, cbSdkConnection con); - cbSdkResult SdkGetType(cbSdkConnectionType * conType, cbSdkInstrumentType * instType) const; - cbSdkResult SdkUnsetTrialConfig(cbSdkTrialType type); - cbSdkResult SdkClose(); - cbSdkResult SdkGetTime(PROCTIME * cbtime) const; - cbSdkResult SdkGetSpkCache(uint16_t channel, cbSPKCACHE **cache) const; - cbSdkResult SdkGetTrialConfig(uint32_t * pbActive, uint16_t * pBegchan, uint32_t * pBegmask, uint32_t * pBegval, - uint16_t * pEndchan, uint32_t * pEndmask, uint32_t * pEndval, - uint32_t * puWaveforms, uint32_t * puConts, uint32_t * puEvents, - uint32_t * puComments, uint32_t * puTrackings) const; - cbSdkResult SdkSetTrialConfig(uint32_t bActive, uint16_t begchan, uint32_t begmask, uint32_t begval, - uint16_t endchan, uint32_t endmask, uint32_t endval, - uint32_t uWaveforms, uint32_t uConts, uint32_t uEvents, uint32_t uComments, uint32_t uTrackings); - cbSdkResult SdkGetChannelLabel(uint16_t channel, uint32_t * bValid, char * label, uint32_t * userflags, int32_t * position) const; - cbSdkResult SdkSetChannelLabel(uint16_t channel, const char * label, uint32_t userflags, const int32_t * position) const; - cbSdkResult SdkGetTrialData(uint32_t bSeek, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking); - cbSdkResult SdkInitTrialData(uint32_t bResetClock, cbSdkTrialEvent* trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking, unsigned long wait_for_comment_msec = 250); - cbSdkResult SdkSetFileConfig(const char * filename, const char * comment, uint32_t bStart, uint32_t options); - cbSdkResult SdkGetFileConfig(char * filename, char * username, bool * pbRecording) const; - cbSdkResult SdkSetPatientInfo(const char * ID, const char * firstname, const char * lastname, - uint32_t DOBMonth, uint32_t DOBDay, uint32_t DOBYear) const; - cbSdkResult SdkInitiateImpedance() const; - cbSdkResult SdkSendPoll(const char* appname, uint32_t mode, uint32_t flags, uint32_t extra); - cbSdkResult SdkSendPacket(void * ppckt) const; - cbSdkResult SdkSetSystemRunLevel(uint32_t runlevel, uint32_t locked, uint32_t resetque) const; - cbSdkResult SdkGetSystemRunLevel(uint32_t * runlevel, uint32_t * runflags, uint32_t * resetque) const; - cbSdkResult SdkSetDigitalOutput(uint16_t channel, uint16_t value) const; - cbSdkResult SdkSetSynchOutput(uint16_t channel, uint32_t nFreq, uint32_t nRepeats) const; - cbSdkResult SdkExtDoCommand(const cbSdkExtCmd * extCmd) const; - cbSdkResult SdkSetAnalogOutput(uint16_t channel, const cbSdkWaveformData * wf, const cbSdkAoutMon * mon) const; - cbSdkResult SdkSetChannelMask(uint16_t channel, uint32_t bActive); - cbSdkResult SdkSetComment(uint32_t rgba, uint8_t charset, const char * comment) const; - cbSdkResult SdkSetChannelConfig(uint16_t channel, cbPKT_CHANINFO * chaninfo) const; - cbSdkResult SdkGetChannelConfig(uint16_t channel, cbPKT_CHANINFO * chaninfo) const; - cbSdkResult SdkGetSampleGroupList(uint32_t proc, uint32_t group, uint32_t *length, uint16_t *list) const; - cbSdkResult SdkGetSampleGroupInfo(uint32_t proc, uint32_t group, char *label, uint32_t *period, uint32_t *length) const; - cbSdkResult SdkSetAinpSampling(uint32_t chan, uint32_t filter, uint32_t group) const; - cbSdkResult SdkSetAinpSpikeOptions(uint32_t chan, uint32_t flags, uint32_t filter) const; - cbSdkResult SdkGetFilterDesc(uint32_t proc, uint32_t filt, cbFILTDESC * filtdesc) const; - cbSdkResult SdkGetTrackObj(char * name, uint16_t * type, uint16_t * pointCount, uint32_t id) const; - cbSdkResult SdkGetVideoSource(char * name, float * fps, uint32_t id) const; - cbSdkResult SdkSetSpikeConfig(uint32_t spklength, uint32_t spkpretrig) const; - cbSdkResult SdkGetSysConfig(uint32_t * spklength, uint32_t * spkpretrig, uint32_t * sysfreq) const; - cbSdkResult SdkSystem(cbSdkSystemType cmd) const; - cbSdkResult SdkCallbackStatus(cbSdkCallbackType callbackType) const; - cbSdkResult SdkRegisterCallback(cbSdkCallbackType callbackType, cbSdkCallback pCallbackFn, void * pCallbackData); - cbSdkResult SdkUnRegisterCallback(cbSdkCallbackType callbackType); - cbSdkResult SdkAnalogToDigital(uint16_t channel, const char * szVoltsUnitString, int32_t * digital) const; - - -protected: - void OnInstNetworkEvent(NetEventType type, unsigned int code) override; // Event from the instrument network - bool m_bInitialized; // If initialized - cbRESULT m_lastCbErr; // Last error - - // Wait condition for connection open - std::condition_variable m_connectWait; - std::mutex m_connectLock; - - // Which channels to listen to - bool m_bChannelMask[cbMAXCHANS]{}; - cbPKT_VIDEOSYNCH m_lastPktVideoSynch{}; // last video synchronization packet - - cbSdkPktLostEvent m_lastLost{}; // Last lost event - cbSdkInstInfo m_lastInstInfo{}; // Last instrument info event - - // Lock for accessing the callbacks - std::mutex m_lockCallback; - // Actual registered callbacks - cbSdkCallback m_pCallback[CBSDKCALLBACK_COUNT]{}; - void * m_pCallbackParams[CBSDKCALLBACK_COUNT]{}; - // Late bound versions are internal - cbSdkCallback m_pLateCallback[CBSDKCALLBACK_COUNT]{}; - void * m_pLateCallbackParams[CBSDKCALLBACK_COUNT]{}; - - ///////////////////////////////////////////////////////////////////////////// - // Declarations for tracking the beginning and end of trials - - std::mutex m_lockTrial; - std::mutex m_lockTrialEvent; - std::mutex m_lockTrialComment; - std::mutex m_lockTrialTracking; - - // For synchronization of threads - std::mutex m_lockGetPacketsEvent; - std::condition_variable m_waitPacketsEvent; - bool m_bPacketsEvent; - - std::mutex m_lockGetPacketsCmt; - std::condition_variable m_waitPacketsCmt; - bool m_bPacketsCmt; - - std::mutex m_lockGetPacketsTrack; - std::condition_variable m_waitPacketsTrack; - bool m_bPacketsTrack; - - uint16_t m_uTrialBeginChannel; // Channel ID that is watched for the trial begin notification - uint32_t m_uTrialBeginMask; // Mask ANDed with channel data to check for trial beginning - uint32_t m_uTrialBeginValue; // Value the masked data is compared to identify trial beginning - uint16_t m_uTrialEndChannel; // Channel ID that is watched for the trial end notification - uint32_t m_uTrialEndMask; // Mask ANDed with channel data to check for trial end - uint32_t m_uTrialEndValue; // Value the masked data is compared to identify trial end - uint32_t m_uTrialWaveforms; // If spike waveform should be stored and returned - uint32_t m_uTrialConts; // Number of continuous data to buffer - uint32_t m_uTrialEvents; // Number of events to buffer - uint32_t m_uTrialComments; // Number of comments to buffer - uint32_t m_uTrialTrackings; // Number of tracking data to buffer - uint32_t m_bWithinTrial; // True is we are within a trial, False if not within a trial - PROCTIME m_uTrialStartTime; // Holds the Cerebus timestamp of the trial start time - PROCTIME m_uCbsdkTime; // Holds the Cerebus timestamp of the last packet received - PROCTIME m_nextTrialStartTime; // TrialStartTime will be updated to this after GetData if InitData has bResetClock=true - - ///////////////////////////////////////////////////////////////////////////// - // Declarations for the data caching structures and variables - - // Continuous data structures are defined in ContinuousData.h - ContinuousData * m_CD; - - // Event data structures are defined in EventData.h - EventData * m_ED; - - // Structure to store all the variables associated with the comment data - struct CommentData - { - uint32_t size; // default is 0 - uint8_t * charset; - uint32_t * rgba; - uint8_t * * comments; - PROCTIME * timestamps; - uint32_t write_index; - uint32_t write_start_index; - - void reset() - { - for (uint32_t i = 0; i < size; ++i) - { - memset(comments[i], 0, (cbMAX_COMMENT + 1) * sizeof(uint8_t)); - timestamps[i] = rgba[i] = charset[i] = 0; - } - write_index = write_start_index = 0; - } - - } * m_CMT; - - // Structure to store all the variables associated with the video tracking data - struct TrackingData - { - uint32_t size; // default is 0 - uint16_t max_point_counts[cbMAXTRACKOBJ]; - uint8_t node_name[cbMAXTRACKOBJ][cbLEN_STR_LABEL + 1]; - uint16_t node_type[cbMAXTRACKOBJ]; // cbTRACKOBJ_TYPE_* (note that 0 means undefined) - uint16_t * point_counts[cbMAXTRACKOBJ]; - void * * coords[cbMAXTRACKOBJ]; - PROCTIME * timestamps[cbMAXTRACKOBJ]; - uint32_t * synch_frame_numbers[cbMAXTRACKOBJ]; - uint32_t * synch_timestamps[cbMAXTRACKOBJ]; - uint32_t write_index[cbMAXTRACKOBJ]; - uint32_t write_start_index[cbMAXTRACKOBJ]; - - void reset() - { - if (size) - { - for (uint32_t i = 0; i < cbMAXTRACKOBJ; ++i) - { - std::fill_n(point_counts[i], size, 0); - std::fill_n(timestamps[i], size, 0); - std::fill_n(synch_frame_numbers[i], size, 0); - std::fill_n(synch_timestamps[i], size, 0); - } - } - - memset(max_point_counts, 0, sizeof(max_point_counts)); - memset(node_name, 0, sizeof(node_name)); - memset(node_type, 0, sizeof(node_type)); - memset(write_index, 0, sizeof(write_index)); - memset(write_start_index, 0, sizeof(write_start_index)); - } - - } * m_TR; -}; - -#endif // SDKAPP_H_INCLUDED diff --git a/src/cbsdk/cbsdk.cpp b/src/cbsdk/cbsdk.cpp deleted file mode 100644 index 831b946e..00000000 --- a/src/cbsdk/cbsdk.cpp +++ /dev/null @@ -1,4130 +0,0 @@ -// =STS=> cbsdk.cpp[5021].aa03 open SMID:3 -////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2010 - 2021 Blackrock Microsystems, LLC -// -// $Workfile: cbsdk.cpp $ -// $Archive: /Cerebus/Human/WindowsApps/cbmex/cbsdk.cpp $ -// $Revision: 1 $ -// $Date: 03/23/11 11:06p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////// -// -// Notes: -// Do NOT throw exceptions, they are evil if cross the shared library. -// catch possible exceptions and handle them the earliest possible in this library, -// then return error code if the library cannot recover. -// Only functions are exported, no data, and definitely NO classes -// -/** -* \file cbsdk.cpp -* \brief Cerebus SDK main file. -*/ - -#include "StdAfx.h" -#include // Use C++ default min and max implementation. -#include // For std::chrono::milliseconds -#include "SdkApp.h" -#include "../central/BmiVersion.h" -#include "cbHwlibHi.h" -#include "debugmacs.h" -#include -#include "../../bindings/cbmex/res/cbmex.rc2" - -#ifndef WIN32 -#ifndef Sleep - #define Sleep(x) usleep((x) * 1000) -#endif -#endif - -// The sdk instances -SdkApp * g_app[cbMAXOPEN] = {nullptr}; - -#ifdef WIN32 -// Author & Date: Ehsan Azar 31 March 2011 -// Purpose: Dll entry initialization -BOOL APIENTRY DllMain( HMODULE hModule, - DWORD ul_reason_for_call, - LPVOID lpReserved - ) -{ - switch (ul_reason_for_call) - { - case DLL_PROCESS_ATTACH: - break; - case DLL_THREAD_ATTACH: - break; - case DLL_THREAD_DETACH: - break; - case DLL_PROCESS_DETACH: - // Only if FreeLibrary is called (extension usage) - // for non-extension usage OS takes better care of cleanup on exit. - if (lpReserved == nullptr) - { - for (int i = 0; i < cbMAXOPEN; ++i) - cbSdkClose(i); - } - break; - } - return TRUE; -} -#endif - -// Author & Date: Kirk Korver 03 Aug 2005 - -/** Called when a Sample Group packet (aka continuous data point or LFP) comes in. -* -* @param[in] pkt the sample group packet -*/ -void SdkApp::OnPktGroup(const cbPKT_GROUP * const pkt) const { - // uint32_t nChanProcStart = 0; - uint32_t nChanProcMax = 0; - cbPROCINFO isProcInfo; - uint32_t nInstrument = 0; -#ifndef CBPROTO_311 - nInstrument = pkt->cbpkt_header.instrument; -#endif - if (IsStandAlone()) - nInstrument = 0; - - if (!m_bWithinTrial || m_CD == nullptr) - return; - - const int group = pkt->cbpkt_header.type; - - if (group > SMPGRP_RAW) - return; - -#ifndef CBPROTO_311 - if (pkt->cbpkt_header.instrument >= cbMAXPROCS) - nInstrument = 0; - for (uint32_t nProc = 0; nProc < cbMAXPROCS; ++nProc) - { - if (NSP_FOUND == cbGetNspStatus(nProc + 1)) - { - if (cbRESULT_OK == ::cbGetProcInfo(nProc + 1, &isProcInfo)) - nChanProcMax += isProcInfo.chancount; - - if (pkt->cbpkt_header.instrument == nProc) - break; - - // nChanProcStart = nChanProcMax; - } - } -#endif - - // Get information about this group... - uint32_t period; // sampling period for the group - uint32_t nChans; // number of channels in the group - uint16_t chanIds[cbNUM_ANALOG_CHANS]; - if (cbGetSampleGroupInfo(nInstrument + 1, group, nullptr, &period, &nChans, m_nInstance) != cbRESULT_OK) - return; - if (cbGetSampleGroupList(nInstrument + 1, group, &nChans, chanIds, m_nInstance) != cbRESULT_OK) - return; - - // Write sample using thread-safe method (handles allocation and locking internally) - const auto grp_idx = group - 1; // Convert to 0-based index - const bool bOverFlow = m_CD->writeSampleThreadSafe(grp_idx, pkt->cbpkt_header.time, - pkt->data, nChans, chanIds); - - if (bOverFlow) - { - /// \todo trial continuous buffer overflow event - } -} - -// Author & Date: Ehsan Azar 24 March 2011 -/** Called when a spike, digital or serial packet (aka event data) comes in. -* -* Also, the trial start and stop are set. -* -* @param[in] pPkt the event packet -*/ -void SdkApp::OnPktEvent(const cbPKT_GENERIC * const pPkt) -{ - // check for trial beginning notification - if (pPkt->cbpkt_header.chid == m_uTrialBeginChannel) - { - if ( (m_uTrialBeginMask & pPkt->data[0]) == m_uTrialBeginValue ) - { - // reset the trial data cache if WithinTrial is currently False - if (!m_bWithinTrial) - { - cbGetSystemClockTime(&m_uTrialStartTime, m_nInstance); - m_nextTrialStartTime = m_uTrialStartTime; - } - - m_bWithinTrial = true; - } - } - - if (m_ED && m_bWithinTrial) - { - bool bOverFlow = false; - - m_lockTrialEvent.lock(); - // double check if buffer is still valid - if (m_ED) - { - // Determine the unit value based on channel type - uint16_t unit; - if (IsChanDigin(pPkt->cbpkt_header.chid) || IsChanSerial(pPkt->cbpkt_header.chid)) - unit = static_cast(pPkt->data[0] & 0x0000ffff); // Store the 0th data sample (truncated to 16-bit). - else - unit = pPkt->cbpkt_header.type; // Store the type. - - // Write event to buffer (writeEvent returns true if overflow occurred) - bOverFlow = m_ED->writeEvent(pPkt->cbpkt_header.chid, pPkt->cbpkt_header.time, unit); - - if (m_bPacketsEvent) - { - m_lockGetPacketsEvent.lock(); - if (pPkt->cbpkt_header.time > m_uTrialStartTime) - { - m_bPacketsEvent = false; - m_waitPacketsEvent.notify_all(); - } - m_lockGetPacketsEvent.unlock(); - } - } - m_lockTrialEvent.unlock(); - - if (bOverFlow) - { - /// \todo trial event buffer overflow event - } - } - - // check for trial end notification - if (pPkt->cbpkt_header.chid == m_uTrialEndChannel) - { - if ( (m_uTrialEndMask & pPkt->data[0]) == m_uTrialEndValue ) - m_bWithinTrial = false; - } -} - -// Author & Date: Ehsan Azar 27 Oct 2011 -/** Called when a comment packet comes in. -* -* @param[in] pPkt the comment packet -*/ -void SdkApp::OnPktComment(const cbPKT_COMMENT * const pPkt) -{ - if (m_CMT && m_bWithinTrial) - { - m_lockTrialComment.lock(); - // double check if buffer is still valid - if (m_CMT) - { - // Add a sample... - // If there's room for more data... - uint32_t new_write_index = m_CMT->write_index + 1; - if (new_write_index >= m_CMT->size) - new_write_index = 0; - - if (new_write_index != m_CMT->write_start_index) - { - uint32_t write_index = m_CMT->write_index; - // Store more data - m_CMT->charset[write_index] = pPkt->info.charset; -#ifndef CBPROTO_311 - m_CMT->timestamps[write_index] = pPkt->timeStarted; - m_CMT->rgba[write_index] = pPkt->rgba; -#endif - - strncpy(reinterpret_cast(m_CMT->comments[write_index]), (const char *)(&pPkt->comment[0]), cbMAX_COMMENT); - m_CMT->write_index = new_write_index; - - if (m_bPacketsCmt) - { - m_lockGetPacketsCmt.lock(); - if (pPkt->cbpkt_header.time > m_uTrialStartTime) - { - m_bPacketsCmt = false; - m_waitPacketsCmt.notify_all(); - } - m_lockGetPacketsCmt.unlock(); - } - } - } - m_lockTrialComment.unlock(); - } -} - -// Author & Date: Hyrum L. Sessions 10 June 2016 -/** Called when a comment packet comes in. -* -* @param[in] pPkt the log packet -*/ -void SdkApp::OnPktLog(const cbPKT_LOG * const pPkt) -{ - if (m_CMT && m_bWithinTrial) - { - m_lockTrialComment.lock(); - // double check if buffer is still valid - if (m_CMT) - { - // Add a sample... - // If there's room for more data... - uint32_t new_write_index = m_CMT->write_index + 1; - if (new_write_index >= m_CMT->size) - new_write_index = 0; - - if (new_write_index != m_CMT->write_start_index) - { - uint32_t write_index = m_CMT->write_index; - // Store more data - m_CMT->charset[write_index] = 0; // force to ANSI charset - m_CMT->rgba[write_index] = 0xFFFFFFFF; - m_CMT->timestamps[write_index] = pPkt->cbpkt_header.time; - - strncpy(reinterpret_cast(m_CMT->comments[write_index]), (const char *)(&pPkt->desc[0]), cbMAX_LOG); - m_CMT->write_index = new_write_index; - - if (m_bPacketsCmt) - { - m_lockGetPacketsCmt.lock(); - if (pPkt->cbpkt_header.time > m_uTrialStartTime) - { - m_bPacketsCmt = false; - m_waitPacketsCmt.notify_all(); - } - m_lockGetPacketsCmt.unlock(); - } - } - } - m_lockTrialComment.unlock(); - } -} - -// Author & Date: Ehsan Azar 27 Oct 2011 -/** Called when a video tracking packet comes in. -* -* Fills tracking global cache considering the last synchronization packet. -* -* @param[in] pPkt the video packet -*/ -void SdkApp::OnPktTrack(const cbPKT_VIDEOTRACK * const pPkt) -{ - if (m_TR && m_bWithinTrial && m_lastPktVideoSynch.cbpkt_header.chid == cbPKTCHAN_CONFIGURATION) - { - const uint16_t id = pPkt->nodeID; // 0-based node id - // double check if buffer is still valid - if (m_TR == nullptr) - return; - uint16_t node_type = 0; - // safety checks - if (id >= cbMAXTRACKOBJ) - return; - if (cbGetTrackObj(nullptr, &node_type, nullptr, id + 1, m_nInstance) != cbRESULT_OK) - { - m_TR->node_type[id] = 0; - m_TR->max_point_counts[id] = 0; - return; - } - m_lockTrialTracking.lock(); - // New tracking data or tracking type changed - if (node_type != m_TR->node_type[id]) - { - cbGetTrackObj(reinterpret_cast(m_TR->node_name[id]), &m_TR->node_type[id], &m_TR->max_point_counts[id], id + 1, m_nInstance); - } - if (m_TR->node_type[id]) - { - // Add a sample... - // If there's room for more data... - uint32_t new_write_index = m_TR->write_index[id] + 1; - if (new_write_index >= m_TR->size) - new_write_index = 0; - - if (new_write_index != m_TR->write_start_index[id]) - { - const uint32_t write_index = m_TR->write_index[id]; - // Store more data - m_TR->timestamps[id][write_index] = pPkt->cbpkt_header.time; - m_TR->synch_timestamps[id][write_index] = m_lastPktVideoSynch.etime; - m_TR->synch_frame_numbers[id][write_index] = m_lastPktVideoSynch.frame; - m_TR->point_counts[id][write_index] = pPkt->pointCount; - - bool bWordData = false; // if data is of word-length - int dim_count = 2; // number of dimensions for each point - switch(m_TR->node_type[id]) - { - case cbTRACKOBJ_TYPE_2DMARKERS: - case cbTRACKOBJ_TYPE_2DBLOB: - case cbTRACKOBJ_TYPE_2DBOUNDARY: - dim_count = 2; - break; - case cbTRACKOBJ_TYPE_1DSIZE: - bWordData = true; - dim_count = 1; - break; - default: - dim_count = 3; - break; - } - uint32_t pointCount = pPkt->pointCount * dim_count; - if (bWordData) - { - if (pointCount > cbMAX_TRACKCOORDS / 2) - pointCount = cbMAX_TRACKCOORDS / 2; - memcpy(m_TR->coords[id][write_index], pPkt->sizes, pointCount * sizeof(uint32_t)); - } - else - { - if (pointCount > cbMAX_TRACKCOORDS) - pointCount = cbMAX_TRACKCOORDS; - memcpy(m_TR->coords[id][write_index], pPkt->coords, pointCount * sizeof(uint16_t)); - } - m_TR->write_index[id] = new_write_index; - - if (m_bPacketsTrack) - { - m_lockGetPacketsTrack.lock(); - if (pPkt->cbpkt_header.time > m_uTrialStartTime) - { - m_bPacketsTrack = false; - m_waitPacketsTrack.notify_all(); - } - m_lockGetPacketsTrack.unlock(); - } - } - } - m_lockTrialTracking.unlock(); - } -} - -// Author & Date: Ehsan Azar 16 May 2012 -/** Late binding of callback function when needed. -* -* Make sure this is called before any callback invocation. -* -* @param[in] callbackType the callback type to bind -*/ -void SdkApp::LateBindCallback(const cbSdkCallbackType callbackType) -{ - if (m_pCallback[callbackType] != m_pLateCallback[callbackType]) - { - m_lockCallback.lock(); - m_pLateCallback[callbackType] = m_pCallback[callbackType]; - m_pLateCallbackParams[callbackType] = m_pCallbackParams[callbackType]; - m_lockCallback.unlock(); - } -} - -///////////////////////////////////////////////////////////////////////////// -// Author & Date: Ehsan Azar 29 March 2011 -/** Signal packet-lost event. -* Only the first registered callback receives packet lost events. -*/ -void SdkApp::LinkFailureEvent(const cbSdkPktLostEvent & lost) -{ - m_lastLost = lost; - cbSdkCallback pCallback = nullptr; - void * pCallbackParams = nullptr; - - for (int i = 0; i < CBSDKCALLBACK_COUNT; ++i) - { - if (m_pCallback[i]) - { - m_lockCallback.lock(); - pCallback = m_pCallback[i]; - pCallbackParams = m_pCallbackParams[i]; - m_lockCallback.unlock(); - break; - } - } - - if (pCallback) - pCallback(m_nInstance, cbSdkPkt_PACKETLOST, &m_lastLost, pCallbackParams); -} - -///////////////////////////////////////////////////////////////////////////// -// Author & Date: Ehsan Azar 29 April 2012 - -/** Signal instrument information event. -* -*/ -void SdkApp::InstInfoEvent(const uint32_t instInfo) -{ - m_lastInstInfo.instInfo = instInfo; - // Late bind the ones needed before usage - LateBindCallback(CBSDKCALLBACK_ALL); - LateBindCallback(CBSDKCALLBACK_INSTINFO); - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_INSTINFO, &m_lastInstInfo, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - if (m_pLateCallback[CBSDKCALLBACK_INSTINFO]) - m_pLateCallback[CBSDKCALLBACK_INSTINFO](m_nInstance, cbSdkPkt_INSTINFO, &m_lastInstInfo, m_pLateCallbackParams[CBSDKCALLBACK_INSTINFO]); -} - -///////////////////////////////////////////////////////////////////////////// -// All the SDK functions must come after this comment -///////////////////////////////////////////////////////////////////////////// - -// Author & Date: Ehsan Azar 23 Feb 2011 -/** Get cbsdk version information. -* See docstring for cbSdkGetVersion in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkGetVersion(cbSdkVersion *version) const { - memset(version, 0, sizeof(cbSdkVersion)); - version->major = BMI_VERSION_MAJOR; - version->minor = BMI_VERSION_MINOR; - version->release = BMI_VERSION_RELEASE; - version->beta = BMI_VERSION_BETA; - version->majorp = cbVERSION_MAJOR; - version->minorp = cbVERSION_MINOR; - if (m_instInfo == 0) - return CBSDKRESULT_WARNCLOSED; - cbPROCINFO isInfo; - cbRESULT cbRet = cbGetProcInfo(cbNSP1, &isInfo, m_nInstance); - if (cbRet != cbRESULT_OK) - return CBSDKRESULT_UNKNOWN; - version->nspmajor = (isInfo.idcode & 0x000000ff); - version->nspminor = (isInfo.idcode & 0x0000ff00) >> 8; - version->nsprelease = (isInfo.idcode & 0x00ff0000) >> 16; - version->nspbeta = (isInfo.idcode & 0xff000000) >> 24; - version->nspmajorp = (isInfo.version & 0xffff0000) >> 16; - version->nspminorp = (isInfo.version & 0x0000ffff); - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkGetVersion(const uint32_t nInstance, cbSdkVersion *version) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (version == nullptr) - return CBSDKRESULT_NULLPTR; - memset(version, 0, sizeof(cbSdkVersion)); - version->major = BMI_VERSION_MAJOR; - version->minor = BMI_VERSION_MINOR; - version->release = BMI_VERSION_RELEASE; - version->beta = BMI_VERSION_BETA; - version->majorp = cbVERSION_MAJOR; - version->minorp = cbVERSION_MINOR; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_WARNCLOSED; - - return g_app[nInstance]->SdkGetVersion(version); -} - -// Author & Date: Ehsan Azar 12 April 2012 -cbSdkResult cbSdkErrorFromCCFError(const ccf::ccfResult err) -{ - cbSdkResult res = CBSDKRESULT_UNKNOWN; - switch(err) - { - case ccf::CCFRESULT_WARN_VERSION: - case ccf::CCFRESULT_WARN_CONVERT: - res = CBSDKRESULT_WARNCONVERT; - break; - case ccf::CCFRESULT_ERR_FORMAT: - res = CBSDKRESULT_ERRFORMATFILE; - break; - case ccf::CCFRESULT_ERR_OPENFAILEDWRITE: - case ccf::CCFRESULT_ERR_OPENFAILEDREAD: - res = CBSDKRESULT_ERROPENFILE; - break; - case ccf::CCFRESULT_ERR_OFFLINE: - res = CBSDKRESULT_CLOSED; - break; - default: - res = CBSDKRESULT_UNKNOWN; - break; - } - return res; -} - -// Author & Date: Ehsan Azar 10 June 2012 -/** CCF callback function. -* See docstring for cbSdkAsynchCCF in cbsdk.h for more information. -*/ -void SdkApp::SdkAsynchCCF(const ccf::ccfResult res, const LPCSTR szFileName, const cbStateCCF state, const uint32_t nProgress) -{ - cbSdkCCFEvent ev; - ev.result = cbSdkErrorFromCCFError(res); - ev.progress = nProgress; - ev.state = state; - ev.szFileName = szFileName; - - LateBindCallback(CBSDKCALLBACK_ALL); - LateBindCallback(CBSDKCALLBACK_CCF); - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_CCF, &ev, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - if (m_pLateCallback[CBSDKCALLBACK_CCF]) - m_pLateCallback[CBSDKCALLBACK_CCF](m_nInstance, cbSdkPkt_CCF, &ev, m_pLateCallbackParams[CBSDKCALLBACK_CCF]);; -} - -void cbSdkAsynchCCF(const uint32_t nInstance, const ccf::ccfResult res, const LPCSTR szFileName, const cbStateCCF state, const uint32_t nProgress) -{ - if (nInstance >= cbMAXOPEN) - return; - if (g_app[nInstance] == nullptr) - return; - g_app[nInstance]->SdkAsynchCCF(res, szFileName, state, nProgress); -} - -// Author & Date: Ehsan Azar 12 April 2012 -/** Get CCF configuration information from file or NSP. -* See docstring for cbSdkReadCCF in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkReadCCF(cbSdkCCF * pData, const char * szFileName, const bool bConvert, const bool bSend, const bool bThreaded) -{ - memset(&pData->data, 0, sizeof(pData->data)); - pData->ccfver = 0; - if (szFileName == nullptr) - { - // Library must be open to read from NSP - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - } - CCFUtils config(bThreaded, &pData->data, m_pCallback[CBSDKCALLBACK_CCF] ? &cbSdkAsynchCCF : nullptr, m_nInstance); - cbSdkResult res = CBSDKRESULT_SUCCESS; - if (szFileName == nullptr) - res = SdkFetchCCF(pData); - else - { - const ccf::ccfResult ccfRes = config.ReadCCF(szFileName, bConvert); // Updates pData->data via config.m_pImpl->m_data; - if (bSend) - SdkSendCCF(pData, config.IsAutosort()); - res = cbSdkErrorFromCCFError(ccfRes); - } - if (res) - return res; - pData->ccfver = config.GetInternalOriginalVersion(); - if (m_instInfo == 0) - return CBSDKRESULT_WARNCLOSED; - return CBSDKRESULT_SUCCESS; -} - -cbSdkResult SdkApp::SdkFetchCCF(cbSdkCCF * pData) const { - ccf::ccfResult res = ccf::CCFRESULT_SUCCESS; - uint32_t nIdx = cb_library_index[m_nInstance]; - if (!cb_library_initialized[nIdx] || cb_cfg_buffer_ptr[nIdx] == nullptr || cb_cfg_buffer_ptr[nIdx]->sysinfo.cbpkt_header.chid == 0) - res = ccf::CCFRESULT_ERR_OFFLINE; - else { - cbCCF & data = pData->data; - for (int i = 0; i < cbNUM_DIGITAL_FILTERS; ++i) - data.filtinfo[i] = cb_cfg_buffer_ptr[nIdx]->filtinfo[0][cbFIRST_DIGITAL_FILTER + i - 1]; // First is 1 based, but index is 0 based - for (int i = 0; i < cbMAXCHANS; ++i) - data.isChan[i] = cb_cfg_buffer_ptr[nIdx]->chaninfo[i]; - data.isAdaptInfo = cb_cfg_buffer_ptr[nIdx]->adaptinfo[cbNSP1]; - data.isSS_Detect = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktDetect; - data.isSS_ArtifactReject = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktArtifReject; - for (uint32_t i = 0; i < cb_pc_status_buffer_ptr[0]->cbGetNumAnalogChans(); ++i) - data.isSS_NoiseBoundary[i] = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktNoiseBoundary[i]; - data.isSS_Statistics = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktStatistics; - { - data.isSS_Status = cb_cfg_buffer_ptr[nIdx]->isSortingOptions.pktStatus; - data.isSS_Status.cntlNumUnits.fElapsedMinutes = 99; - data.isSS_Status.cntlUnitStats.fElapsedMinutes = 99; - } - { - data.isSysInfo = cb_cfg_buffer_ptr[nIdx]->sysinfo; - // only set spike len and pre trigger len - data.isSysInfo.cbpkt_header.type = cbPKTTYPE_SYSSETSPKLEN; - } - for (int i = 0; i < cbMAXNTRODES; ++i) - data.isNTrodeInfo[i] = cb_cfg_buffer_ptr[nIdx]->isNTrodeInfo[i]; - data.isLnc = cb_cfg_buffer_ptr[nIdx]->isLnc[cbNSP1]; - for (int i = 0; i < AOUT_NUM_GAIN_CHANS; ++i) - { - for (int j = 0; j < cbMAX_AOUT_TRIGGER; ++j) - { - data.isWaveform[i][j] = cb_cfg_buffer_ptr[nIdx]->isWaveform[i][j]; - // Unset triggered state, so that when loading it does not start generating waveform - data.isWaveform[i][j].active = 0; - } - } - } - return cbSdkErrorFromCCFError(res); -} - -CBSDKAPI cbSdkResult cbSdkReadCCF(const uint32_t nInstance, cbSdkCCF * pData, const char * szFileName, const bool bConvert, const bool bSend, const bool bThreaded) -{ - if (pData == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - // TODO: make it possible to convert even with library closed - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkReadCCF(pData, szFileName, bConvert, bSend, bThreaded); -} - -// Author & Date: Ehsan Azar 12 April 2012 -/** Write CCF configuration information to file or send to NSP. -* See docstring for cbSdkWriteCCF in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkWriteCCF(cbSdkCCF * pData, const char * szFileName, const bool bThreaded) -{ - pData->ccfver = 0; - if (szFileName == nullptr) - { - // Library must be open to write to NSP - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - } - cbCCFCallback callbackFn = m_pCallback[CBSDKCALLBACK_CCF] ? &cbSdkAsynchCCF : nullptr; - CCFUtils config(bThreaded, &pData->data, callbackFn, m_nInstance); - // Set proc info on config object. This might be used by Write* operations (if XML). - cbPROCINFO isInfo; - cbGetProcInfo(cbNSP1, &isInfo, m_nInstance); - config.SetProcInfo(isInfo); // Ignore return. It works if XML, and fails otherwise. - - ccf::ccfResult res = ccf::CCFRESULT_SUCCESS; - if (szFileName == nullptr) - { - // No filename, attempt to send CCF to NSP - if (callbackFn) - callbackFn(m_nInstance, res, nullptr, CCFSTATE_SEND, 0); - SdkSendCCF(pData, false); - if (callbackFn) - callbackFn(m_nInstance, res, nullptr, CCFSTATE_SEND, 100); - } - else - res = config.WriteCCFNoPrompt(szFileName); - if (res) - return cbSdkErrorFromCCFError(res); - pData->ccfver = config.GetInternalVersion(); - if (m_instInfo == 0) - return CBSDKRESULT_WARNCLOSED; - return CBSDKRESULT_SUCCESS; -} - -cbSdkResult SdkApp::SdkSendCCF(cbSdkCCF * pData, bool bAutosort) -{ - if (pData == nullptr) - return CBSDKRESULT_NULLPTR; - - cbCCF data = pData->data; - - // Custom digital filters - for (auto & info : data.filtinfo) - { - if (info.filt) - { - info.cbpkt_header.type = cbPKTTYPE_FILTSET; - cbSendPacket(&info, m_nInstance); - } - } - // Chaninfo - int nAinChan = 1; - int nAoutChan = 1; - int nDinChan = 1; - int nSerialChan = 1; - int nDoutChan = 1; - uint32_t nChannelNumber = 0; - for (auto & info : data.isChan) - { - if (info.chan) - { - // this function is supposed to line up channels based on channel capabilities. It doesn't - // work with the multiple NSP setup. TODO look into this at a future time - nChannelNumber = 0; - switch (info.chancaps) - { - case cbCHAN_EXISTS | cbCHAN_CONNECTED | cbCHAN_ISOLATED | cbCHAN_AINP: // FE channels -#ifdef CBPROTO_311 - nChannelNumber = cbGetExpandedChannelNumber(1, data.isChan[info].chan); -#else - nChannelNumber = cbGetExpandedChannelNumber(info.cbpkt_header.instrument + 1, info.chan); -#endif - break; - case cbCHAN_EXISTS | cbCHAN_CONNECTED | cbCHAN_AINP: // Analog input channels - nChannelNumber = GetAIAnalogInChanNumber(nAinChan++); - break; - case cbCHAN_EXISTS | cbCHAN_CONNECTED | cbCHAN_AOUT: // Analog & Audio output channels - nChannelNumber = GetAnalogOrAudioOutChanNumber(nAoutChan++); - break; - case cbCHAN_EXISTS | cbCHAN_CONNECTED | cbCHAN_DINP: // digital & serial input channels - if (info.dinpcaps & cbDINP_SERIALMASK) - { - nChannelNumber = GetSerialChanNumber(nSerialChan++); - } - else - { - nChannelNumber = GetDiginChanNumber(nDinChan++); - } - break; - case cbCHAN_EXISTS | cbCHAN_CONNECTED | cbCHAN_DOUT: // digital output channels - nChannelNumber = GetDigoutChanNumber(nDoutChan++); - break; - default: - nChannelNumber = 0; - } - // send it if it's a valid channel number - if ((0 != nChannelNumber)) // && (data.isChan[info].chan)) - { - info.chan = nChannelNumber; - info.cbpkt_header.type = cbPKTTYPE_CHANSET; -#ifndef CBPROTO_311 - info.cbpkt_header.instrument = info.proc - 1; // send to the correct instrument -#endif - cbSendPacket(&info, m_nInstance); - } - } - } - // Sorting - { - if (data.isSS_Statistics.cbpkt_header.type) - { - data.isSS_Statistics.cbpkt_header.type = cbPKTTYPE_SS_STATISTICSSET; - cbSendPacket(&data.isSS_Statistics, m_nInstance); - } - for (uint32_t nChan = 0; nChan < cb_pc_status_buffer_ptr[0]->cbGetNumAnalogChans(); ++nChan) - { - if (data.isSS_NoiseBoundary[nChan].chan) - { - data.isSS_NoiseBoundary[nChan].cbpkt_header.type = cbPKTTYPE_SS_NOISE_BOUNDARYSET; - cbSendPacket(&data.isSS_NoiseBoundary[nChan], m_nInstance); - } - } - if (data.isSS_Detect.cbpkt_header.type) - { - data.isSS_Detect.cbpkt_header.type = cbPKTTYPE_SS_DETECTSET; - cbSendPacket(&data.isSS_Detect, m_nInstance); - } - if (data.isSS_ArtifactReject.cbpkt_header.type) - { - data.isSS_ArtifactReject.cbpkt_header.type = cbPKTTYPE_SS_ARTIF_REJECTSET; - cbSendPacket(&data.isSS_ArtifactReject, m_nInstance); - } - } - // Sysinfo - if (data.isSysInfo.cbpkt_header.type) - { - data.isSysInfo.cbpkt_header.type = cbPKTTYPE_SYSSETSPKLEN; - cbSendPacket(&data.isSysInfo, m_nInstance); - } - // LNC - if (data.isLnc.cbpkt_header.type) - { - data.isLnc.cbpkt_header.type = cbPKTTYPE_LNCSET; - cbSendPacket(&data.isLnc, m_nInstance); - } - // Analog output waveforms - for (uint32_t nChan = 0; nChan < cb_pc_status_buffer_ptr[0]->cbGetNumAnalogoutChans(); ++nChan) - { - for (int nTrigger = 0; nTrigger < cbMAX_AOUT_TRIGGER; ++nTrigger) - { - if (data.isWaveform[nChan][nTrigger].chan) - { - data.isWaveform[nChan][nTrigger].chan = GetAnalogOutChanNumber(nChan + 1); -#ifndef CBPROTO_311 - data.isWaveform[nChan][nTrigger].cbpkt_header.instrument = cbGetChanInstrument(data.isWaveform[nChan][nTrigger].chan); -#endif - data.isWaveform[nChan][nTrigger].cbpkt_header.type = cbPKTTYPE_WAVEFORMSET; - cbSendPacket(&data.isWaveform[nChan][nTrigger], m_nInstance); - } - } - } - // NTrode - for (int nNTrode = 0; nNTrode < cbMAXNTRODES; ++nNTrode) - { - char szNTrodeLabel[cbLEN_STR_LABEL + 1] = {}; // leave space for trailing null - cbGetNTrodeInfo(nNTrode + 1, szNTrodeLabel, nullptr, nullptr, nullptr, nullptr); - - if ( - (0 != strlen(szNTrodeLabel)) -#ifndef CBPROTO_311 - && (cbGetNTrodeInstrument(nNTrode + 1) == data.isNTrodeInfo->cbpkt_header.instrument + 1) -#endif - ) - { - if (data.isNTrodeInfo[nNTrode].ntrode) - { - data.isNTrodeInfo[nNTrode].cbpkt_header.type = cbPKTTYPE_SETNTRODEINFO; - cbSendPacket(&data.isNTrodeInfo[nNTrode], m_nInstance); - } - } - } - // Adaptive filter - if (data.isAdaptInfo.cbpkt_header.type) - { - data.isAdaptInfo.cbpkt_header.type = cbPKTTYPE_ADAPTFILTSET; - cbSendPacket(&data.isAdaptInfo, m_nInstance); - } - // if any spike sorting packets were read and the protocol is before the combined firmware, - // set all the channels to autosorting - if (CCFUtils config(false, &pData->data, nullptr, m_nInstance); (config.GetInternalVersion() < 8) && !bAutosort) - { - cbPKT_SS_STATISTICS isSSStatistics; - - isSSStatistics.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - isSSStatistics.cbpkt_header.type = cbPKTTYPE_SS_STATISTICSSET; - isSSStatistics.cbpkt_header.dlen = ((sizeof(*this) / 4) - cbPKT_HEADER_32SIZE); - - isSSStatistics.nUpdateSpikes = 300; - isSSStatistics.nAutoalg = cbAUTOALG_HOOPS; - isSSStatistics.nMode = cbAUTOALG_MODE_APPLY; - isSSStatistics.fMinClusterPairSpreadFactor = 9; - isSSStatistics.fMaxSubclusterSpreadFactor = 125; - isSSStatistics.fMinClusterHistCorrMajMeasure = 0.80f; - isSSStatistics.fMaxClusterPairHistCorrMajMeasure = 0.94f; - isSSStatistics.fClusterHistValleyPercentage = 0.50f; - isSSStatistics.fClusterHistClosePeakPercentage = 0.50f; - isSSStatistics.fClusterHistMinPeakPercentage = 0.016f; - isSSStatistics.nWaveBasisSize = 250; - isSSStatistics.nWaveSampleSize = 0; - - cbSendPacket(&isSSStatistics, m_nInstance); - - } - if (data.isSS_Status.cbpkt_header.type) - { - data.isSS_Status.cbpkt_header.type = cbPKTTYPE_SS_STATUSSET; - data.isSS_Status.cntlNumUnits.nMode = ADAPT_NEVER; // Prevent rebuilding spike sorting when loading ccf. - cbSendPacket(&data.isSS_Status, m_nInstance); - } - - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkWriteCCF(const uint32_t nInstance, cbSdkCCF * pData, const char * szFileName, const bool bThreaded) -{ - if (pData == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - /// \todo make it possible to convert even with library closed - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkWriteCCF(pData, szFileName, bThreaded); -} - -// Author & Date: Ehsan Azar 23 Feb 2011 -/** Open cbsdk library. -* See docstring for cbSdkOpen in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkOpen(const uint32_t nInstance, cbSdkConnectionType conType, cbSdkConnection con) -{ - // check if the library is already open - if (m_instInfo != 0) - return CBSDKRESULT_WARNOPEN; - - // Some sanity checks - if (con.szInIP == nullptr || con.szInIP[0] == 0) - { -#ifdef WIN32 - con.szInIP = cbNET_UDP_ADDR_INST; -#elif __APPLE__ - con.szInIP = "255.255.255.255"; -#else - // On Linux bind to bcast - con.szInIP = cbNET_UDP_ADDR_BCAST; -#endif - } - if (con.szOutIP == nullptr || con.szOutIP[0] == 0) - con.szOutIP = cbNET_UDP_ADDR_CNT; - - // Null the trial buffers - m_CD = nullptr; - m_ED = nullptr; - - // Unregister all callbacks - m_lockCallback.lock(); - for (int i = 0; i < CBSDKCALLBACK_COUNT; ++i) - { - m_pCallbackParams[i] = nullptr; - m_pCallback[i] = nullptr; - } - m_lockCallback.unlock(); - - // Unmask all channels - for (bool & mask : m_bChannelMask) - mask = true; - - // open the library and verify that the central application is there - if (conType == CBSDKCONNECTION_DEFAULT || conType == CBSDKCONNECTION_CENTRAL) - { - // Run cbsdk under Central - char buf[64] = {0}; - if (nInstance == 0) - _snprintf(buf, sizeof(buf), "cbSharedDataMutex"); - else - _snprintf(buf, sizeof(buf), "cbSharedDataMutex%d", nInstance); - if (cbCheckApp(buf) == cbRESULT_OK) - { - conType = CBSDKCONNECTION_CENTRAL; - } - else - { - if (conType == CBSDKCONNECTION_CENTRAL) - return CBSDKRESULT_ERROPENCENTRAL; - conType = CBSDKCONNECTION_UDP; // Now try UDP - } - } - - // reset synchronization packet - memset(&m_lastPktVideoSynch, 0, sizeof(m_lastPktVideoSynch)); - - // reset the trial begin/end notification variables. - m_uTrialBeginChannel = 0; - m_uTrialBeginMask = 0; - m_uTrialBeginValue = 0; - m_uTrialEndChannel = 0; - m_uTrialEndMask = 0; - m_uTrialEndValue = 0; - m_uTrialWaveforms = 0; - m_uTrialConts = cbSdk_CONTINUOUS_DATA_SAMPLES; - m_uTrialEvents = cbSdk_EVENT_DATA_SAMPLES; - m_uTrialComments = 0; - m_uTrialTrackings = 0; - - // make sure that cache data storage is switched off so that the monitoring thread will - // not be saving data and then set up the cache control variables - m_bWithinTrial = false; - m_uTrialStartTime = 0; - m_nextTrialStartTime = 0; - - std::unique_lock connectLock(m_connectLock); - - if (conType == CBSDKCONNECTION_UDP) - { - Open(nInstance, con.nInPort, con.nOutPort, con.szInIP, con.szOutIP, con.nRecBufSize, con.nRange); - } - else if (conType == CBSDKCONNECTION_CENTRAL) - { - Open(nInstance); - } - else - return CBSDKRESULT_NOTIMPLEMENTED; - - // Wait for (dis)connection to happen (wait forever if debug) - bool bWait; -#ifndef NDEBUG - m_connectWait.wait(connectLock); - bWait = true; -#else - bWait = (m_connectWait.wait_for(connectLock, std::chrono::milliseconds(15000)) == std::cv_status::no_timeout); -#endif - - if (!bWait) - return CBSDKRESULT_TIMEOUT; - if (IsStandAlone()) - { - if (m_instInfo == 0) - { - cbSdkResult sdkres = CBSDKRESULT_UNKNOWN; - // Try to make sense of the error - switch (GetLastCbErr()) - { - case cbRESULT_NOCENTRALAPP: - sdkres = CBSDKRESULT_ERROPENCENTRAL; - break; - case cbRESULT_SOCKERR: - sdkres = CBSDKRESULT_ERROPENUDPPORT; - break; - case cbRESULT_SOCKOPTERR: - sdkres = CBSDKRESULT_OPTERRUDP; - break; - case cbRESULT_SOCKMEMERR: - sdkres = CBSDKRESULT_MEMERRUDP; - break; - case cbRESULT_INSTINVALID: - sdkres = CBSDKRESULT_INVALIDINST; - break; - case cbRESULT_SOCKBIND: - sdkres = CBSDKRESULT_ERROPENUDP; - break; - case cbRESULT_LIBINITERROR: - case cbRESULT_EVSIGERR: - sdkres = CBSDKRESULT_ERRINIT; - break; - case cbRESULT_SYSLOCK: - sdkres = CBSDKRESULT_BUSY; - break; - case cbRESULT_BUFRECALLOCERR: - case cbRESULT_BUFGXMTALLOCERR: - case cbRESULT_BUFLXMTALLOCERR: - case cbRESULT_BUFCFGALLOCERR: - case cbRESULT_BUFPCSTATALLOCERR: - case cbRESULT_BUFSPKALLOCERR: - sdkres = CBSDKRESULT_ERRMEMORY; - break; - case cbRESULT_INSTOUTDATED: - sdkres = CBSDKRESULT_INSTOUTDATED; - break; - case cbRESULT_LIBOUTDATED: - sdkres = CBSDKRESULT_LIBOUTDATED; - break; - case cbRESULT_OK: - sdkres = CBSDKRESULT_ERROFFLINE; - break; - default: sdkres = CBSDKRESULT_UNKNOWN; - } - return sdkres; - } - } - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkOpen(const uint32_t nInstance, const cbSdkConnectionType conType, const cbSdkConnection &con) -{ - // check if the library is already open - if (conType < 0 || conType >= CBSDKCONNECTION_CLOSED) - return CBSDKRESULT_INVALIDPARAM; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - { - try { - g_app[nInstance] = new SdkApp(); - } catch (...) { - g_app[nInstance] = nullptr; - } - } - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_ERRMEMORY; - - return g_app[nInstance]->SdkOpen(nInstance, conType, con); -} - -// Author & Date: Ehsan Azar 24 Feb 2011 -/** Close cbsdk library. -* See docstring for cbSdkClose in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkClose() -{ - cbSdkResult res = CBSDKRESULT_SUCCESS; - if (m_instInfo == 0) - res = CBSDKRESULT_WARNCLOSED; - - // clear the data cache if it exists - // first disable writing to the cache - m_bWithinTrial = false; - - if (m_CD != nullptr) - SdkUnsetTrialConfig(CBSDKTRIAL_CONTINUOUS); - - if (m_ED != nullptr) - SdkUnsetTrialConfig(CBSDKTRIAL_EVENTS); - - if (m_CMT != nullptr) - SdkUnsetTrialConfig(CBSDKTRIAL_COMMENTS); - - if (m_TR != nullptr) - SdkUnsetTrialConfig(CBSDKTRIAL_TRACKING); - - // Unregister all callbacks - m_lockCallback.lock(); - for (int i = 0; i < CBSDKCALLBACK_COUNT; ++i) - { - m_pCallbackParams[i] = nullptr; - m_pCallback[i] = nullptr; - } - m_lockCallback.unlock(); - - // Close the app - Close(); - - return res; -} - -CBSDKAPI cbSdkResult cbSdkClose(const uint32_t nInstance) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_WARNCLOSED; - - const cbSdkResult res = g_app[nInstance]->SdkClose(); - - // Delete this instance - delete g_app[nInstance]; - g_app[nInstance] = nullptr; - - /// \todo see if this is necessary and useful before removing -#if 0 - // Close application if this is the last - if (QAppPriv::pApp) - { - bool bLastInstance = false; - for (int i = 0; i < cbMAXOPEN; ++i) - { - if (g_app[i] != nullptr) - { - bLastInstance = true; - break; - } - } - if (bLastInstance) - { - delete QAppPriv::pApp; - QAppPriv::pApp = nullptr; - } - } -#endif - - return res; -} - -// Author & Date: Ehsan Azar 24 Feb 2011 -/** Get cbsdk connection and instrument type. -* See docstring for cbSdkGetType in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkGetType(cbSdkConnectionType * conType, cbSdkInstrumentType * instType) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - if (instType) - { - uint32_t instInfo = 0; - if (cbGetInstInfo(cbNSP1, &instInfo, m_nInstance) == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (instInfo & cbINSTINFO_NPLAY) - { - if (instInfo & cbINSTINFO_LOCAL) - *instType = CBSDKINSTRUMENT_NPLAY; - else - *instType = CBSDKINSTRUMENT_REMOTENPLAY; - } - else - { - if (instInfo & cbINSTINFO_LOCAL) - *instType = CBSDKINSTRUMENT_LOCALNSP; - else - *instType = CBSDKINSTRUMENT_NSP; - } - } - - if (conType) - { - *conType = CBSDKCONNECTION_CLOSED; - if (IsStandAlone()) - *conType = CBSDKCONNECTION_UDP; - else if (m_instInfo != 0) - *conType = CBSDKCONNECTION_CENTRAL; - } - - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkGetType(const uint32_t nInstance, cbSdkConnectionType * conType, cbSdkInstrumentType * instType) -{ - if (instType == nullptr && conType == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetType(conType, instType); -} - -// Author & Date: Ehsan Azar 25 Oct 2011 -/** Internal lock-less function to deallocate given trial construct. -* -* \nThis function returns the error code -*/ -cbSdkResult SdkApp::unsetTrialConfig(const cbSdkTrialType type) -{ - switch (type) - { - case CBSDKTRIAL_CONTINUOUS: - if (m_CD == nullptr) - return CBSDKRESULT_ERRCONFIG; - // Cleanup all groups - m_CD->cleanup(); - // Delete the structure itself - delete m_CD; - m_CD = nullptr; - m_uTrialConts = 0; - break; - case CBSDKTRIAL_EVENTS: - if (m_ED == nullptr) - return CBSDKRESULT_ERRCONFIG; - // TODO: Go back to using cbNUM_ANALOG_CHANS + 2 after we have m_ChIdxInType - // Cleanup waveform data if dynamically allocated - if (m_ED->getWaveformData() != nullptr) - { - if (m_uTrialWaveforms > cbPKT_SPKCACHEPKTCNT) - delete[] m_ED->getWaveformData(); - } - delete m_ED; - m_ED = nullptr; - break; - case CBSDKTRIAL_COMMENTS: - if (m_CMT == nullptr) - return CBSDKRESULT_ERRCONFIG; - if (m_CMT->timestamps) - { - delete[] m_CMT->timestamps; - m_CMT->timestamps = nullptr; - } - if (m_CMT->rgba) - { - delete[] m_CMT->rgba; - m_CMT->rgba = nullptr; - } - if (m_CMT->charset) - { - delete[] m_CMT->charset; - m_CMT->charset = nullptr; - } - if (m_CMT->comments) - { - for (uint32_t i = 0; i < m_CMT->size; ++i) - { - if (m_CMT->comments[i]) - { - delete[] m_CMT->comments[i]; - m_CMT->comments[i] = nullptr; - } - } - delete[] m_CMT->comments; - m_CMT->comments = nullptr; - } - m_CMT->size = 0; - delete m_CMT; - m_CMT = nullptr; - break; - case CBSDKTRIAL_TRACKING: - if (m_TR == nullptr) - return CBSDKRESULT_ERRCONFIG; - for (uint32_t i = 0; i < cbMAXTRACKOBJ; ++i) - { - if (m_TR->timestamps[i]) - { - delete[] m_TR->timestamps[i]; - m_TR->timestamps[i] = nullptr; - } - if (m_TR->synch_timestamps[i]) - { - delete[] m_TR->synch_timestamps[i]; - m_TR->synch_timestamps[i] = nullptr; - } - if (m_TR->synch_frame_numbers[i]) - { - delete[] m_TR->synch_frame_numbers[i]; - m_TR->synch_frame_numbers[i] = nullptr; - } - if (m_TR->point_counts[i]) - { - delete[] m_TR->point_counts[i]; - m_TR->point_counts[i] = nullptr; - } - if (m_TR->coords[i]) - { - for (uint32_t j = 0; j < m_TR->size; ++j) - { - if (m_TR->coords[i][j]) - { - delete[] static_cast(m_TR->coords[i][j]); - m_TR->coords[i][j] = nullptr; - } - } - delete[] m_TR->coords[i]; - m_TR->coords[i] = nullptr; - } - } // end for (uint32_t i = 0 - m_TR->size = 0; - delete m_TR; - m_TR = nullptr; - break; - } - return CBSDKRESULT_SUCCESS; -} - -// Author & Date: Ehsan Azar 25 Oct 2011 -/** Deallocate given trial construct. -* See docstring for cbSdkUnsetTrialConfig in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkUnsetTrialConfig(const cbSdkTrialType type) -{ - cbSdkResult res = CBSDKRESULT_SUCCESS; - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - switch (type) - { - case CBSDKTRIAL_CONTINUOUS: - m_lockTrial.lock(); - res = unsetTrialConfig(type); - m_lockTrial.unlock(); - break; - case CBSDKTRIAL_EVENTS: - m_lockTrialEvent.lock(); - res = unsetTrialConfig(type); - m_lockTrialEvent.unlock(); - break; - case CBSDKTRIAL_COMMENTS: - m_lockTrialComment.lock(); - res = unsetTrialConfig(type); - m_lockTrialComment.unlock(); - break; - case CBSDKTRIAL_TRACKING: - m_lockTrialTracking.lock(); - res = unsetTrialConfig(type); - m_lockTrialTracking.unlock(); - break; - } - return res; -} - -CBSDKAPI cbSdkResult cbSdkUnsetTrialConfig(const uint32_t nInstance, const cbSdkTrialType type) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkUnsetTrialConfig(type); -} - -// Author & Date: Ehsan Azar 24 Feb 2011 -/** Get time from instrument. -* See docstring for cbSdkGetTime in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkGetTime(PROCTIME * cbtime) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - *cbtime = m_uCbsdkTime; - if (cbGetSystemClockTime(cbtime, m_nInstance) != cbRESULT_OK) - return CBSDKRESULT_CLOSED; - - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkGetTime(const uint32_t nInstance, PROCTIME * cbtime) -{ - if (cbtime == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetTime(cbtime); -} - -// Author & Date: Ehsan Azar 29 April 2012 -/** Get direct access to spike cache shared memory. -* See docstring for cbSdkGetSpkCache in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkGetSpkCache(const uint16_t channel, cbSPKCACHE **cache) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (cbGetSpkCache(channel, cache, m_nInstance) != cbRESULT_OK) - return CBSDKRESULT_CLOSED; - - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkGetSpkCache(const uint32_t nInstance, const uint16_t channel, cbSPKCACHE **cache) -{ - if (cache == nullptr) - return CBSDKRESULT_NULLPTR; - if (*cache == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetSpkCache(channel, cache); -} - -// Author & Date: Ehsan Azar 25 June 2012 -/** Get information about configured data collection trial and its active status. -* See docstring for cbSdkGetTrialConfig in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkGetTrialConfig(uint32_t * pbActive, uint16_t * pBegchan, uint32_t * pBegmask, uint32_t * pBegval, - uint16_t * pEndchan, uint32_t * pEndmask, uint32_t * pEndval, - uint32_t * puWaveforms, uint32_t * puConts, uint32_t * puEvents, - uint32_t * puComments, uint32_t * puTrackings) const -{ - if (pbActive) - *pbActive = m_bWithinTrial; - if (pBegchan) - *pBegchan = m_uTrialBeginChannel; - if (pBegmask) - *pBegmask = m_uTrialBeginMask; - if (pBegval) - *pBegval = m_uTrialBeginValue; - if (pEndchan) - *pEndchan = m_uTrialEndChannel; - if (pEndmask) - *pEndmask = m_uTrialEndMask; - if (pEndval) - *pEndval = m_uTrialEndValue; - if (puWaveforms) - *puWaveforms = m_uTrialWaveforms; - if (puConts) - *puConts = m_uTrialConts; - if (puEvents) - *puEvents = m_uTrialEvents; - if (puComments) - *puComments = m_uTrialComments; - if (puTrackings) - *puTrackings = m_uTrialTrackings; - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkGetTrialConfig(const uint32_t nInstance, - uint32_t * pbActive, uint16_t * pBegchan, uint32_t * pBegmask, uint32_t * pBegval, - uint16_t * pEndchan, uint32_t * pEndmask, uint32_t * pEndval, - uint32_t * puWaveforms, uint32_t * puConts, uint32_t * puEvents, - uint32_t * puComments, uint32_t * puTrackings) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetTrialConfig(pbActive, pBegchan, pBegmask, pBegval, - pEndchan, pEndmask, pEndval, - puWaveforms, puConts, puEvents, - puComments, puTrackings); -} - -// Author & Date: Ehsan Azar 25 Feb 2011 -/** Allocate buffers and start a data collection trial. -* See docstring for cbSdkSetTrialConfig in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkSetTrialConfig(const uint32_t bActive, const uint16_t begchan, const uint32_t begmask, const uint32_t begval, - const uint16_t endchan, const uint32_t endmask, const uint32_t endval, - const uint32_t uWaveforms, const uint32_t uConts, const uint32_t uEvents, const uint32_t uComments, const uint32_t uTrackings) -{ - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - // load the values into the global trial control variables for use by the - // real-time data monitoring thread. Trim them to 8-bit and 16-bit values - m_uTrialBeginChannel = begchan; - m_uTrialBeginMask = begmask; - m_uTrialBeginValue = begval; - m_uTrialEndChannel = endchan; - m_uTrialEndMask = endmask; - m_uTrialEndValue = endval; - m_uTrialWaveforms = uWaveforms; - - if (uConts && m_CD == nullptr) - { - m_lockTrial.lock(); - try { - m_CD = new ContinuousData; - m_CD->default_size = uConts; - // Groups are initialized by their constructors - // Buffers are allocated lazily in OnPktGroup when first packet arrives - } catch (...) { - if (m_CD) - { - delete m_CD; - m_CD = nullptr; - } - } - m_lockTrial.unlock(); - - if (m_CD == nullptr) - return CBSDKRESULT_ERRMEMORYTRIAL; - m_uTrialConts = uConts; - } - - if (uEvents && m_ED == nullptr) - { - m_lockTrialEvent.lock(); - try { - m_ED = new EventData; - } catch (...) { - m_ED = nullptr; - } - if (m_ED) - { - // Allocate buffers for event data - if (!m_ED->allocate(uEvents)) - { - delete m_ED; - m_ED = nullptr; - } - } - m_lockTrialEvent.unlock(); - if (m_ED == nullptr) - return CBSDKRESULT_ERRMEMORYTRIAL; - m_uTrialEvents = uEvents; - } - - if (uComments && m_CMT == nullptr) - { - m_lockTrialComment.lock(); - try { - m_CMT = new CommentData; - } catch (...) { - m_CMT = nullptr; - } - if (m_CMT) - { - m_CMT->size = uComments; - bool bErr = false; - m_CMT->timestamps = new PROCTIME[m_CMT->size]; - m_CMT->rgba = new uint32_t[m_CMT->size]; - m_CMT->charset = new uint8_t[m_CMT->size]; - m_CMT->comments = new uint8_t * [m_CMT->size]; // uint8_t * array[m_CMT->size] - if (m_CMT->timestamps == nullptr || m_CMT->rgba == nullptr || m_CMT->charset == nullptr || m_CMT->comments == nullptr) - bErr = true; - try { - if (!bErr) - { - for (uint32_t i = 0; i < m_CMT->size; ++i) - { - m_CMT->comments[i] = new uint8_t[cbMAX_COMMENT + 1]; - if (m_CMT->comments[i] == nullptr) - { - bErr = true; - break; - } - } - } - } catch (...) { - bErr = true; - } - if (bErr) - unsetTrialConfig(CBSDKTRIAL_COMMENTS); - } - if (m_CMT) m_CMT->reset(); - m_lockTrialComment.unlock(); - if (m_CMT == nullptr) - return CBSDKRESULT_ERRMEMORYTRIAL; - m_uTrialComments = uComments; - } - - if (uTrackings && m_TR == nullptr) - { - m_lockTrialTracking.lock(); - try { - m_TR = new TrackingData; - } catch (...) { - m_TR = nullptr; - } - if (m_TR) - { - m_TR->size = uTrackings; - bool bErr = false; - for (uint32_t i = 0; i < cbMAXTRACKOBJ; ++i) - { - try { - m_TR->timestamps[i] = new PROCTIME[m_TR->size]; - m_TR->synch_timestamps[i] = new uint32_t[m_TR->size]; - m_TR->synch_frame_numbers[i] = new uint32_t[m_TR->size]; - m_TR->point_counts[i] = new uint16_t[m_TR->size]; - m_TR->coords[i] = new void * [m_TR->size]; - - if (m_TR->timestamps[i] == nullptr || m_TR->synch_timestamps[i] == nullptr || m_TR->synch_frame_numbers[i] == nullptr || - m_TR->point_counts[i]== nullptr || m_TR->coords[i] == nullptr) - bErr = true; - - if (!bErr) - { - for (uint32_t j = 0; j < m_TR->size; ++j) - { - // This is equivalent to uint32_t[cbMAX_TRACKCOORDS/2] used for word-size union - m_TR->coords[i][j] = new uint16_t[cbMAX_TRACKCOORDS]; - if (m_TR->coords[i][j] == nullptr) - { - bErr = true; - break; - } - } - } - } catch (...) { - bErr = true; - } - if (bErr) - { - unsetTrialConfig(CBSDKTRIAL_TRACKING); - break; - } - } - } - if (m_TR) m_TR->reset(); - m_lockTrialTracking.unlock(); - if (m_TR == nullptr) - return CBSDKRESULT_ERRMEMORYTRIAL; - m_uTrialTrackings = uTrackings; - } - - if (m_ED) - { - if (uWaveforms && m_ED->getWaveformData() == nullptr) - { - if (uWaveforms > m_ED->getSize()) - return CBSDKRESULT_ERRMEMORYTRIAL; - /// \todo implement using cache - return CBSDKRESULT_NOTIMPLEMENTED; - } - } - else if (uWaveforms > cbPKT_SPKCACHEPKTCNT) - return CBSDKRESULT_INVALIDPARAM; // Cannot cache waveforms if no cache is available - - // get the trial status, if zero, set the WithinTrial flag to off - if (!bActive) - { - m_bWithinTrial = false; - } - else - { // otherwise set the status to true - // reset the trial data cache if WithinTrial is currently False - if (!m_bWithinTrial) - { - cbGetSystemClockTime(&m_uTrialStartTime, m_nInstance); - m_nextTrialStartTime = m_uTrialStartTime; - - if (m_CD) - { - // Clear continuous data indices for all groups - m_lockTrial.lock(); - for (auto& grp : m_CD->groups) - { - grp.setWriteIndex(0); - grp.setWriteStartIndex(0); - } - m_lockTrial.unlock(); - } - - if (m_ED) - { - // Clear event data array - m_lockTrialEvent.lock(); - m_ED->reset(); - m_lockTrialEvent.unlock(); - } - - if (m_CMT) - { - // Clear comment data array - m_lockTrialComment.lock(); - m_CMT->write_index = 0; - m_CMT->write_start_index = 0; - m_lockTrialComment.unlock(); - } - - if (m_TR) - { - // Clear tracking data array - m_lockTrialTracking.lock(); - memset(m_TR->write_index, 0, sizeof(m_TR->write_index)); - memset(m_TR->write_start_index, 0, sizeof(m_TR->write_start_index)); - m_lockTrialTracking.unlock(); - } - } - m_bWithinTrial = true; - } - - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkSetTrialConfig(const uint32_t nInstance, - const uint32_t bActive, const uint16_t begchan, const uint32_t begmask, const uint32_t begval, - const uint16_t endchan, const uint32_t endmask, const uint32_t endval, - const uint32_t uWaveforms, const uint32_t uConts, const uint32_t uEvents, const uint32_t uComments, const uint32_t uTrackings) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetTrialConfig(bActive, begchan, begmask, begval, - endchan, endmask, endval, - uWaveforms, uConts, uEvents, uComments, uTrackings); -} - -// Author & Date: Ehsan Azar 25 Feb 2011 -/** Get channel label for a given channel -* See docstring for cbSdkGetChannelLabel in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkGetChannelLabel(const uint16_t channel, uint32_t * bValid, char * label, uint32_t * userflags, int32_t * position) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - if (label) memset(label, 0, cbLEN_STR_LABEL); - if (cbGetChanLabel(channel, label, userflags, position, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - if (bValid) - { - for (int i = 0; i < 6; ++i) - bValid[i] = 0; - cbHOOP hoops[cbMAXUNITS][cbMAXHOOPS]; - if (channel <= cb_pc_status_buffer_ptr[0]->cbGetNumAnalogChans()) - { - cbGetAinpSpikeHoops(channel, &hoops[0][0], m_nInstance); - bValid[0] = IsSpikeProcessingEnabled(channel); - for (int i = 0; i < cbMAXUNITS; ++i) - bValid[i + 1] = hoops[i][0].valid; - } - else if (IsChanDigin(channel) || IsChanSerial(channel)) - { - uint32_t options; - cbGetDinpOptions(channel, &options, nullptr, m_nInstance); - bValid[0] = (options != 0); - } - } - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkGetChannelLabel(const uint32_t nInstance, - const uint16_t channel, uint32_t * bValid, char * label, uint32_t * userflags, int32_t * position) -{ - if (channel == 0 || channel > cbMAXCHANS) - return CBSDKRESULT_INVALIDCHANNEL; - if (label == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetChannelLabel(channel, bValid, label, userflags, position); -} - -// Author & Date: Ehsan Azar 21 March 2011 -/** Set channel label for a given channel. -* See docstring for cbSdkSetChannelLabel in cbsdk.h for more information. -*/ - -cbSdkResult SdkApp::SdkSetChannelLabel(const uint16_t channel, const char * label, const uint32_t userflags, const int32_t * position) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - if (cbSetChanLabel(channel, label, userflags, position, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkSetChannelLabel(const uint32_t nInstance, - const uint16_t channel, const char * label, const uint32_t userflags, const int32_t * position) -{ - if (channel == 0 || channel > cbMAXCHANS) - return CBSDKRESULT_INVALIDCHANNEL; - if (label == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetChannelLabel(channel, label, userflags, position); -} - -CBSDKAPI cbSdkResult cbSdkIsChanAnalogIn(const uint32_t nInstance, const uint16_t channel, uint32_t* bResult) -{ - *bResult = IsChanAnalogIn(channel, nInstance); - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkIsChanAnyDigIn(const uint32_t nInstance, const uint16_t channel, uint32_t* bResult) -{ - *bResult = IsChanAnyDigIn(channel, nInstance); - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkIsChanSerial(const uint32_t nInstance, const uint16_t channel, uint32_t* bResult) -{ - *bResult = IsChanSerial(channel, nInstance); - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkIsChanCont(const uint32_t nInstance, const uint16_t channel, uint32_t* bResult) -{ - *bResult = IsChanCont(channel, nInstance); - return CBSDKRESULT_SUCCESS; -} - -/* -bool IsChanFEAnalogIn(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanAIAnalogIn(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanSerial(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanDigin(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanDigout(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanAnalogOut(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -bool IsChanAudioOut(uint32_t dwChan, uint32_t nInstance = 0); // TRUE means yes; FALSE, no -*/ - - -// Author & Date: Ehsan Azar 11 March 2011 -/** Retrieve data of a configured trial. -See cbSdkGetTrialData docstring in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkGetTrialData(const uint32_t bSeek, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking) -{ - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - if (trialcont) - { - if (m_CD == nullptr) - return CBSDKRESULT_ERRCONFIG; - - // Set trial start time - trialcont->trial_start_time = m_uTrialStartTime; - - // Validate group index - const uint32_t g = trialcont->group; - if (g >= cbMAXGROUPS) - return CBSDKRESULT_INVALIDPARAM; - - // Use thread-safe read method (handles locking internally) - uint32_t num_samples = trialcont->num_samples; - const bool success = m_CD->readSamples(g - 1, - trialcont->samples, - trialcont->timestamps, - num_samples, - bSeek != 0); - - if (!success) - return CBSDKRESULT_ERRCONFIG; - - trialcont->num_samples = num_samples; - } - - if (trialevent) - { - if (m_ED == nullptr) - return CBSDKRESULT_ERRCONFIG; - - // Set trial start time - trialevent->trial_start_time = m_uTrialStartTime; - - // Lock for reading - m_lockTrialEvent.lock(); - - // Read all events from the buffer - much simpler with flat layout! - const uint32_t num_read = m_ED->readEvents( - trialevent->timestamps, - trialevent->channels, - trialevent->units, - trialevent->num_events, // max events to read - bSeek // update read position? - ); - - trialevent->num_events = num_read; // Update with actual count - - m_lockTrialEvent.unlock(); - } - - if (trialcomment) - { - if (m_CMT == nullptr) - return CBSDKRESULT_ERRCONFIG; - - // Set trial start time - trialcomment->trial_start_time = m_uTrialStartTime; - - // Take a snapshot - uint32_t read_start_index = m_CMT->write_start_index; - m_lockTrialComment.lock(); - const uint32_t read_end_index = m_CMT->write_index; - m_lockTrialComment.unlock(); - - // copy the data from the "cache" to the allocated memory. - uint32_t read_index = m_CMT->write_start_index; - auto num_samples = read_end_index - read_index; - if (num_samples < 0) - num_samples += m_CMT->size; - // See which one finishes first - num_samples = std::min(static_cast(num_samples), trialcomment->num_samples); - // retrieved number of samples - trialcomment->num_samples = num_samples; - - for (int i = 0; i < num_samples; ++i) - { - // Null means ignore - if (PROCTIME * p_ts = trialcomment->timestamps) - *(p_ts + i) = m_CMT->timestamps[read_index]; - if (uint32_t * p_rgba = trialcomment->rgbas) - *(p_rgba + i) = m_CMT->rgba[read_index]; - if (uint8_t * p_charset = trialcomment->charsets) - *(p_charset + i) = m_CMT->charset[read_index]; - - // Must take a copy because it might get overridden - if (trialcomment->comments != nullptr && trialcomment->comments[i] != nullptr) - strncpy(reinterpret_cast(trialcomment->comments[i]), reinterpret_cast(m_CMT->comments[read_index]), cbMAX_COMMENT); - - read_index++; - if (read_index >= m_CMT->size) - read_index = 0; - } - if (bSeek) - { - read_start_index = read_index; - m_lockTrialComment.lock(); - m_CMT->write_start_index = read_start_index; - m_lockTrialComment.unlock(); - } - } - - if (trialtracking) - { - uint32_t read_end_index[cbMAXTRACKOBJ]; - uint32_t read_start_index[cbMAXTRACKOBJ]; - if (m_TR == nullptr) - return CBSDKRESULT_ERRCONFIG; - - // Set trial start time - trialtracking->trial_start_time = m_uTrialStartTime; - - // Take a snapshot - memcpy(read_start_index, m_TR->write_start_index, sizeof(m_TR->write_start_index)); - m_lockTrialTracking.lock(); - memcpy(read_end_index, m_TR->write_index, sizeof(m_TR->write_index)); - m_lockTrialTracking.unlock(); - - // copy the data from the "cache" to the allocated memory. - for (uint16_t idx = 0; idx < trialtracking->count; ++idx) - { - const uint16_t id = trialtracking->ids[idx]; // actual ID - - auto read_index = m_TR->write_start_index[id]; - auto num_samples = read_end_index[id] - read_index; - if (num_samples < 0) - num_samples += m_TR->size; - - // See which one finishes first. Do the ternary expression twice to minimize casting. - trialtracking->num_samples[id] = trialtracking->num_samples[id] <= num_samples ? trialtracking->num_samples[id] : static_cast(num_samples); - num_samples = num_samples <= trialtracking->num_samples[id] ? num_samples : static_cast(trialtracking->num_samples[id]); - - bool bWordData = false; - int dim_count = 2; // number of dimensions for each point - switch(m_TR->node_type[id]) - { - case cbTRACKOBJ_TYPE_2DMARKERS: - case cbTRACKOBJ_TYPE_2DBLOB: - case cbTRACKOBJ_TYPE_2DBOUNDARY: - dim_count = 2; - break; - case cbTRACKOBJ_TYPE_1DSIZE: - bWordData = true; - dim_count = 1; - break; - default: - dim_count = 3; - break; - } - - for (int i = 0; i < num_samples; ++i) - { - { - if (PROCTIME * p_ts = trialtracking->timestamps[id]) - *(p_ts + i) = m_TR->timestamps[id][read_index]; - } - { - if (uint32_t * p_synch_ts = trialtracking->synch_timestamps[id]) - *(p_synch_ts + i) = m_TR->synch_timestamps[id][read_index]; - if (uint32_t * p_synch_fn = trialtracking->synch_frame_numbers[id]) - *(p_synch_fn + i) = m_TR->synch_frame_numbers[id][read_index]; - } - { - const uint16_t pointCount = std::min(m_TR->point_counts[id][read_index], m_TR->max_point_counts[id]); - if (uint16_t * p_points = trialtracking->point_counts[id]) - *(p_points + i) = pointCount; - if (trialtracking->coords[id]) - { - if (bWordData) - { - if (auto * coordsptr = static_cast(trialtracking->coords[id][i])) - memcpy(coordsptr, m_TR->coords[id][read_index], pointCount * dim_count * sizeof(uint32_t)); - } - else - { - if (auto * coordsptr = static_cast(trialtracking->coords[id][i])) - memcpy(coordsptr, m_TR->coords[id][read_index], pointCount * dim_count * sizeof(uint16_t)); - } - } - } - read_index++; - if (read_index >= m_TR->size) - read_index = 0; - } - // Flush the buffer and start a new 'trial'... - if (bSeek) - read_start_index[id] = read_index; - } - - if (bSeek) - { - m_lockTrialTracking.lock(); - memcpy(m_TR->write_start_index, read_start_index, sizeof(read_start_index)); - m_lockTrialTracking.unlock(); - } - } - - // If InitTrial had bResetClock, then the time of Init becomes the _next_ trial start time. - // Otherwise, this value will not have changed. - m_uTrialStartTime = m_nextTrialStartTime; - - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkGetTrialData(const uint32_t nInstance, - const uint32_t bSeek, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - // Note: bActive is now interpreted as bSeek for consistency - // bActive=1 means advance read pointer (seek), bActive=0 means just peek - return g_app[nInstance]->SdkGetTrialData(bSeek, trialevent, trialcont, trialcomment, trialtracking); -} - -// Author & Date: Ehsan Azar 22 March 2011 -/** Initialize the structures. -* See cbSdkInitTrialData docstring in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkInitTrialData(const uint32_t bResetClock, cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking, const unsigned long wait_for_comment_msec) -{ - // Optionally get the current time as the next trial start time. - if (bResetClock) - cbGetSystemClockTime(&m_nextTrialStartTime, m_nInstance); - - if (trialevent) - { - trialevent->num_events = 0; - if (m_instInfo == 0) - return CBSDKRESULT_WARNCLOSED; - if (m_ED == nullptr) - return CBSDKRESULT_ERRCONFIG; - - // Wait for packets to come in - m_lockGetPacketsEvent.lock(); - m_bPacketsEvent = true; - m_lockGetPacketsEvent.unlock(); - - // Lock for reading - m_lockTrialEvent.lock(); - - // With flat layout, just report total number of events available - trialevent->num_events = m_ED->getNumEvents(); - - m_lockTrialEvent.unlock(); - } - if (trialcont) - { - trialcont->count = 0; - trialcont->num_samples = 0; - - if (m_instInfo == 0) - { - memset(trialcont->chan, 0, sizeof(trialcont->chan)); - return CBSDKRESULT_WARNCLOSED; - } - - if (m_CD == nullptr) - return CBSDKRESULT_ERRCONFIG; - - // Validate group index (user must set this before calling Init) - const uint32_t g = trialcont->group; - if (g >= cbMAXGROUPS) - return CBSDKRESULT_INVALIDPARAM; - - // Take a thread-safe snapshot of the group state - GroupSnapshot snapshot{}; - if (!m_CD->snapshotForReading(g - 1, snapshot)) - return CBSDKRESULT_INVALIDPARAM; - - if (!snapshot.is_allocated) - { - // Group not allocated - return success with count=0 - memset(trialcont->chan, 0, sizeof(trialcont->chan)); - return CBSDKRESULT_SUCCESS; - } - - // Populate trial structure with group info from snapshot - trialcont->count = snapshot.num_channels; - trialcont->num_samples = snapshot.num_samples; - - // Copy channel IDs from the group (thread-safe through ContinuousData::m_mutex) - const auto& grp = m_CD->groups[g - 1]; - const uint16_t* chan_ids = grp.getChannelIds(); - memcpy(trialcont->chan, chan_ids, snapshot.num_channels * sizeof(uint16_t)); - } - if (trialcomment) - { - trialcomment->num_samples = 0; - if (m_instInfo == 0) - return CBSDKRESULT_WARNCLOSED; - if (m_CMT == nullptr) - return CBSDKRESULT_ERRCONFIG; - - // Wait for packets to come in - { - std::unique_lock lock(m_lockGetPacketsCmt); - m_bPacketsCmt = true; - m_waitPacketsCmt.wait_for(lock, std::chrono::milliseconds(wait_for_comment_msec)); - } - - // Take a snapshot of the current write pointer - m_lockTrialComment.lock(); - const uint32_t read_end_index = m_CMT->write_index; - const uint32_t read_index = m_CMT->write_start_index; - m_lockTrialComment.unlock(); - auto num_samples = read_end_index - read_index; - if (num_samples < 0) - num_samples += m_CMT->size; - if (num_samples) - trialcomment->num_samples = num_samples; - } - if (trialtracking) - { - trialtracking->count = 0; - memset(trialtracking->num_samples, 0, sizeof(trialtracking->num_samples)); - if (m_instInfo == 0) - { - memset(trialtracking->ids, 0, sizeof(trialtracking->ids)); - memset(trialtracking->max_point_counts, 0, sizeof(trialtracking->max_point_counts)); - memset(trialtracking->types, 0, sizeof(trialtracking->types)); - memset(trialtracking->names, 0, sizeof(trialtracking->names)); - return CBSDKRESULT_WARNCLOSED; - } - if (m_TR == nullptr) - return CBSDKRESULT_ERRCONFIG; - - uint32_t read_end_index[cbMAXTRACKOBJ]; - - // Wait for packets to come in - { - std::unique_lock lock(m_lockGetPacketsTrack); - m_bPacketsTrack = true; - m_waitPacketsTrack.wait_for(lock, std::chrono::milliseconds(250)); - } - - // Take a snapshot of the current write pointer - m_lockTrialTracking.lock(); - memcpy(read_end_index, m_TR->write_index, sizeof(m_TR->write_index)); - m_lockTrialTracking.unlock(); - int count = 0; - for (uint16_t id = 0; id < cbMAXTRACKOBJ; ++id) - { - auto num_samples = read_end_index[id] - m_TR->write_start_index[id]; - if (num_samples < 0) - num_samples += m_TR->size; - uint16_t type = m_TR->node_type[id]; - if (num_samples && type) - { - trialtracking->ids[count] = id; // Actual trackable ID - trialtracking->num_samples[count] = num_samples; - trialtracking->types[count] = type; - trialtracking->max_point_counts[count] = m_TR->max_point_counts[id]; - strncpy(reinterpret_cast(trialtracking->names[count]), reinterpret_cast(m_TR->node_name[id]), cbLEN_STR_LABEL); - count++; - } - } - trialtracking->count = count; - } - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkInitTrialData(const uint32_t nInstance, const uint32_t bResetClock, - cbSdkTrialEvent * trialevent, cbSdkTrialCont * trialcont, - cbSdkTrialComment * trialcomment, cbSdkTrialTracking * trialtracking, const unsigned long wait_for_comment_msec) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkInitTrialData(bResetClock, trialevent, trialcont, trialcomment, trialtracking, wait_for_comment_msec); -} - -// Author & Date: Ehsan Azar 25 Feb 2011 -/** Start or stop file recording. -* See cbSdkSetFileConfig docstring in cbsdk.h for more information. -*/ -cbSdkResult SdkApp::SdkSetFileConfig(const char * filename, const char * comment, const uint32_t bStart, const uint32_t options) -{ - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - // declare the packet that will be sent - cbPKT_FILECFG fcpkt = {}; - fcpkt.cbpkt_header.time = 1; - fcpkt.cbpkt_header.chid = 0x8000; - fcpkt.cbpkt_header.type = cbPKTTYPE_SETFILECFG; - fcpkt.cbpkt_header.dlen = cbPKTDLEN_FILECFG; - fcpkt.options = options; - fcpkt.extctrl = 0; - fcpkt.filename[0] = 0; - memcpy(fcpkt.filename, filename, strlen(filename)); - memcpy(fcpkt.comment, comment, strlen(comment)); - // get computer name -#ifdef WIN32 - DWORD cchBuff = sizeof(fcpkt.username); - GetComputerNameA(fcpkt.username, &cchBuff) ; -#else - char * szHost = getenv("HOSTNAME"); - strncpy(fcpkt.username, szHost == nullptr ? "" : szHost, sizeof(fcpkt.username)); -#endif - fcpkt.username[sizeof(fcpkt.username) - 1] = 0; - - // fill in the boolean recording control field - fcpkt.recording = bStart ? 1 : 0; - - // send the packet - if (cbSendPacket(&fcpkt, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetFileConfig -CBSDKAPI cbSdkResult cbSdkSetFileConfig(const uint32_t nInstance, - const char * filename, const char * comment, const uint32_t bStart, const uint32_t options) -{ - if (filename == nullptr || comment == nullptr) - return CBSDKRESULT_NULLPTR; - if (strlen(comment) >= 256) - return CBSDKRESULT_INVALIDCOMMENT; - if (strlen(filename) >= 256) - return CBSDKRESULT_INVALIDFILENAME; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetFileConfig(filename, comment, bStart, options); -} - -// Author & Date: Ehsan Azar 20 Feb 2013 -/** Get file recording information. -* -* @param[out] filename file name being recorded -* @param[out] username username recording the file -* @param[out] pbRecording If recording is in progress (depends on keep-alive mechanism) - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkGetFileConfig(char * filename, char * username, bool * pbRecording) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - // declare the packet that will be sent - cbPKT_FILECFG filecfg; - if (cbGetFileInfo(&filecfg, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - if (filename) - strncpy(filename, filecfg.filename, sizeof(filecfg.filename)); - if (username) - strncpy(username, filecfg.username, sizeof(filecfg.username)); - if (pbRecording) - *pbRecording = (filecfg.options == cbFILECFG_OPT_REC); - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetFileConfig -CBSDKAPI cbSdkResult cbSdkGetFileConfig(const uint32_t nInstance, - char * filename, char * username, bool * pbRecording) -{ - if (filename == nullptr && username == nullptr && pbRecording == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetFileConfig(filename, username, pbRecording); -} - -// Author & Date: Tom Richins 31 Mar 2011 -/** Share Patient demographics for recording. -* -* @param[in] ID - patient ID -* @param[in] firstname - patient firstname -* @param[in] lastname - patient lastname -* @param[in] DOBMonth - patient Date of Birth month -* @param[in] DOBDay - patient DOB day -* @param[in] DOBYear - patient DOB year - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetPatientInfo(const char * ID, const char * firstname, const char * lastname, - const uint32_t DOBMonth, const uint32_t DOBDay, const uint32_t DOBYear) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - // declare the packet that will be sent - cbPKT_PATIENTINFO fcpkt = {}; - fcpkt.cbpkt_header.time = 1; - fcpkt.cbpkt_header.chid = 0x8000; - fcpkt.cbpkt_header.type = cbPKTTYPE_SETPATIENTINFO; - fcpkt.cbpkt_header.dlen = cbPKTDLEN_PATIENTINFO; - fcpkt.ID[0] = 0; - fcpkt.firstname[0] = 0; - fcpkt.lastname[0] = 0; - memset(fcpkt.ID, 0, 24); - memset(fcpkt.firstname, 0, 24); - memset(fcpkt.lastname, 0, 24); - memcpy(fcpkt.ID, ID, strlen(ID)); - memcpy(fcpkt.firstname, firstname, strlen(firstname)); - memcpy(fcpkt.lastname, lastname, strlen(lastname)); - fcpkt.DOBDay = DOBDay; - fcpkt.DOBMonth = DOBMonth; - fcpkt.DOBYear = DOBYear; - - // send the packet - if (cbSendPacket(&fcpkt, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetPatientInfo -CBSDKAPI cbSdkResult cbSdkSetPatientInfo(const uint32_t nInstance, - const char * ID, const char * firstname, const char * lastname, - const uint32_t DOBMonth, const uint32_t DOBDay, const uint32_t DOBYear) -{ - if (ID == nullptr || firstname == nullptr || lastname == nullptr) - return CBSDKRESULT_NULLPTR; - if (strlen(ID) >= 256) - return CBSDKRESULT_INVALIDPARAM; - if (strlen(firstname) >= 256) - return CBSDKRESULT_INVALIDPARAM; - if (strlen(lastname) >= 256) - return CBSDKRESULT_INVALIDPARAM; - if ( DOBMonth < 1 || DOBMonth > 12 ) - return CBSDKRESULT_INVALIDPARAM; - if ( DOBDay < 1 || DOBDay > 31 ) // incomplete test - return CBSDKRESULT_INVALIDPARAM; - if ( DOBYear < 1900 ) - return CBSDKRESULT_INVALIDPARAM; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetPatientInfo(ID, firstname, lastname, DOBMonth, DOBDay, DOBYear); -} - -// Author & Date: Tom Richins 13 Apr 2011 -/** Initiate autoimpedance through central. -* -* \nThis function returns the error code -*/ -cbSdkResult SdkApp::SdkInitiateImpedance() const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - // declare the packet that will be sent - cbPKT_INITIMPEDANCE iipkt = {}; - iipkt.cbpkt_header.time = 1; - iipkt.cbpkt_header.chid = 0x8000; - iipkt.cbpkt_header.type = cbPKTTYPE_SETINITIMPEDANCE; - iipkt.cbpkt_header.dlen = cbPKTDLEN_INITIMPEDANCE; - - iipkt.initiate = 1; // start autoimpedance - // send the packet - if (cbSendPacket(&iipkt, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkInitiateImpedance -CBSDKAPI cbSdkResult cbSdkInitiateImpedance(const uint32_t nInstance) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkInitiateImpedance(); -} - -// Author & Date: Tom Richins 18 Apr 2011 -/** Send poll for running applications and request response. -* -* Running applications will respond. -* @param[in] appname application name (zero ended) -* @param[in] mode Poll mode -* @param[in] flags Poll flags -* @param[in] extra Extra information - -* \n This function returns the error code -* -*/ -cbSdkResult SdkApp::SdkSendPoll(const char* appname, const uint32_t mode, const uint32_t flags, const uint32_t extra) -{ - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - // declare the packet that will be sent - cbPKT_POLL polepkt = {}; - polepkt.cbpkt_header.chid = 0x8000; - polepkt.cbpkt_header.type = cbPKTTYPE_SETPOLL; - polepkt.cbpkt_header.dlen = cbPKTDLEN_POLL; - polepkt.mode = mode; - polepkt.flags = flags; - polepkt.extra = extra; - strncpy(polepkt.appname, appname, sizeof(polepkt.appname)); - // get computer name -#ifdef WIN32 - DWORD cchBuff = sizeof(polepkt.username); - GetComputerNameA(polepkt.username, &cchBuff) ; -#else - strncpy(polepkt.username, getenv("HOSTNAME"), sizeof(polepkt.username)); -#endif - polepkt.username[sizeof(polepkt.username) - 1] = 0; - - // send the packet - if (cbSendPacket(&polepkt, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSendPoll -CBSDKAPI cbSdkResult SdkSendPoll(const uint32_t nInstance, - const char* appname, const uint32_t mode, const uint32_t flags, const uint32_t extra) -{ - if (appname == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSendPoll(appname, mode, flags, extra); -} - -// Author & Date: Tom Richins 26 May 2011 -/** Send packet. -* -* This is used by any process that needs to send a packet using cbSdk rather -* than directly through the library. -* @param[in] ppckt void * packet to send - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSendPacket(void * ppckt) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - // send the packet - if (cbSendPacket(ppckt, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSendPacket -CBSDKAPI cbSdkResult cbSdkSendPoll(const uint32_t nInstance, void * ppckt) -{ - if (ppckt == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSendPacket(ppckt); -} - -// Author & Date: Tom Richins 2 Jun 2011 -/** Set system runlevel. -* -* @param[in] runlevel cbRUNLEVEL_* -* @param[in] locked run nflag -* @param[in] resetque The channel for the reset to que on - -* \n This function returns success or unknown failure -*/ -cbSdkResult SdkApp::SdkSetSystemRunLevel(const uint32_t runlevel, const uint32_t locked, uint32_t resetque) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - // send the packet - cbPKT_SYSINFO sysinfo; - sysinfo.cbpkt_header.time = 1; - sysinfo.cbpkt_header.chid = 0x8000; - sysinfo.cbpkt_header.type = cbPKTTYPE_SYSSETRUNLEV; - sysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; - sysinfo.runlevel = runlevel; - sysinfo.resetque = resetque; - sysinfo.runflags = locked; - - // Enter the packet into the XMT buffer queue - if (cbSendPacket(&sysinfo, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetSystemRunLevel -CBSDKAPI cbSdkResult cbSdkSetSystemRunLevel(const uint32_t nInstance, const uint32_t runlevel, const uint32_t locked, uint32_t resetque) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetSystemRunLevel(runlevel, locked, resetque); -} - -// Author & Date: Johnluke Siska 15 May 2014 -/** Wrapper to get the system runlevel information. -* -* @param[out] runlevel cbRUNLEVEL_* -* @param[out] runflags For instance if the system is locked for recording -* @param[out] resetque The channel for the reset to que on - -* \n This function returns success or unknown failure -*/ -cbSdkResult SdkApp::SdkGetSystemRunLevel(uint32_t * runlevel, uint32_t * runflags, uint32_t * resetque) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - cbRESULT cbRes = cbGetSystemRunLevel(runlevel, runflags, resetque, m_nInstance); - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes == cbRESULT_HARDWAREOFFLINE) - return CBSDKRESULT_ERROFFLINE; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetSystemRunLevel -CBSDKAPI cbSdkResult cbSdkGetSystemRunLevel(const uint32_t nInstance, uint32_t * runlevel, uint32_t * runflags, uint32_t * resetque) -{ - if (runlevel == nullptr && runflags == nullptr && resetque == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetSystemRunLevel(runlevel, runflags, resetque); -} - -// Author & Date: Ehsan Azar 25 Feb 2011 -/** Send a digital output. -* -* @param[in] channel the channel number (1-based) must be digital output channel -* @param[in] value the digital value to send - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetDigitalOutput(const uint16_t channel, const uint16_t value) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - // declare the packet that will be sent - cbPKT_SET_DOUT dopkt; - dopkt.cbpkt_header.time = 1; - dopkt.cbpkt_header.chid = 0x8000; - dopkt.cbpkt_header.type = cbPKTTYPE_SET_DOUTSET; - dopkt.cbpkt_header.dlen = cbPKTDLEN_SET_DOUT; -#ifndef CBPROTO_311 - dopkt.cbpkt_header.instrument = cbGetChanInstrument(channel) - 1; -#endif - // get the channel number - dopkt.chan = cb_cfg_buffer_ptr[0]->chaninfo[channel - 1].chan; - - // fill in the boolean on/off field - dopkt.value = value; - - // send the packet - if (cbSendPacket(&dopkt, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetDigitalOutput -CBSDKAPI cbSdkResult cbSdkSetDigitalOutput(const uint32_t nInstance, const uint16_t channel, const uint16_t value) -{ - uint32_t nChan = channel; - - // verify that the connection is open - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - // get the channel number - if (!IsChanDigout(channel)) - nChan = GetDigoutChanNumber(channel); - - // verify we didn't get an invalid channel back - if (!IsChanDigout(nChan)) - return CBSDKRESULT_INVALIDCHANNEL; // Not a digital output channel - - return g_app[nInstance]->SdkSetDigitalOutput(nChan, value); -} - -// Author & Date: Ehsan Azar 25 Feb 2013 -/** Send a synch output. -* -* @param[in] channel the channel number (1-based) must be synch output channel -* @param[in] nFreq frequency in mHz (0 means stop the clock) -* @param[in] nRepeats number of pulses to generate (0 means forever) - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetSynchOutput(const uint16_t channel, const uint32_t nFreq, const uint32_t nRepeats) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - // Only CerePlex currently supports this, for NSP digital output may be used with similar results - if (!(m_instInfo & cbINSTINFO_CEREPLEX)) - return CBSDKRESULT_NOTIMPLEMENTED; - // Currently only one synch output supported - if (channel != 1) - return CBSDKRESULT_INVALIDCHANNEL; // Not a synch output channel - // Not supported - if (nFreq > 100000) - return CBSDKRESULT_INVALIDPARAM; - - // declare the packet that will be sent - cbPKT_NM nmpkt; - nmpkt.cbpkt_header.chid = 0x8000; - nmpkt.cbpkt_header.type = cbPKTTYPE_NMSET; - nmpkt.cbpkt_header.dlen = cbPKTDLEN_NM; - nmpkt.mode = cbNM_MODE_SYNCHCLOCK; - nmpkt.value = nFreq; - nmpkt.opt[0] = nRepeats; - - // send the packet - if (cbSendPacket(&nmpkt, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetSynchOutput -CBSDKAPI cbSdkResult cbSdkSetSynchOutput(const uint32_t nInstance, const uint16_t channel, const uint32_t nFreq, const uint32_t nRepeats) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetSynchOutput(channel, nFreq, nRepeats); -} - -// Author & Date: Ehsan Azar 5 May 2013 -/** Upload a file to NSP. -* -* This could be used to even update the firmware itself -* Warning: -* Be very careful in using this mechanism, -* if system files or firmware is overwritten, factory image is needed -* Warning: -* Must not shut down NSP or communications in midst of operation -* @param[in] szSrc path to file to be uploaded to NSP -* @param[in] szDstDir destination directory (should not include the file name, nor path ending) -* @param[in] nInstance library instance number - -* \n This function returns the error code -*/ -cbSdkResult cbSdkUpload(const char * szSrc, const char * szDstDir, const uint32_t nInstance) -{ - uint32_t runlevel; - cbSdkResult res = CBSDKRESULT_SUCCESS; - cbRESULT cbres = cbGetSystemRunLevel(&runlevel, nullptr, nullptr); - if (cbres) - return CBSDKRESULT_UNKNOWN; - // If running or even updating do not use this, only when standby this function should be used - if (runlevel != cbRUNLEVEL_STANDBY) - return CBSDKRESULT_INVALIDINST; - - uint32_t cbRead = 0; - uint32_t cbFile = 0; - FILE * pFile = fopen(szSrc, "rb"); - if (pFile == nullptr) - return CBSDKRESULT_ERROPENFILE; - - fseek(pFile, 0, SEEK_END); - cbFile = ftell(pFile); - if (cbFile > cbSdk_MAX_UPOLOAD_SIZE) - { - // Too big of a file to upload - fclose(pFile); - return CBSDKRESULT_ERRFORMATFILE; - } - fseek(pFile, 0, SEEK_SET); - uint8_t * pFileData = nullptr; - try { - pFileData = new uint8_t[cbFile]; - } catch (...) { - pFileData = nullptr; - } - if (pFileData == nullptr) - { - fclose(pFile); - return CBSDKRESULT_ERRMEMORY; - } - // Read entire file into memory - cbRead = static_cast(fread(pFileData, sizeof(uint8_t), cbFile, pFile)); - fclose(pFile); - if (cbFile != cbRead) - { - free(pFileData); - return CBSDKRESULT_ERRFORMATFILE; - } - const char * szBaseName = &szSrc[0]; - // Find the base name and use it on destination too - auto nLen = strlen(szSrc); - for (int i = static_cast(nLen) - 1; i >= 0; --i) - { -#ifdef WIN32 - if (szSrc[i] == '/' || szSrc[i] == '\\') -#else - if (szSrc[i] == '/') -#endif - { - szBaseName = &szSrc[i + 1]; - break; - } - else if (szSrc[i] == ' ') - { - szBaseName = &szSrc[i]; - break; - } - } - // Do not accept any space in the file name - if (szBaseName[0] == ' ' || szBaseName[0] == 0) - { - free(pFileData); - return CBSDKRESULT_INVALIDFILENAME; - } - nLen = strlen(szBaseName) + strlen(szDstDir) + 1; - if (nLen >= 64) - { - free(pFileData); - return CBSDKRESULT_INVALIDFILENAME; - } - - cbPKT_UPDATE upkt = {}; - upkt.cbpkt_header.time = 0; - upkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - upkt.cbpkt_header.type = cbPKTTYPE_UPDATESET; - upkt.cbpkt_header.dlen = cbPKTDLEN_UPDATE; - _snprintf(upkt.filename, sizeof(upkt.filename), "%s/%s", szDstDir, szBaseName); - - const uint32_t blocks = (cbFile / 512) + 1; - for (uint32_t b = 0; b < blocks; ++b) - { - upkt.blockseq = b; - upkt.blockend = (b == (blocks - 1)); - upkt.blocksiz = std::min(static_cast(cbFile - (b * 512)), (int32_t) 512); - memcpy(&upkt.block[0], pFileData + (b * 512), upkt.blocksiz); - do { - cbres = cbSendPacket(&upkt, nInstance); - } while (cbres == cbRESULT_MEMORYUNAVAIL); - if (cbres) - { - res = CBSDKRESULT_UNKNOWN; - break; - } - } - - free(pFileData); - - if (res == CBSDKRESULT_SUCCESS) - { - /// \todo must wait for cbLOG_MODE_UPLOAD_RES because mount is asynch - } - - return res; -} - -// Author & Date: Ehsan Azar 5 May 2013 -/** Send an extension command. -* -* @param[in] extCmd the extension command - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkExtDoCommand(const cbSdkExtCmd * extCmd) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - // Only NSP1.5 compatible devices currently supports this - if (m_instInfo & cbINSTINFO_NSP1) - return CBSDKRESULT_NOTIMPLEMENTED; - - cbRESULT cbres; - cbPKT_LOG pktLog = {}; - strcpy(pktLog.name, "cbSDK"); - pktLog.cbpkt_header.chid = 0x8000; - pktLog.cbpkt_header.type = cbPKTTYPE_LOGSET; - pktLog.cbpkt_header.dlen = cbPKTDLEN_LOG; - - switch (extCmd->cmd) - { - case cbSdkExtCmd_INPUT: - pktLog.mode = cbLOG_MODE_RPC_INPUT; - strncpy(pktLog.desc, extCmd->szCmd, sizeof(pktLog.desc)); - // send the packet - cbres = cbSendPacket(&pktLog, m_nInstance); - if (cbres) - return CBSDKRESULT_UNKNOWN; - break; - case cbSdkExtCmd_RPC: - pktLog.mode = cbLOG_MODE_RPC; - strncpy(pktLog.desc, extCmd->szCmd, sizeof(pktLog.desc)); - // send the packet - cbres = cbSendPacket(&pktLog, m_nInstance); - if (cbres) - return CBSDKRESULT_UNKNOWN; - break; - case cbSdkExtCmd_UPLOAD: - return cbSdkUpload(extCmd->szCmd, "/extnroot/root", m_nInstance); - break; - case cbSdkExtCmd_TERMINATE: - pktLog.mode = cbLOG_MODE_RPC_KILL; - // send the packet - cbres = cbSendPacket(&pktLog, m_nInstance); - if (cbres) - return CBSDKRESULT_UNKNOWN; - break; - case cbSdkExtCmd_END_PLUGIN: - pktLog.mode = cbLOG_MODE_ENDPLUGIN; - // send the packet - cbres = cbSendPacket(&pktLog, m_nInstance); - if (cbres) - return CBSDKRESULT_UNKNOWN; - break; - case cbSdkExtCmd_NSP_REBOOT: - pktLog.mode = cbLOG_MODE_NSP_REBOOT; - // send the packet - cbres = cbSendPacket(&pktLog, m_nInstance); - if (cbres) - return CBSDKRESULT_UNKNOWN; - break; - case cbSdkExtCmd_PLUGINFO: - pktLog.mode = cbLOG_MODE_PLUGINFO; - // send the packet - cbres = cbSendPacket(&pktLog, m_nInstance); - if (cbres) - return CBSDKRESULT_UNKNOWN; - break; - default: - return CBSDKRESULT_INVALIDPARAM; - break; - } - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::cbSdkExtCmd -CBSDKAPI cbSdkResult cbSdkExtDoCommand(const uint32_t nInstance, const cbSdkExtCmd * extCmd) -{ - if (extCmd == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkExtDoCommand(extCmd); -} - -// Author & Date: Ehsan Azar 26 Oct 2011 -/** Send an analog output waveform, or monitor a channel. If none is given then analog output channel is disabled. -* -* @param[in] channel the channel number (1-based) must be digital output channel -* @param[in] wf pointer to waveform structure -* @param[in] mon the structure to specify monitoring channel - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetAnalogOutput(const uint16_t channel, const cbSdkWaveformData * wf, const cbSdkAoutMon * mon) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (wf != nullptr) - { - if (wf->type < cbSdkWaveform_NONE || wf->type >= cbSdkWaveform_COUNT) - return CBSDKRESULT_INVALIDPARAM; - if (wf->trigNum >= cbMAX_AOUT_TRIGGER) - return CBSDKRESULT_INVALIDPARAM; - if (wf->trig < cbSdkWaveformTrigger_NONE || wf->trig >= cbSdkWaveformTrigger_COUNT) - return CBSDKRESULT_INVALIDPARAM; - if (wf->type == cbSdkWaveform_PARAMETERS && wf->phases > cbMAX_WAVEFORM_PHASES) - return CBSDKRESULT_NOTIMPLEMENTED; - switch (wf->trig) - { - case cbWAVEFORM_TRIGGER_DINPREG: - case cbWAVEFORM_TRIGGER_DINPFEG: - if (wf->trigChan == 0 || wf->trigChan > 16) - return CBSDKRESULT_INVALIDPARAM; - break; - case cbWAVEFORM_TRIGGER_SPIKEUNIT: - if (wf->trigChan == 0 || wf->trigChan > cbMAXCHANS) - return CBSDKRESULT_INVALIDCHANNEL; - break; - default: - break; - } - } - - uint32_t dwOptions; - uint32_t nMonitoredChan; - cbRESULT cbres = cbGetAoutOptions(channel, &dwOptions, &nMonitoredChan, nullptr, m_nInstance); - switch (cbres) - { - case cbRESULT_OK: - break; - case cbRESULT_INVALIDCHANNEL: - return CBSDKRESULT_INVALIDCHANNEL; - break; - default: - return CBSDKRESULT_UNKNOWN; - break; - } - if (wf != nullptr) - { - cbPKT_AOUT_WAVEFORM wfPkt = {}; - wfPkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; - wfPkt.cbpkt_header.type = cbPKTTYPE_WAVEFORMSET; - wfPkt.cbpkt_header.dlen = cbPKTDLEN_WAVEFORM; -#ifndef CBPROTO_311 - wfPkt.cbpkt_header.instrument = cbGetChanInstrument(channel) - 1; -#endif - // Set common fields - wfPkt.mode = wf->type; - wfPkt.chan = cb_cfg_buffer_ptr[0]->chaninfo[channel - 1].chan; - wfPkt.repeats = wf->repeats; - wfPkt.trig = wf->trig; - wfPkt.trigChan = wf->trigChan; - wfPkt.trigValue = wf->trigValue; - wfPkt.trigNum = wf->trigNum; - // Mode-specific fields - if (wfPkt.mode == cbWAVEFORM_MODE_PARAMETERS) - { - if (wf->phases > cbMAX_WAVEFORM_PHASES) - return CBSDKRESULT_INVALIDPARAM; - - memcpy(wfPkt.wave.duration, wf->duration, sizeof(uint16_t) * wf->phases); - memcpy(wfPkt.wave.amplitude, wf->amplitude, sizeof(int16_t) * wf->phases); - wfPkt.wave.seq = 0; - wfPkt.wave.seqTotal = 1; - wfPkt.wave.phases = wf->phases; - wfPkt.wave.offset = wf->offset; - wfPkt.mode = cbWAVEFORM_MODE_PARAMETERS; - } - else if (wfPkt.mode == cbWAVEFORM_MODE_SINE) - { - wfPkt.wave.offset = wf->offset; - wfPkt.wave.sineFrequency = wf->sineFrequency; - wfPkt.wave.sineAmplitude = wf->sineAmplitude; - wfPkt.mode = cbWAVEFORM_MODE_SINE; - } - // Sending a none-trigger we turn it into instant activation - if (wfPkt.trig == cbWAVEFORM_TRIGGER_NONE) - wfPkt.active = 1; - - // send the waveform packet - cbSendPacket(&wfPkt, m_nInstance); - // Also make sure channel is to output waveform - dwOptions &= ~(cbAOUT_MONITORSMP | cbAOUT_MONITORSPK); - dwOptions |= cbAOUT_WAVEFORM; - } - else if (mon != nullptr) - { - nMonitoredChan = mon->chan; - dwOptions &= ~(cbAOUT_MONITORSMP | cbAOUT_MONITORSPK | cbAOUT_TRACK); - if (mon->bSpike) - dwOptions |= cbAOUT_MONITORSPK; - else - dwOptions |= cbAOUT_MONITORSMP; - if (mon->bTrack) - dwOptions |= cbAOUT_TRACK; - } - else - { - dwOptions &= ~(cbAOUT_MONITORSMP | cbAOUT_MONITORSPK | cbAOUT_WAVEFORM | cbAOUT_EXTENSION); - } - - // Set monitoring option - cbres = cbSetAoutOptions(channel, dwOptions, nMonitoredChan, 0, m_nInstance); - switch (cbres) - { - case cbRESULT_OK: - break; - case cbRESULT_INVALIDCHANNEL: - return CBSDKRESULT_INVALIDCHANNEL; - break; - default: - return CBSDKRESULT_UNKNOWN; - break; - } - - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetAnalogOutput -CBSDKAPI cbSdkResult cbSdkSetAnalogOutput(const uint32_t nInstance, - const uint16_t channel, const cbSdkWaveformData * wf, const cbSdkAoutMon * mon) -{ - uint32_t nChan = channel; - - // verify that the connection is open - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - if (!IsChanAnalogOut(channel) && !IsChanAudioOut(channel)) - nChan = GetAnalogOrAudioOutChanNumber(channel); - - if (wf != nullptr && mon != nullptr) - return CBSDKRESULT_INVALIDPARAM; // cannot specify both - if (!IsChanAnalogOut(nChan) && !IsChanAudioOut(nChan)) - return CBSDKRESULT_INVALIDCHANNEL; - - return g_app[nInstance]->SdkSetAnalogOutput(nChan, wf, mon); -} - -// Author & Date: Ehsan Azar 25 Feb 2011 -/** Activate or deactivate channels. Only activated channels are monitored. -* -* @param[in] channel channel number (1-based), zero means all channels -* @param[in] bActive the character set of the comment - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetChannelMask(const uint16_t channel, const uint32_t bActive) -{ - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - // zero means all - if (channel == 0) - { - for (bool & i : m_bChannelMask) - i = (bActive > 0); - } - else - m_bChannelMask[channel - 1] = (bActive > 0); - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetChannelMask -CBSDKAPI cbSdkResult cbSdkSetChannelMask(const uint32_t nInstance, const uint16_t channel, const uint32_t bActive) -{ - if (channel > cbMAXCHANS) - return CBSDKRESULT_INVALIDCHANNEL; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetChannelMask(channel, bActive); -} - -// Author & Date: Ehsan Azar 25 Feb 2011 -/** Send a comment or custom event. -* -* @param[in] rgba the color or custom event number -* @param[in] charset the character set of the comment -* @param[in] comment comment string (can be nullptr) - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetComment(const uint32_t rgba, const uint8_t charset, const char * comment) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (cbSetComment(charset, rgba, 0, comment, m_nInstance)) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetComment -CBSDKAPI cbSdkResult cbSdkSetComment(const uint32_t nInstance, const uint32_t t_bgr, const uint8_t charset, const char * comment) -{ - if (comment) - { - if (strlen(comment) >= cbMAX_COMMENT) - return CBSDKRESULT_INVALIDCOMMENT; - } - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetComment(t_bgr, charset, comment); -} - -// Author & Date: Ehsan Azar 3 March 2011 -/** Send a full channel configuration packet. -* -* @param[in] channel channel number (1-based) -* @param[in] chaninfo the full channel configuration - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetChannelConfig(const uint16_t channel, cbPKT_CHANINFO * chaninfo) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (cb_cfg_buffer_ptr[m_nIdx]->chaninfo[channel - 1].cbpkt_header.chid == 0) - return CBSDKRESULT_INVALIDCHANNEL; - - chaninfo->cbpkt_header.type = cbPKTTYPE_CHANSET; - chaninfo->cbpkt_header.dlen = cbPKTDLEN_CHANINFO; - const cbRESULT cbRes = cbSendPacket(chaninfo, m_nInstance); - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetChannelConfig -CBSDKAPI cbSdkResult cbSdkSetChannelConfig(const uint32_t nInstance, const uint16_t channel, cbPKT_CHANINFO * chaninfo) -{ - if (channel == 0 || channel > cbMAXCHANS) - return CBSDKRESULT_INVALIDCHANNEL; - if (chaninfo == nullptr) - return CBSDKRESULT_NULLPTR; - if (chaninfo->cbpkt_header.chid != cbPKTCHAN_CONFIGURATION) - return CBSDKRESULT_INVALIDPARAM; - if (chaninfo->cbpkt_header.dlen > cbPKTDLEN_CHANINFO || chaninfo->cbpkt_header.dlen < cbPKTDLEN_CHANINFOSHORT) - return CBSDKRESULT_INVALIDPARAM; - if (chaninfo->chan != channel) - return CBSDKRESULT_INVALIDCHANNEL; - - /// \todo do more validation before sending the packet - - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetChannelConfig(channel, chaninfo); -} - -// Author & Date: Ehsan Azar 28 Feb 2011 -// Purpose: Get a channel configuration packet -// Inputs: -// channel - channel number (1-based) -// Outputs: -// chaninfo - the full channel configuration -// returns the error code -/** Get a channel configuration packet. -* -* @param[in] channel channel number (1-based) -* @param[in] chaninfo the full channel configuration - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkGetChannelConfig(const uint16_t channel, cbPKT_CHANINFO * chaninfo) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (!cb_library_initialized[m_nIdx]) - return CBSDKRESULT_CLOSED; - - const cbRESULT cbRes = cbGetChanInfo(channel, chaninfo, m_nInstance); - if (cbRes == cbRESULT_INVALIDCHANNEL) - return CBSDKRESULT_INVALIDCHANNEL; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetChannelConfig -CBSDKAPI cbSdkResult cbSdkGetChannelConfig(const uint32_t nInstance, const uint16_t channel, cbPKT_CHANINFO * chaninfo) -{ - if (channel == 0 || channel > cbMAXCHANS) - return CBSDKRESULT_INVALIDCHANNEL; - if (chaninfo == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetChannelConfig(channel, chaninfo); -} - -// Author & Date: Tom Richins 11 Apr 2011 -// Purpose: retrieve group list -// wrapper to export cbGetSampleGroupList - -/** Retrieve group list. -* -* @param[in] proc -* @param[in] group group (1-5) -* @param[in,out] length length of group list -* @param[in,out] list list of channels in selected group (1-based) - - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkGetSampleGroupList(const uint32_t proc, const uint32_t group, uint32_t *length, uint16_t *list) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - const cbRESULT cbRes = cbGetSampleGroupList(proc, group, length, list, m_nInstance); - if (cbRes == cbRESULT_INVALIDADDRESS) - return CBSDKRESULT_INVALIDPARAM; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetSampleGroupList -CBSDKAPI cbSdkResult cbSdkGetSampleGroupList(const uint32_t nInstance, - const uint32_t proc, const uint32_t group, uint32_t *length, uint16_t *list) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetSampleGroupList(proc, group, length, list); -} - -// Author & Date: Sylvana Alpert 13 Jan 2014 -// Purpose: retrieve sample group info -// wrapper to export cbGetSampleGroupInfo -/** Retrieve group info. -* -* @param[in] proc -* @param[in] group group (1-5) -* @param[in,out] label -* @param[in,out] period -* @param[in,out] length - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkGetSampleGroupInfo(const uint32_t proc, const uint32_t group, char *label, uint32_t *period, uint32_t *length) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - const cbRESULT cbRes = cbGetSampleGroupInfo(proc, group, label, period, length, m_nInstance); - if (cbRes == cbRESULT_INVALIDADDRESS) - return CBSDKRESULT_INVALIDPARAM; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetSampleGroupInfo -CBSDKAPI cbSdkResult cbSdkGetSampleGroupInfo(const uint32_t nInstance, - const uint32_t proc, const uint32_t group, char *label, uint32_t *period, uint32_t *length) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetSampleGroupInfo(proc, group, label, period, length); -} - - -// Author & Date: Tom Richins 24 Jun 2011 -// Purpose: Get filter description -// wrapper to export cbGetFilterDesc - -/** Get filter description. -* -* @param[in] proc -* @param[in] filt filter number -* @param[out] filtdesc the filter description - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkGetFilterDesc(const uint32_t proc, const uint32_t filt, cbFILTDESC * filtdesc) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - const cbRESULT cbRes = cbGetFilterDesc(proc, filt, filtdesc, m_nInstance); - if (cbRes == cbRESULT_INVALIDADDRESS) - return CBSDKRESULT_INVALIDPARAM; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetFilterDesc -CBSDKAPI cbSdkResult cbSdkGetFilterDesc(const uint32_t nInstance, const uint32_t proc, const uint32_t filt, cbFILTDESC * filtdesc) -{ - if (filtdesc == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetFilterDesc(proc, filt, filtdesc); -} - -// Author & Date: Ehsan Azar 27 Oct 2011 -// Purpose: retrieve group list -// wrapper to export cbGetTrackObj - -/** Retrieve tracking information. -* -* @param[in] id trackable object ID (1 to cbMAXTRACKOBJ) -* @param[out] name name of the video source -* @param[out] type type of the trackable object (start from 0) -* @param[out] pointCount the maximum number of points for this trackable - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkGetTrackObj(char * name, uint16_t * type, uint16_t * pointCount, const uint32_t id) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - const cbRESULT cbRes = cbGetTrackObj(name, type, pointCount, id, m_nInstance); - if (cbRes == cbRESULT_INVALIDADDRESS) - return CBSDKRESULT_INVALIDTRACKABLE; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetTrackObj -CBSDKAPI cbSdkResult cbSdkGetTrackObj(const uint32_t nInstance, char * name, uint16_t * type, uint16_t * pointCount, const uint32_t id) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetTrackObj(name, type, pointCount, id); -} - -// Author & Date: Ehsan Azar 27 Oct 2011 -// Purpose: retrieve group list -// wrapper to export cbGetVideoSource - -/** Retrieve video source. -* -* @param[in] id video source ID (1 to cbMAXVIDEOSOURCE) -* @param[out] name name of the video source -* @param[out] fps the frame rate of the video source - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkGetVideoSource(char * name, float * fps, const uint32_t id) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - - const cbRESULT cbRes = cbGetVideoSource(name, fps, id, m_nInstance); - if (cbRes == cbRESULT_INVALIDADDRESS) - return CBSDKRESULT_INVALIDVIDEOSRC; - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetVideoSource -CBSDKAPI cbSdkResult cbSdkGetVideoSource(const uint32_t nInstance, char * name, float * fps, const uint32_t id) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetVideoSource(name, fps, id); -} - -// Author & Date: Ehsan Azar 30 March 2011 -/** Send global spike configuration. -* -* @param[in] spklength spike length -* @param[in] spkpretrig spike pre-trigger number of samples - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetSpikeConfig(const uint32_t spklength, const uint32_t spkpretrig) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - const cbRESULT cbRes = cbSetSpikeLength(spklength, spkpretrig, m_nInstance); - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSetSpikeConfig -CBSDKAPI cbSdkResult cbSdkSetSpikeConfig(const uint32_t nInstance, const uint32_t spklength, const uint32_t spkpretrig) -{ - if (spklength > cbMAX_PNTS || spkpretrig >= spklength) - return CBSDKRESULT_INVALIDPARAM; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSetSpikeConfig(spklength, spkpretrig); -} - -// Author & Date: Ehsan Azar 30 March 2011 -/** Send global spike configuration. -* -* @param[in] chan channel id (1-based) -* @param[in] filter filter id -* @param[in] group the sample group (1-6) -* -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSetAinpSampling(const uint32_t chan, const uint32_t filter, const uint32_t group) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - const cbRESULT cbRes = cbSetAinpSampling(chan, filter, group, m_nInstance); - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkSetAinpSampling(const uint32_t nInstance, const uint32_t chan, const uint32_t filter, const uint32_t group) -{ - if (chan > cbMAXCHANS || filter >= cbMAXFILTS) - return CBSDKRESULT_INVALIDPARAM; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - return g_app[nInstance]->SdkSetAinpSampling(chan, filter, group); -} - -cbSdkResult SdkApp::SdkSetAinpSpikeOptions(const uint32_t chan, const uint32_t flags, const uint32_t filter) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - const cbRESULT cbRes = cbSetAinpSpikeOptions(chan, flags, filter); - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -CBSDKAPI cbSdkResult cbSdkSetAinpSpikeOptions(const uint32_t nInstance, const uint32_t chan, const uint32_t flags, const uint32_t filter) -{ - if (chan > cbMAXCHANS || filter >= cbMAXFILTS) - return CBSDKRESULT_INVALIDPARAM; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - return g_app[nInstance]->SdkSetAinpSpikeOptions(chan, flags, filter); -} - -// Author & Date: Ehsan Azar 30 March 2011 -/** Get global system configuration. -* -* @param[out] spklength spike length -* @param[out] spkpretrig spike pre-trigger number of samples -* @param[out] sysfreq system clock frequency in Hz - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkGetSysConfig(uint32_t * spklength, uint32_t * spkpretrig, uint32_t * sysfreq) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - const cbRESULT cbRes = cbGetSpikeLength(spklength, spkpretrig, sysfreq, m_nInstance); - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkGetSysConfig -CBSDKAPI cbSdkResult cbSdkGetSysConfig(const uint32_t nInstance, uint32_t * spklength, uint32_t * spkpretrig, uint32_t * sysfreq) -{ - if (spklength == nullptr && spkpretrig == nullptr && sysfreq == nullptr) - return CBSDKRESULT_NULLPTR; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkGetSysConfig(spklength, spkpretrig, sysfreq); -} - -// Author & Date: Ehsan Azar 11 MAy 2012 -/** Perform given runlevel system command. -* -* @param[out] cmd system command to perform - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkSystem(const cbSdkSystemType cmd) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - cbPKT_SYSINFO pktsysinfo; - pktsysinfo.cbpkt_header.time = 0; - pktsysinfo.cbpkt_header.chid = 0x8000; - pktsysinfo.cbpkt_header.type = cbPKTTYPE_SYSSETRUNLEV; - pktsysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; -#ifndef CBPROTO_311 - pktsysinfo.cbpkt_header.instrument = 0; -#endif - switch (cmd) - { - case cbSdkSystem_RESET: - pktsysinfo.runlevel = cbRUNLEVEL_RESET; - break; - case cbSdkSystem_SHUTDOWN: - pktsysinfo.runlevel = cbRUNLEVEL_SHUTDOWN; - break; - case cbSdkSystem_STANDBY: - pktsysinfo.runlevel = cbRUNLEVEL_HARDRESET; - break; - default: - return CBSDKRESULT_NOTIMPLEMENTED; - } - const cbRESULT cbRes = cbSendPacket(&pktsysinfo, m_nInstance); - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkSystem -CBSDKAPI cbSdkResult cbSdkSystem(const uint32_t nInstance, const cbSdkSystemType cmd) -{ - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkSystem(cmd); -} - -// Author & Date: Ehsan Azar 28 Feb 2011 -/** Register a callback function. -* -* @param[in] callbackType the callback type to register function against -* @param[in] pCallbackFn callback function -* @param[in] pCallbackData custom parameter callback is called with -* -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkRegisterCallback(const cbSdkCallbackType callbackType, const cbSdkCallback pCallbackFn, void * pCallbackData) -{ - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (m_pCallback[callbackType]) // Already registered - return CBSDKRESULT_CALLBACKREGFAILED; - - m_lockCallback.lock(); - m_pCallbackParams[callbackType] = pCallbackData; - m_pCallback[callbackType] = pCallbackFn; - m_lockCallback.unlock(); - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkRegisterCallback -CBSDKAPI cbSdkResult cbSdkRegisterCallback(const uint32_t nInstance, - const cbSdkCallbackType callbackType, const cbSdkCallback pCallbackFn, void * pCallbackData) -{ - if (!pCallbackFn) - return CBSDKRESULT_NULLPTR; - if (callbackType >= CBSDKCALLBACK_COUNT) - return CBSDKRESULT_INVALIDCALLBACKTYPE; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkRegisterCallback(callbackType, pCallbackFn, pCallbackData); -} - -// Author & Date: Ehsan Azar 28 Feb 2011 -/** Unregister the current callback. -* -* @param[in] callbackType the callback type to unregister - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkUnRegisterCallback(const cbSdkCallbackType callbackType) -{ - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (!m_pCallback[callbackType]) // Already unregistered - return CBSDKRESULT_CALLBACKREGFAILED; - - m_lockCallback.lock(); - m_pCallback[callbackType] = nullptr; - m_pCallbackParams[callbackType] = nullptr; - m_lockCallback.unlock(); - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkUnRegisterCallback -CBSDKAPI cbSdkResult cbSdkUnRegisterCallback(const uint32_t nInstance, const cbSdkCallbackType callbackType) -{ - if (callbackType >= CBSDKCALLBACK_COUNT) - return CBSDKRESULT_INVALIDCALLBACKTYPE; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkUnRegisterCallback(callbackType); -} - -// Author & Date: Ehsan Azar 7 Aug 2013 -/** Get callback status. -* if unregistered returns success, and means a register should not fail -* @param[in] callbackType the callback type to unregister - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkCallbackStatus(const cbSdkCallbackType callbackType) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (m_pCallback[callbackType]) - return CBSDKRESULT_CALLBACKREGFAILED; // Already registered - return CBSDKRESULT_SUCCESS; -} - -// Purpose: sdk stub for SdkApp::SdkCallbackStatus -CBSDKAPI cbSdkResult cbSdkCallbackStatus(const uint32_t nInstance, const cbSdkCallbackType callbackType) -{ - if (callbackType >= CBSDKCALLBACK_COUNT) - return CBSDKRESULT_INVALIDCALLBACKTYPE; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - - return g_app[nInstance]->SdkCallbackStatus(callbackType); -} - -// Author & Date: Ehsan Azar 21 Feb 2013 -/** Convert volts string (e.g. '5V', '-65mV', ...) to its raw digital value equivalent for given channel. -* -* @param[in] channel the channel number (1-based) -* @param[in] szVoltsUnitString the volts string -* @param[out] digital the raw digital value - -* \n This function returns the error code -*/ -cbSdkResult SdkApp::SdkAnalogToDigital(uint16_t channel, const char * szVoltsUnitString, int32_t * digital) const { - if (m_instInfo == 0) - return CBSDKRESULT_CLOSED; - if (!cb_library_initialized[m_nIdx]) - return CBSDKRESULT_CLOSED; - if (static_cast(strlen(szVoltsUnitString)) > 16) - return CBSDKRESULT_INVALIDPARAM; - int32_t nFactor = 0; - std::string strVolts = szVoltsUnitString; - std::string strUnit; - if (strVolts.rfind("mV") != std::string::npos) - { - nFactor = 1000; - strUnit = "mV"; - } - else if (strVolts.rfind("uV") != std::string::npos) - { - nFactor = 1000000; - strUnit = "uV"; - } - else if (strVolts.rfind("nV") != std::string::npos) - { - nFactor = 1000000000; - strUnit = "nV"; - } - else if (strVolts.rfind('V') != std::string::npos) - { - nFactor = 1; - strUnit = "V"; - } - char * pEnd = nullptr; - double dValue = 0; - long nValue = 0; - // If no units specified, assume raw integer passed as string - if (nFactor == 0) - { - nValue = strtol(szVoltsUnitString, &pEnd, 0); - } - else - { - dValue = strtod(szVoltsUnitString, &pEnd); - } - if (pEnd == szVoltsUnitString) - return CBSDKRESULT_INVALIDPARAM; - // What remains should be just the unit string - std::string strRest = pEnd; - // Remove all spaces - std::string::iterator end_pos = std::remove(strRest.begin(), strRest.end(), ' '); - strRest.erase(end_pos, strRest.end()); - if (strRest != strUnit) - return CBSDKRESULT_INVALIDPARAM; - if (nFactor == 0) - { - *digital = static_cast(nValue); - return CBSDKRESULT_SUCCESS; - } - cbSCALING scale; - cbRESULT cbRes; - if (IsChanAnalogIn(channel)) - cbRes = cbGetAinpScaling(channel, &scale, m_nInstance); - else - cbRes = cbGetAoutScaling(channel, &scale, m_nInstance); - if (cbRes == cbRESULT_NOLIBRARY) - return CBSDKRESULT_CLOSED; - if (cbRes) - return CBSDKRESULT_UNKNOWN; - strUnit = scale.anaunit; - double chan_factor = 1; - if (strUnit == "mV") - chan_factor = 1000; - else if (strUnit == "uV") - chan_factor = 1000000; - // TODO: see if anagain needs to be used - *digital = static_cast(floor(((dValue * scale.digmax) * chan_factor) / (static_cast(nFactor) * scale.anamax))); - return CBSDKRESULT_SUCCESS; -} - -/// sdk stub for SdkApp::SdkAnalogToDigital -CBSDKAPI cbSdkResult cbSdkAnalogToDigital(const uint32_t nInstance, const uint16_t channel, const char * szVoltsUnitString, int32_t * digital) -{ - if (channel == 0 || channel > cbMAXCHANS) - return CBSDKRESULT_INVALIDCHANNEL; - if (nInstance >= cbMAXOPEN) - return CBSDKRESULT_INVALIDPARAM; - if (g_app[nInstance] == nullptr) - return CBSDKRESULT_CLOSED; - if (szVoltsUnitString == nullptr || digital == nullptr) - return CBSDKRESULT_NULLPTR; - - return g_app[nInstance]->SdkAnalogToDigital(channel, szVoltsUnitString, digital); -} - - -// Author & Date: Ehsan Azar 29 April 2012 -/// Sdk app base constructor -SdkApp::SdkApp() - : m_bInitialized(false), m_lastCbErr(cbRESULT_OK) - , m_bPacketsEvent(false), m_bPacketsCmt(false), m_bPacketsTrack(false) - , m_uTrialBeginChannel(0), m_uTrialBeginMask(0), m_uTrialBeginValue(0) - , m_uTrialEndChannel(0), m_uTrialEndMask(0), m_uTrialEndValue(0) - , m_uTrialWaveforms(0) - , m_uTrialConts(0), m_uTrialEvents(0), m_uTrialComments(0) - , m_uTrialTrackings(0), m_bWithinTrial(false), m_uTrialStartTime(0), m_uCbsdkTime(0) - , m_nextTrialStartTime(0), m_CD(nullptr), m_ED(nullptr), m_CMT(nullptr), m_TR(nullptr) -{ - memset(&m_lastPktVideoSynch, 0, sizeof(m_lastPktVideoSynch)); - memset(&m_bChannelMask, 0, sizeof(m_bChannelMask)); - memset(&m_lastPktVideoSynch, 0, sizeof(m_lastPktVideoSynch)); - memset(&m_lastLost, 0, sizeof(m_lastLost)); - memset(&m_lastInstInfo, 0, sizeof(m_lastInstInfo)); - for (int i = 0; i < CBSDKCALLBACK_COUNT; ++i) - { - m_pCallback[i] = nullptr; - m_pCallbackParams[i] = nullptr; - m_pLateCallback[i] = nullptr; - m_pLateCallbackParams[i] = nullptr; - } -} - -// Author & Date: Ehsan Azar 29 April 2012 -/// Sdk app base destructor -SdkApp::~SdkApp() -{ - // Close networking - Close(); -} - -// Author & Date: Ehsan Azar 29 April 2012 -/** Open sdk application, network and Qt message loop. -* -* @param[in] nInstance instance ID -* @param[in] nInPort Client port number -* @param[in] nOutPort Instrument port number -* @param[in] szInIP Client IPv4 address -* @param[in] szOutIP Instrument IPv4 address -* @param[in] nRecBufSize -* @param[in] nRange Unused -*/ -void SdkApp::Open(const uint32_t nInstance, const int nInPort, const int nOutPort, const LPCSTR szInIP, const LPCSTR szOutIP, const int nRecBufSize, int nRange) -{ - // clear las library error - m_lastCbErr = cbRESULT_OK; - // Close networking thread if already running - Close(); - // One-time initialization - if (!m_bInitialized) - { - m_bInitialized = true; - // InstNetworkEvent now directly calls OnInstNetworkEvent (virtual function call) - // Add myself as the sole listener - InstNetwork::Open(this); - } - // instance id and connection details are persistent in the process - m_nInstance = nInstance; - m_nInPort = nInPort; - m_nOutPort = nOutPort; - m_nRecBufSize = nRecBufSize; - m_strInIP = szInIP; - m_strOutIP = szOutIP; - -#ifndef WIN32 - // On Linux bind to broadcast - if (m_strInIP.size() >= 4 && m_strInIP.substr(m_strInIP.size() - 4) == ".255") - m_bBroadcast = true; -#endif - - // Restart networking thread - Start(); -} - -// Author & Date: Ehsan Azar 29 April 2012 -/// Proxy for all incoming packets -void SdkApp::ProcessIncomingPacket(const cbPKT_GENERIC * const pPkt) -{ - // This is a hot code path, and crosses the shared library - // we want as minimal locking as possible - // we also want to reduce deadlock if someone calls unregister within the callback itself - // thus we late bind callback when a change is noticed and only when needed, - // then use the late bound callback function - // As a rule of thumb, no locks should be active before any callback is called - - // This callback type needs to be bound only once - LateBindCallback(CBSDKCALLBACK_ALL); - bool b_checkEvent = false; - - // check for configuration class packets - if (pPkt->cbpkt_header.chid & cbPKTCHAN_CONFIGURATION) - { - // Check for configuration packets - if (pPkt->cbpkt_header.chid == cbPKTCHAN_CONFIGURATION) - { - if (pPkt->cbpkt_header.type == cbPKTTYPE_SYSHEARTBEAT) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_SYSHEARTBEAT, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_SYSHEARTBEAT); - if (m_pLateCallback[CBSDKCALLBACK_SYSHEARTBEAT]) - m_pLateCallback[CBSDKCALLBACK_SYSHEARTBEAT](m_nInstance, cbSdkPkt_SYSHEARTBEAT, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_SYSHEARTBEAT]); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_REPIMPEDANCE) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_IMPEDANCE, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_IMPEDANCE); - if (m_pLateCallback[CBSDKCALLBACK_IMPEDANCE]) - m_pLateCallback[CBSDKCALLBACK_IMPEDANCE](m_nInstance, cbSdkPkt_IMPEDANCE, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_IMPEDANCE]); - } - else if ((pPkt->cbpkt_header.type & 0xF0) == cbPKTTYPE_CHANREP) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_CHANINFO, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_CHANINFO); - if (m_pLateCallback[CBSDKCALLBACK_CHANINFO]) - m_pLateCallback[CBSDKCALLBACK_CHANINFO](m_nInstance, cbSdkPkt_CHANINFO, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_CHANINFO]); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_NMREP) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_NM, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_NM); - if (m_pLateCallback[CBSDKCALLBACK_NM]) - m_pLateCallback[CBSDKCALLBACK_NM](m_nInstance, cbSdkPkt_NM, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_NM]); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_GROUPREP) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_GROUPINFO, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_GROUPINFO); - if (m_pLateCallback[CBSDKCALLBACK_GROUPINFO]) - m_pLateCallback[CBSDKCALLBACK_GROUPINFO](m_nInstance, cbSdkPkt_GROUPINFO, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_GROUPINFO]); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_COMMENTREP) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_COMMENT, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_COMMENT); - if (m_pLateCallback[CBSDKCALLBACK_COMMENT]) - m_pLateCallback[CBSDKCALLBACK_COMMENT](m_nInstance, cbSdkPkt_COMMENT, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_COMMENT]); - // Fillout trial if setup - OnPktComment(reinterpret_cast(pPkt)); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_REPFILECFG) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_FILECFG, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_FILECFG); - if (m_pLateCallback[CBSDKCALLBACK_FILECFG]) - m_pLateCallback[CBSDKCALLBACK_FILECFG](m_nInstance, cbSdkPkt_FILECFG, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_COMMENT]); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_REPPOLL) - { - // The callee should check flags to find if it is a response to poll, and do accordingly - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_POLL, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_POLL); - if (m_pLateCallback[CBSDKCALLBACK_POLL]) - m_pLateCallback[CBSDKCALLBACK_POLL](m_nInstance, cbSdkPkt_POLL, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_POLL]); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_VIDEOTRACKREP) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_TRACKING, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_TRACKING); - if (m_pLateCallback[CBSDKCALLBACK_TRACKING]) - m_pLateCallback[CBSDKCALLBACK_TRACKING](m_nInstance, cbSdkPkt_TRACKING, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_TRACKING]); - // Fillout trial if setup - OnPktTrack(reinterpret_cast(pPkt)); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_VIDEOSYNCHREP) - { - m_lastPktVideoSynch = *reinterpret_cast(pPkt); - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_SYNCH, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_SYNCH); - if (m_pLateCallback[CBSDKCALLBACK_SYNCH]) - m_pLateCallback[CBSDKCALLBACK_SYNCH](m_nInstance, cbSdkPkt_SYNCH, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_SYNCH]); - } - else if (pPkt->cbpkt_header.type == cbPKTTYPE_LOGREP) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_LOG, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_LOG); - if (m_pLateCallback[CBSDKCALLBACK_LOG]) - m_pLateCallback[CBSDKCALLBACK_LOG](m_nInstance, cbSdkPkt_LOG, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_LOG]); - // Fill out trial if setup - OnPktLog(reinterpret_cast(pPkt)); - //OnPktComment(reinterpret_cast(pPkt)); - } - } // end if (pPkt->chid==0x8000 - } // end if (pPkt->chid & 0x8000 - else if (pPkt->cbpkt_header.chid == 0) - { - // No mask applied here - // Inside the callback cbPKT_GROUP.type can be used to find the sample group number - if (const uint8_t smpGroup = ((cbPKT_GROUP *)pPkt)->cbpkt_header.type; smpGroup > 0 && smpGroup <= cbMAXGROUPS) - { - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_CONTINUOUS, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_CONTINUOUS); - if (m_pLateCallback[CBSDKCALLBACK_CONTINUOUS]) - m_pLateCallback[CBSDKCALLBACK_CONTINUOUS](m_nInstance, cbSdkPkt_CONTINUOUS, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_CONTINUOUS]); - } - } - // check for channel event packets cerebus channels 1-272 - else if (IsChanAnalogIn(pPkt->cbpkt_header.chid)) - { - if (m_bChannelMask[pPkt->cbpkt_header.chid - 1]) - { - b_checkEvent = true; - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_SPIKE, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_SPIKE); - if (m_pLateCallback[CBSDKCALLBACK_SPIKE]) - m_pLateCallback[CBSDKCALLBACK_SPIKE](m_nInstance, cbSdkPkt_SPIKE, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_SPIKE]); - } - } - // catch digital input port events and save them as NSAS experiment event packets - else if (IsChanDigin(pPkt->cbpkt_header.chid)) - { - if (m_bChannelMask[pPkt->cbpkt_header.chid - 1]) - { - b_checkEvent = true; - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_DIGITAL, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_DIGITAL); - if (m_pLateCallback[CBSDKCALLBACK_DIGITAL]) - m_pLateCallback[CBSDKCALLBACK_DIGITAL](m_nInstance, cbSdkPkt_DIGITAL, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_DIGITAL]); - } - } - // catch serial input port events and save them as NSAS experiment event packets - else if (IsChanSerial(pPkt->cbpkt_header.chid)) - { - if (m_bChannelMask[pPkt->cbpkt_header.chid - 1]) - { - b_checkEvent = true; - if (m_pLateCallback[CBSDKCALLBACK_ALL]) - m_pLateCallback[CBSDKCALLBACK_ALL](m_nInstance, cbSdkPkt_SERIAL, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_ALL]); - // Late bind before usage - LateBindCallback(CBSDKCALLBACK_SERIAL); - if (m_pLateCallback[CBSDKCALLBACK_SERIAL]) - m_pLateCallback[CBSDKCALLBACK_SERIAL](m_nInstance, cbSdkPkt_SERIAL, pPkt, m_pLateCallbackParams[CBSDKCALLBACK_SERIAL]); - } - } - - // save the timestamp to overcome the case where the reset button is pressed - // (or recording started) which resets the timestamp to 0, but Central doesn't - // reset its timer to 0 for cbGetSystemClockTime - m_uCbsdkTime = pPkt->cbpkt_header.time; - - // Process continuous data if we're within a trial... - if (pPkt->cbpkt_header.chid == 0) - OnPktGroup(reinterpret_cast(pPkt)); - - // and only look at event data packets - if (b_checkEvent) - OnPktEvent(pPkt); -} - -// Author & Date: Ehsan Azar 29 April 2012 -/// Network events -void SdkApp::OnInstNetworkEvent(const NetEventType type, const unsigned int code) -{ - cbSdkPktLostEvent lostEvent; - switch (type) - { - case NET_EVENT_INSTINFO: - m_connectLock.lock(); - m_connectWait.notify_all(); - m_connectLock.unlock(); - InstInfoEvent(m_instInfo); - break; - case NET_EVENT_CLOSE: - m_instInfo = 0; - m_connectLock.lock(); - m_connectWait.notify_all(); - m_connectLock.unlock(); - InstInfoEvent(m_instInfo); - break; - case NET_EVENT_CBERR: - m_lastCbErr = code; - m_instInfo = 0; - m_connectLock.lock(); - m_connectWait.notify_all(); - m_connectLock.unlock(); - break; - case NET_EVENT_NETOPENERR: - m_lastCbErr = code; - m_instInfo = 0; - m_connectLock.lock(); - m_connectWait.notify_all(); - m_connectLock.unlock(); - lostEvent.type = CBSDKPKTLOSTEVENT_NET; - LinkFailureEvent(lostEvent); - break; - case NET_EVENT_LINKFAILURE: - lostEvent.type = CBSDKPKTLOSTEVENT_LINKFAILURE; - LinkFailureEvent(lostEvent); - break; - case NET_EVENT_PCTONSPLOST: - lostEvent.type = CBSDKPKTLOSTEVENT_PC2NSP; - LinkFailureEvent(lostEvent); - break; - default: - // Ignore other events - break; - } -} diff --git a/src/cbsdk/include/cbsdk/cbsdk.h b/src/cbsdk/include/cbsdk/cbsdk.h new file mode 100644 index 00000000..59da58b6 --- /dev/null +++ b/src/cbsdk/include/cbsdk/cbsdk.h @@ -0,0 +1,973 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file cbsdk.h +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief C API for CereLink SDK v2 +/// +/// This is the main C API for cbsdk. It provides a clean +/// C interface that wraps the C++ SdkSession implementation. +/// If you are using C++, consider using sdk_session.h instead. +/// This API is designed for easy FFI usage from other languages. +/// +/// Key Design Principles: +/// - Opaque handle pattern for session management +/// - Integer error codes for easy FFI +/// - C-style callbacks with user_data pointers +/// - Clear ownership semantics +/// +/// Example Usage: +/// @code{.c} +/// cbsdk_session_t session = NULL; +/// cbsdk_config_t config = cbsdk_config_default(); +/// config.device_type = CBPROTO_DEVICE_TYPE_HUB1; +/// +/// int result = cbsdk_session_create(&session, &config); +/// if (result != CBSDK_RESULT_SUCCESS) { +/// fprintf(stderr, "Error: %s\n", cbsdk_get_error_message(result)); +/// return 1; +/// } +/// +/// cbsdk_session_set_packet_callback(session, my_packet_callback, user_data); +/// // ... do work ... +/// cbsdk_session_destroy(session); +/// @endcode +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBSDK_V2_CBSDK_H +#define CBSDK_V2_CBSDK_H + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// DLL Export/Import +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#if defined(CBSDK_SHARED) + #if defined(_WIN32) || defined(__CYGWIN__) + #if defined(CBSDK_EXPORTS) + #define CBSDK_API __declspec(dllexport) + #else + #define CBSDK_API __declspec(dllimport) + #endif + #elif defined(__GNUC__) && __GNUC__ >= 4 + #define CBSDK_API __attribute__((visibility("default"))) + #else + #define CBSDK_API + #endif +#else + #define CBSDK_API +#endif + +// Protocol types (need for callbacks) +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Result Codes +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Result codes returned by cbsdk functions +typedef enum { + CBSDK_RESULT_SUCCESS = 0, ///< Operation succeeded + CBSDK_RESULT_INVALID_PARAMETER = -1, ///< Invalid parameter (NULL pointer, etc.) + CBSDK_RESULT_ALREADY_RUNNING = -2, ///< Session is already running + CBSDK_RESULT_NOT_RUNNING = -3, ///< Session is not running + CBSDK_RESULT_SHMEM_ERROR = -4, ///< Shared memory error + CBSDK_RESULT_DEVICE_ERROR = -5, ///< Device connection error + CBSDK_RESULT_INTERNAL_ERROR = -6, ///< Internal error +} cbsdk_result_t; + +/// Channel info field selector for bulk extraction +typedef enum { + CBSDK_CHANINFO_FIELD_SMPGROUP = 0, + CBSDK_CHANINFO_FIELD_SMPFILTER = 1, + CBSDK_CHANINFO_FIELD_SPKFILTER = 2, + CBSDK_CHANINFO_FIELD_AINPOPTS = 3, + CBSDK_CHANINFO_FIELD_SPKOPTS = 4, + CBSDK_CHANINFO_FIELD_SPKTHRLEVEL = 5, + CBSDK_CHANINFO_FIELD_LNCRATE = 6, + CBSDK_CHANINFO_FIELD_REFELECCHAN = 7, + CBSDK_CHANINFO_FIELD_AMPLREJPOS = 8, + CBSDK_CHANINFO_FIELD_AMPLREJNEG = 9, + CBSDK_CHANINFO_FIELD_CHANCAPS = 10, + CBSDK_CHANINFO_FIELD_BANK = 11, + CBSDK_CHANINFO_FIELD_TERM = 12, +} cbsdk_chaninfo_field_t; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Structures +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// SDK configuration (C version of SdkConfig) +typedef struct { + // Device type (automatically maps to correct address/port and shared memory name) + // Used only when creating new shared memory (STANDALONE mode) + cbproto_device_type_t device_type; ///< Device type to connect to + + // Callback thread configuration + size_t callback_queue_depth; ///< Packets to buffer (default: 16384) + bool enable_realtime_priority; ///< Elevated thread priority + bool drop_on_overflow; ///< Drop oldest on overflow (vs newest) + + // Advanced options + int recv_buffer_size; ///< UDP receive buffer size (default: 6MB) + bool non_blocking; ///< Non-blocking sockets + + // Optional custom device configuration (overrides device_type mapping) + // Use NULL/0 for automatic detection based on device_type + const char* custom_device_address; ///< Override device IP (NULL = auto) + const char* custom_client_address; ///< Override client IP (NULL = auto) + uint16_t custom_device_port; ///< Override device port (0 = auto) + uint16_t custom_client_port; ///< Override client port (0 = auto) +} cbsdk_config_t; + +/// SDK statistics (C version of SdkStats) +typedef struct { + // Device statistics + uint64_t packets_received_from_device; ///< Packets from UDP socket + uint64_t bytes_received_from_device; ///< Bytes from UDP socket + + // Shared memory statistics + uint64_t packets_stored_to_shmem; ///< Packets written to shmem + + // Callback queue statistics + uint64_t packets_queued_for_callback; ///< Packets added to queue + uint64_t packets_delivered_to_callback; ///< Packets delivered to user + uint64_t packets_dropped; ///< Dropped due to queue overflow + uint64_t queue_current_depth; ///< Current queue usage + uint64_t queue_max_depth; ///< Peak queue usage + + // Transmit statistics (STANDALONE mode only) + uint64_t packets_sent_to_device; ///< Packets sent to device + + // Error counters + uint64_t shmem_store_errors; ///< Failed to store to shmem + uint64_t receive_errors; ///< Socket receive errors + uint64_t send_errors; ///< Socket send errors +} cbsdk_stats_t; + +/// Channel scaling information (mirrors cbSCALING from cbproto) +typedef struct { + int16_t digmin; ///< Digital value corresponding to anamin + int16_t digmax; ///< Digital value corresponding to anamax + int32_t anamin; ///< Minimum analog value + int32_t anamax; ///< Maximum analog value + int32_t anagain; ///< Gain applied to analog values + char anaunit[8]; ///< Unit string (e.g., "uV", "mV", "MPa") +} cbsdk_channel_scaling_t; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Callback Types +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Callback handle for unregistering typed callbacks +typedef uint32_t cbsdk_callback_handle_t; + +/// User callback for received packets (all packet types) +/// @param pkts Pointer to array of packets +/// @param count Number of packets in array +/// @param user_data User data pointer passed to registration function +typedef void (*cbsdk_packet_callback_fn)(const cbPKT_GENERIC* pkts, size_t count, void* user_data); + +/// Event callback for spike/digital/serial event packets +/// @param pkt Pointer to the event packet +/// @param user_data User data pointer passed to registration function +typedef void (*cbsdk_event_callback_fn)(const cbPKT_GENERIC* pkt, void* user_data); + +/// Group callback for continuous sample data packets (chid == 0) +/// @param pkt Pointer to the group packet +/// @param user_data User data pointer passed to registration function +typedef void (*cbsdk_group_callback_fn)(const cbPKT_GROUP* pkt, void* user_data); + +/// Config callback for system/configuration packets (chid & 0x8000) +/// @param pkt Pointer to the config packet +/// @param user_data User data pointer passed to registration function +typedef void (*cbsdk_config_callback_fn)(const cbPKT_GENERIC* pkt, void* user_data); + +/// Error callback for queue overflow and other errors +/// @param error_message Description of the error (null-terminated string) +/// @param user_data User data pointer passed to registration function +typedef void (*cbsdk_error_callback_fn)(const char* error_message, void* user_data); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Opaque Handle +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Opaque session handle (do not access fields directly) +typedef struct cbsdk_session_impl* cbsdk_session_t; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Functions +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get default SDK configuration +/// @return Default configuration structure +CBSDK_API cbsdk_config_t cbsdk_config_default(void); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Management +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Create a new SDK session +/// @param[out] session Pointer to receive session handle (must not be NULL) +/// @param[in] config Configuration (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_create(cbsdk_session_t* session, const cbsdk_config_t* config); + +/// Destroy an SDK session and free resources +/// @param session Session handle (can be NULL) +CBSDK_API void cbsdk_session_destroy(cbsdk_session_t session); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Control +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Start receiving packets from device +/// @param session Session handle (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_start(cbsdk_session_t session); + +/// Stop receiving packets +/// @param session Session handle (must not be NULL) +CBSDK_API void cbsdk_session_stop(cbsdk_session_t session); + +/// Check if session is running +/// @param session Session handle (must not be NULL) +/// @return true if running, false otherwise +CBSDK_API bool cbsdk_session_is_running(cbsdk_session_t session); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Callbacks +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Set callback for all received packets (legacy convenience -- registers a PACKET callback) +/// @param session Session handle (must not be NULL) +/// @param callback Callback function (can be NULL to clear) +/// @param user_data User data pointer passed to callback +CBSDK_API void cbsdk_session_set_packet_callback(cbsdk_session_t session, + cbsdk_packet_callback_fn callback, + void* user_data); + +/// Set callback for errors (queue overflow, etc.) +/// @param session Session handle (must not be NULL) +/// @param callback Callback function (can be NULL to clear) +/// @param user_data User data pointer passed to callback +CBSDK_API void cbsdk_session_set_error_callback(cbsdk_session_t session, + cbsdk_error_callback_fn callback, + void* user_data); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Typed Callback Registration +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Register callback for all packets (catch-all) +/// @param session Session handle (must not be NULL) +/// @param callback Callback function (must not be NULL) +/// @param user_data User data pointer passed to callback +/// @return Handle for unregistration, or 0 on failure +CBSDK_API cbsdk_callback_handle_t cbsdk_session_register_packet_callback( + cbsdk_session_t session, + cbsdk_packet_callback_fn callback, + void* user_data); + +/// Register callback for event packets (spikes, digital events, etc.) +/// @param session Session handle (must not be NULL) +/// @param channel_type Channel type filter (use CBPROTO_CHANNEL_TYPE_FRONTEND for spikes, etc.) +/// Pass -1 (cast to cbproto_channel_type_t) for all event channels. +/// @param callback Callback function (must not be NULL) +/// @param user_data User data pointer passed to callback +/// @return Handle for unregistration, or 0 on failure +CBSDK_API cbsdk_callback_handle_t cbsdk_session_register_event_callback( + cbsdk_session_t session, + cbproto_channel_type_t channel_type, + cbsdk_event_callback_fn callback, + void* user_data); + +/// Register callback for continuous sample group packets +/// @param session Session handle (must not be NULL) +/// @param rate Sample rate to match (CBPROTO_GROUP_RATE_500Hz through CBPROTO_GROUP_RATE_RAW) +/// @param callback Callback function (must not be NULL) +/// @param user_data User data pointer passed to callback +/// @return Handle for unregistration, or 0 on failure +CBSDK_API cbsdk_callback_handle_t cbsdk_session_register_group_callback( + cbsdk_session_t session, + cbproto_group_rate_t rate, + cbsdk_group_callback_fn callback, + void* user_data); + +/// Register callback for config/system packets +/// @param session Session handle (must not be NULL) +/// @param packet_type Packet type to match (e.g., cbPKTTYPE_COMMENTREP, cbPKTTYPE_SYSREPRUNLEV) +/// @param callback Callback function (must not be NULL) +/// @param user_data User data pointer passed to callback +/// @return Handle for unregistration, or 0 on failure +CBSDK_API cbsdk_callback_handle_t cbsdk_session_register_config_callback( + cbsdk_session_t session, + uint16_t packet_type, + cbsdk_config_callback_fn callback, + void* user_data); + +/// Unregister a previously registered callback +/// @param session Session handle (must not be NULL) +/// @param handle Handle returned by a register_*_callback function +CBSDK_API void cbsdk_session_unregister_callback(cbsdk_session_t session, + cbsdk_callback_handle_t handle); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Statistics & Monitoring +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get current statistics +/// @param session Session handle (must not be NULL) +/// @param[out] stats Pointer to receive statistics (must not be NULL) +CBSDK_API void cbsdk_session_get_stats(cbsdk_session_t session, cbsdk_stats_t* stats); + +/// Reset statistics counters to zero +/// @param session Session handle (must not be NULL) +CBSDK_API void cbsdk_session_reset_stats(cbsdk_session_t session); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Access (read from shared memory -- always up-to-date) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get current device run level +/// @param session Session handle (must not be NULL) +/// @return Current run level (cbRUNLEVEL_*), or 0 if unknown +CBSDK_API uint32_t cbsdk_session_get_runlevel(cbsdk_session_t session); + +/// Get the protocol version used by this session (STANDALONE mode only) +/// @param session Session handle (must not be NULL) +/// @return Protocol version (cbproto_protocol_version_t), or 0 (UNKNOWN) if unavailable +CBSDK_API uint32_t cbsdk_session_get_protocol_version(cbsdk_session_t session); + +/// Get the processor identification string (e.g. "Gemini Hub 1") +/// @param session Session handle (must not be NULL) +/// @param buf Output buffer for the ident string +/// @param buf_size Size of the output buffer in bytes +/// @return Number of bytes written (excluding null terminator), or 0 if unavailable +CBSDK_API uint32_t cbsdk_session_get_proc_ident(cbsdk_session_t session, char* buf, uint32_t buf_size); + +/// Get the global spike event length (samples per spike waveform) +/// @param session Session handle (must not be NULL) +/// @return Spike length in samples, or 0 if unavailable +CBSDK_API uint32_t cbsdk_session_get_spike_length(cbsdk_session_t session); + +/// Get the global spike pre-trigger length (samples before threshold crossing) +/// @param session Session handle (must not be NULL) +/// @return Pre-trigger length in samples, or 0 if unavailable +CBSDK_API uint32_t cbsdk_session_get_spike_pretrigger(cbsdk_session_t session); + +/// Set the global spike event length and pre-trigger +/// @param session Session handle (must not be NULL) +/// @param spike_length Total spike waveform length in samples +/// @param spike_pretrigger Pre-trigger samples (must be < spike_length) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_spike_length( + cbsdk_session_t session, + uint32_t spike_length, + uint32_t spike_pretrigger); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Information Accessors +// +// These provide FFI-friendly access to channel/group configuration without +// requiring the full struct layouts. Pointers are valid for the session lifetime. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get the number of channels +/// @return cbMAXCHANS (compile-time constant) +CBSDK_API uint32_t cbsdk_get_max_chans(void); + +/// Get the number of front-end channels +/// @return cbNUM_FE_CHANS (compile-time constant) +CBSDK_API uint32_t cbsdk_get_num_fe_chans(void); + +/// Get the number of analog channels +/// @return cbNUM_ANALOG_CHANS (compile-time constant) +CBSDK_API uint32_t cbsdk_get_num_analog_chans(void); + +/// Get a channel's label +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Pointer to null-terminated label string, or NULL if invalid. +/// Pointer is valid for the lifetime of the session. +CBSDK_API const char* cbsdk_session_get_channel_label(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's sample group assignment +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Sample group (0-6, where 0 = disabled), or 0 on error +CBSDK_API uint32_t cbsdk_session_get_channel_smpgroup(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's capabilities flags +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Channel capabilities (cbCHAN_* flags), or 0 on error +CBSDK_API uint32_t cbsdk_session_get_channel_chancaps(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's type classification based on its capabilities +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Channel type, or -1 if invalid +CBSDK_API cbproto_channel_type_t cbsdk_session_get_channel_type(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's continuous-time pathway filter ID +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Filter ID, or 0 on error +CBSDK_API uint32_t cbsdk_session_get_channel_smpfilter(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's spike pathway filter ID +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Filter ID, or 0 on error +CBSDK_API uint32_t cbsdk_session_get_channel_spkfilter(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's spike processing options +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Spike options (cbAINPSPK_* flags), or 0 on error +CBSDK_API uint32_t cbsdk_session_get_channel_spkopts(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's spike threshold level +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Spike threshold level, or 0 on error +CBSDK_API int32_t cbsdk_session_get_channel_spkthrlevel(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's analog input options (LNC, reference electrode, etc.) +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Analog input options (cbAINP_* flags), or 0 on error +CBSDK_API uint32_t cbsdk_session_get_channel_ainpopts(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's line noise cancellation adaptation rate +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return LNC rate, or 0 on error +CBSDK_API uint32_t cbsdk_session_get_channel_lncrate(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's software reference electrode channel +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Reference electrode channel ID, or 0 on error +CBSDK_API uint32_t cbsdk_session_get_channel_refelecchan(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's positive amplitude rejection threshold +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Positive amplitude rejection value, or 0 on error +CBSDK_API int16_t cbsdk_session_get_channel_amplrejpos(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's negative amplitude rejection threshold +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @return Negative amplitude rejection value, or 0 on error +CBSDK_API int16_t cbsdk_session_get_channel_amplrejneg(cbsdk_session_t session, uint32_t chan_id); + +/// Get a channel's input scaling information (user-defined scalin) +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param[out] scaling Pointer to struct to fill with scaling data +/// @return CBSDK_RESULT_SUCCESS on success, error code otherwise +CBSDK_API cbsdk_result_t cbsdk_session_get_channel_scaling( + cbsdk_session_t session, uint32_t chan_id, cbsdk_channel_scaling_t* scaling); + +/// Get any numeric field from a single channel by field selector +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param field Field to extract +/// @return Field value widened to int64_t, or 0 on error +CBSDK_API int64_t cbsdk_session_get_channel_field( + cbsdk_session_t session, + uint32_t chan_id, + cbsdk_chaninfo_field_t field); + +/// Get a sample group's label +/// @param session Session handle (must not be NULL) +/// @param group_id Group ID (1-6) +/// @return Pointer to null-terminated label string, or NULL if invalid +CBSDK_API const char* cbsdk_session_get_group_label(cbsdk_session_t session, uint32_t group_id); + +/// Get the list of channels in a sample group +/// @param session Session handle (must not be NULL) +/// @param group_id Group ID (1-6) +/// @param[out] list Pointer to receive channel list (caller-allocated, max cbNUM_ANALOG_CHANS entries) +/// @param[in,out] count On input: size of list array. On output: number of channels written. +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_get_group_list( + cbsdk_session_t session, + uint32_t group_id, + uint16_t* list, + uint32_t* count); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Configuration +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Set sampling rate for channels of a specific type +/// @param session Session handle (must not be NULL) +/// @param n_chans Number of channels to configure (use cbMAXCHANS for all) +/// @param chan_type Channel type filter +/// @param rate Sample rate (CBPROTO_GROUP_RATE_NONE to disable, _500Hz through _RAW) +/// @param disable_others If true, disable sampling on unselected channels of this type +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_sample_group( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + cbproto_group_rate_t rate, + bool disable_others); + +/// Set AC input coupling (offset correction) for channels of a specific type +/// @param session Session handle (must not be NULL) +/// @param n_chans Number of channels to configure (use cbMAXCHANS for all) +/// @param chan_type Channel type filter +/// @param enabled true = AC coupling, false = DC coupling +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_ac_input_coupling( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + bool enabled); + +/// Set full channel configuration by sending a CHANINFO packet +/// @param session Session handle (must not be NULL) +/// @param chaninfo Complete channel info packet to send +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_config( + cbsdk_session_t session, + const cbPKT_CHANINFO* chaninfo); + +/// Set a channel's label +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param label New label string (max 15 chars, null-terminated) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_label( + cbsdk_session_t session, + uint32_t chan_id, + const char* label); + +/// Set a channel's continuous-time pathway filter +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param filter_id Filter ID (0 to cbMAXFILTS-1) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_smpfilter( + cbsdk_session_t session, + uint32_t chan_id, + uint32_t filter_id); + +/// Set a channel's spike pathway filter +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param filter_id Filter ID (0 to cbMAXFILTS-1) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_spkfilter( + cbsdk_session_t session, + uint32_t chan_id, + uint32_t filter_id); + +/// Set a channel's analog input options (LNC mode, reference electrode, etc.) +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param ainpopts Analog input option flags (cbAINP_* flags) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_ainpopts( + cbsdk_session_t session, + uint32_t chan_id, + uint32_t ainpopts); + +/// Set a channel's line noise cancellation adaptation rate +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param lncrate LNC rate +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_lncrate( + cbsdk_session_t session, + uint32_t chan_id, + uint32_t lncrate); + +/// Set a channel's spike processing options +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param spkopts Spike option flags (cbAINPSPK_* flags) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_spkopts( + cbsdk_session_t session, + uint32_t chan_id, + uint32_t spkopts); + +/// Set a channel's spike threshold level +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param level Threshold level +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_spkthrlevel( + cbsdk_session_t session, + uint32_t chan_id, + int32_t level); + +/// Enable or disable auto-thresholding for a channel +/// @param session Session handle (must not be NULL) +/// @param chan_id 1-based channel ID (1 to cbMAXCHANS) +/// @param enabled true to enable, false to disable +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_autothreshold( + cbsdk_session_t session, + uint32_t chan_id, + bool enabled); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Bulk Channel Queries +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get the 1-based IDs of channels matching a type +/// @param session Session handle (must not be NULL) +/// @param n_chans Max channels to return (use cbMAXCHANS for all) +/// @param chan_type Channel type filter +/// @param[out] out_ids Caller-allocated array to receive channel IDs +/// @param[in,out] out_count On input: size of out_ids array. On output: channels written. +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_get_matching_channels( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + uint32_t* out_ids, + uint32_t* out_count); + +/// Get a numeric field from all channels matching a type +/// @param session Session handle (must not be NULL) +/// @param n_chans Max channels to query (use cbMAXCHANS for all) +/// @param chan_type Channel type filter +/// @param field Which field to extract +/// @param[out] out_values Caller-allocated array to receive field values +/// @param[in,out] out_count On input: size of out_values array. On output: values written. +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_get_channels_field( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + cbsdk_chaninfo_field_t field, + int64_t* out_values, + uint32_t* out_count); + +/// Get labels from all channels matching a type +/// @param session Session handle (must not be NULL) +/// @param n_chans Max channels to query (use cbMAXCHANS for all) +/// @param chan_type Channel type filter +/// @param[out] out_labels Caller-allocated array of char pointers (size out_count) +/// @param[out] out_buf Caller-allocated buffer for label strings (each up to 16 bytes) +/// @param out_buf_size Size of out_buf in bytes +/// @param[in,out] out_count On input: size of out_labels array. On output: labels written. +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_get_channels_labels( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + char* out_buf, + size_t label_stride, + uint32_t* out_count); + +/// Get positions from all channels matching a type +/// @param session Session handle (must not be NULL) +/// @param n_chans Max channels to query (use cbMAXCHANS for all) +/// @param chan_type Channel type filter +/// @param[out] out_positions Caller-allocated int32_t array (4 values per channel: x,y,z,w) +/// @param[in,out] out_count On input: max channels (out_positions must hold 4 * out_count int32_ts). +/// On output: number of channels written. +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_get_channels_positions( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + int32_t* out_positions, + uint32_t* out_count); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Bulk Configuration Access +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get the system sampling frequency +/// @param session Session handle (must not be NULL) +/// @return System frequency in Hz, or 0 if unavailable +CBSDK_API uint32_t cbsdk_session_get_sysfreq(cbsdk_session_t session); + +/// Get the number of available filters +/// @return cbMAXFILTS (compile-time constant) +CBSDK_API uint32_t cbsdk_get_num_filters(void); + +/// Get a filter's label +/// @param session Session handle (must not be NULL) +/// @param filter_id Filter ID (0 to cbMAXFILTS-1) +/// @return Pointer to label string, or NULL if invalid +CBSDK_API const char* cbsdk_session_get_filter_label(cbsdk_session_t session, uint32_t filter_id); + +/// Get a filter's high-pass corner frequency +/// @param session Session handle (must not be NULL) +/// @param filter_id Filter ID (0 to cbMAXFILTS-1) +/// @return High-pass frequency in milliHertz, or 0 if invalid +CBSDK_API uint32_t cbsdk_session_get_filter_hpfreq(cbsdk_session_t session, uint32_t filter_id); + +/// Get a filter's high-pass order +/// @param session Session handle (must not be NULL) +/// @param filter_id Filter ID (0 to cbMAXFILTS-1) +/// @return High-pass filter order, or 0 if invalid +CBSDK_API uint32_t cbsdk_session_get_filter_hporder(cbsdk_session_t session, uint32_t filter_id); + +/// Get a filter's low-pass corner frequency +/// @param session Session handle (must not be NULL) +/// @param filter_id Filter ID (0 to cbMAXFILTS-1) +/// @return Low-pass frequency in milliHertz, or 0 if invalid +CBSDK_API uint32_t cbsdk_session_get_filter_lpfreq(cbsdk_session_t session, uint32_t filter_id); + +/// Get a filter's low-pass order +/// @param session Session handle (must not be NULL) +/// @param filter_id Filter ID (0 to cbMAXFILTS-1) +/// @return Low-pass filter order, or 0 if invalid +CBSDK_API uint32_t cbsdk_session_get_filter_lporder(cbsdk_session_t session, uint32_t filter_id); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Commands +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Send a comment string to the device (appears in recorded data) +/// @param session Session handle (must not be NULL) +/// @param comment Comment text (max 127 chars, null-terminated) +/// @param rgba Color as RGBA uint32_t (0 = white) +/// @param charset Character set (0 = ANSI) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_send_comment( + cbsdk_session_t session, + const char* comment, + uint32_t rgba, + uint8_t charset); + +/// Send a raw packet to the device (STANDALONE mode only) +/// @param session Session handle (must not be NULL) +/// @param pkt Packet to send (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_send_packet( + cbsdk_session_t session, + const cbPKT_GENERIC* pkt); + +/// Set digital output value +/// @param session Session handle (must not be NULL) +/// @param chan_id Channel ID (1-based) of a digital output channel +/// @param value Digital output value (bitmask) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_digital_output( + cbsdk_session_t session, + uint32_t chan_id, + uint16_t value); + +/// Set system run level +/// @param session Session handle (must not be NULL) +/// @param runlevel Desired run level (cbRUNLEVEL_*) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_runlevel( + cbsdk_session_t session, + uint32_t runlevel); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Mapping (CMP) Files +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Load a channel mapping file (.cmp) and apply electrode positions +/// +/// CMP files define physical electrode positions on arrays. Because the device does not +/// persist the position field in chaninfo, positions are stored locally and overlaid +/// onto channel info whenever config data arrives from the device. +/// +/// Can be called multiple times for different ports on a Hub device. +/// +/// @param session Session handle (must not be NULL) +/// @param filepath Path to the .cmp file (must not be NULL) +/// @param bank_offset Offset added to CMP bank indices. A=1+offset, B=2+offset, etc. +/// Use 0 for port 1, 4 for port 2, 8 for port 3, etc. +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_load_channel_map( + cbsdk_session_t session, + const char* filepath, + uint32_t bank_offset); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// CCF Configuration Files +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Save the current device configuration to a CCF (XML) file +/// @param session Session handle (must not be NULL) +/// @param filename Path to the CCF file to write (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_save_ccf(cbsdk_session_t session, const char* filename); + +/// Load a CCF file and apply its configuration to the device +/// @param session Session handle (must not be NULL) +/// @param filename Path to the CCF file to read (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_load_ccf(cbsdk_session_t session, const char* filename); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Instrument Time +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get most recent device timestamp from shared memory +/// On Gemini (protocol 4.0+) this is PTP nanoseconds. +/// On legacy NSP (protocol 3.x) this is 30kHz ticks. +/// @param session Session handle (must not be NULL) +/// @param[out] time Pointer to receive raw device timestamp (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_get_time(cbsdk_session_t session, uint64_t* time); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Patient Information +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Set patient information (embedded in recorded files) +/// Must be called before starting recording. +/// @param session Session handle (must not be NULL) +/// @param id Patient identification string (must not be NULL) +/// @param firstname Patient first name (can be NULL) +/// @param lastname Patient last name (can be NULL) +/// @param dob_month Birth month (1-12, 0 = unset) +/// @param dob_day Birth day (1-31, 0 = unset) +/// @param dob_year Birth year (e.g. 1990, 0 = unset) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_patient_info( + cbsdk_session_t session, + const char* id, + const char* firstname, + const char* lastname, + uint32_t dob_month, + uint32_t dob_day, + uint32_t dob_year); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Analog Output +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Set analog output monitoring (route a channel's audio to an analog/audio output) +/// @param session Session handle (must not be NULL) +/// @param aout_chan_id 1-based channel ID of the analog/audio output channel +/// @param monitor_chan_id 1-based channel ID of the channel to monitor +/// @param track_last If true, track last channel clicked in Central +/// @param spike_only If true, monitor spike signal; if false, monitor continuous signal +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_analog_output_monitor( + cbsdk_session_t session, + uint32_t aout_chan_id, + uint32_t monitor_chan_id, + bool track_last, + bool spike_only); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Recording Control +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Start Central file recording on the device +/// @param session Session handle (must not be NULL) +/// @param filename Base filename without extension (must not be NULL) +/// @param comment Recording comment (can be NULL or empty) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_start_central_recording( + cbsdk_session_t session, + const char* filename, + const char* comment); + +/// Stop Central file recording on the device +/// @param session Session handle (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_stop_central_recording(cbsdk_session_t session); + +/// Open Central's File Storage dialog +/// Must be called before start_central_recording (wait ~250ms after for dialog to initialize) +/// @param session Session handle (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_open_central_file_dialog(cbsdk_session_t session); + +/// Close Central's File Storage dialog +/// @param session Session handle (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_close_central_file_dialog(cbsdk_session_t session); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Spike Sorting +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Set spike sorting options for channels of a specific type +/// @param session Session handle (must not be NULL) +/// @param n_chans Number of channels to configure +/// @param chan_type Channel type filter +/// @param sort_options Spike sorting option flags (cbAINPSPK_*) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_set_channel_spike_sorting( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + uint32_t sort_options); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Clock Synchronization +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get the current clock offset: device_ns - steady_clock_ns +/// @param session Session handle (must not be NULL) +/// @param[out] offset_ns Pointer to receive offset in nanoseconds +/// @return CBSDK_RESULT_SUCCESS if offset is available, CBSDK_RESULT_NOT_RUNNING if no sync data +CBSDK_API cbsdk_result_t cbsdk_session_get_clock_offset( + cbsdk_session_t session, + int64_t* offset_ns); + +/// Get the clock uncertainty (half-RTT from best probe) +/// @param session Session handle (must not be NULL) +/// @param[out] uncertainty_ns Pointer to receive uncertainty in nanoseconds +/// @return CBSDK_RESULT_SUCCESS if available, CBSDK_RESULT_NOT_RUNNING if no sync data +CBSDK_API cbsdk_result_t cbsdk_session_get_clock_uncertainty( + cbsdk_session_t session, + int64_t* uncertainty_ns); + +/// Send a clock synchronization probe to the device +/// @param session Session handle (must not be NULL) +/// @return CBSDK_RESULT_SUCCESS on success, error code on failure +CBSDK_API cbsdk_result_t cbsdk_session_send_clock_probe(cbsdk_session_t session); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Utility +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get the current value of std::chrono::steady_clock in nanoseconds since epoch. +/// Useful for correlating C++ steady_clock with other time sources (e.g., Python time.monotonic). +/// @return Nanoseconds since steady_clock epoch +CBSDK_API int64_t cbsdk_get_steady_clock_ns(void); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Error Handling +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get human-readable error message for result code +/// @param result Result code +/// @return Error message string (never NULL, always valid) +CBSDK_API const char* cbsdk_get_error_message(cbsdk_result_t result); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Version Information +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Get SDK version string +/// @return Version string (e.g., "2.0.0") +CBSDK_API const char* cbsdk_get_version(void); + +#ifdef __cplusplus +} +#endif + +#endif // CBSDK_V2_CBSDK_H diff --git a/src/cbsdk/include/cbsdk/sdk_session.h b/src/cbsdk/include/cbsdk/sdk_session.h new file mode 100644 index 00000000..8e1e0cd7 --- /dev/null +++ b/src/cbsdk/include/cbsdk/sdk_session.h @@ -0,0 +1,727 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file sdk_session.h +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief SDK session that orchestrates cbdev + cbshm +/// +/// This is the main SDK implementation that combines device communication (cbdev) with +/// shared memory management (cbshm), providing a clean API for receiving packets from +/// Cerebus devices with user callbacks. +/// +/// Architecture: +/// Device → cbdev receive thread → cbshm (fast!) → queue → callback thread → user callback +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBSDK_V2_SDK_SESSION_H +#define CBSDK_V2_SDK_SESSION_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Protocol types (from upstream) +#include +#include + +namespace cbsdk { + +template +using Result = cbutil::Result; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Lock-Free SPSC Queue (Single Producer, Single Consumer) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Lock-free ring buffer for passing packets from receive thread to callback thread +/// Uses atomic operations for wait-free enqueue/dequeue +template +class SPSCQueue { +public: + SPSCQueue() : m_head(0), m_tail(0) {} + + /// Try to push an item (returns false if queue is full) + bool push(const T& item) { + size_t current_tail = m_tail.load(std::memory_order_relaxed); + size_t next_tail = (current_tail + 1) % CAPACITY; + + if (next_tail == m_head.load(std::memory_order_acquire)) { + return false; // Queue full + } + + m_buffer[current_tail] = item; + m_tail.store(next_tail, std::memory_order_release); + return true; + } + + /// Try to pop an item (returns false if queue is empty) + bool pop(T& item) { + size_t current_head = m_head.load(std::memory_order_relaxed); + + if (current_head == m_tail.load(std::memory_order_acquire)) { + return false; // Queue empty + } + + item = m_buffer[current_head]; + m_head.store((current_head + 1) % CAPACITY, std::memory_order_release); + return true; + } + + /// Get current size (approximate, may be stale) + size_t size() const { + size_t head = m_head.load(std::memory_order_relaxed); + size_t tail = m_tail.load(std::memory_order_relaxed); + if (tail >= head) { + return tail - head; + } else { + return CAPACITY - head + tail; + } + } + + /// Get capacity + size_t capacity() const { return CAPACITY - 1; } // One slot reserved for full detection + + /// Check if empty (approximate) + bool empty() const { + return m_head.load(std::memory_order_relaxed) == m_tail.load(std::memory_order_relaxed); + } + +private: + std::array m_buffer; + alignas(64) std::atomic m_head; // Cache line alignment + alignas(64) std::atomic m_tail; +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Device type for automatic address/port configuration +enum class DeviceType { + LEGACY_NSP, ///< Legacy NSP (192.168.137.128, ports 51001/51002) + NSP, ///< Gemini NSP (192.168.137.128, port 51001 bidirectional) + HUB1, ///< Gemini Hub 1 (192.168.137.200, port 51002 bidirectional) + HUB2, ///< Gemini Hub 2 (192.168.137.201, port 51003 bidirectional) + HUB3, ///< Gemini Hub 3 (192.168.137.202, port 51004 bidirectional) + NPLAY ///< NPlay loopback (127.0.0.1, ports 51001/51002) +}; + +/// SDK configuration +struct SdkConfig { + // Device type (automatically maps to correct address/port and shared memory name) + // Used only when creating new shared memory (STANDALONE mode) + DeviceType device_type = DeviceType::LEGACY_NSP; + + // Callback thread configuration + size_t callback_queue_depth = 16384; ///< Packets to buffer (as discussed) + bool enable_realtime_priority = false; ///< Elevated thread priority + bool drop_on_overflow = true; ///< Drop oldest on overflow (vs newest) + + // Advanced options + int recv_buffer_size = 6000000; ///< UDP receive buffer (6MB) + bool non_blocking = false; ///< Non-blocking sockets (false = blocking, better for dedicated receive thread) + bool autorun = true; ///< Automatically start device (full handshake). If false, only requests configuration. + + // Optional custom device configuration (overrides device_type mapping) + // Used rarely for non-standard network configurations + std::optional custom_device_address; ///< Override device IP + std::optional custom_client_address; ///< Override client IP + std::optional custom_device_port; ///< Override device port + std::optional custom_client_port; ///< Override client port +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Statistics +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// SDK statistics +struct SdkStats { + // Device statistics + uint64_t packets_received_from_device = 0; ///< Packets from UDP socket + uint64_t bytes_received_from_device = 0; ///< Bytes from UDP socket + + // Shared memory statistics + uint64_t packets_stored_to_shmem = 0; ///< Packets written to shmem + + // Callback queue statistics + uint64_t packets_queued_for_callback = 0; ///< Packets added to queue + uint64_t packets_delivered_to_callback = 0; ///< Packets delivered to user + uint64_t packets_dropped = 0; ///< Dropped due to queue overflow + uint64_t queue_current_depth = 0; ///< Current queue usage + uint64_t queue_max_depth = 0; ///< Peak queue usage + + // Transmit statistics (STANDALONE mode only) + uint64_t packets_sent_to_device = 0; ///< Packets sent to device + + // Error counters + uint64_t shmem_store_errors = 0; ///< Failed to store to shmem + uint64_t receive_errors = 0; ///< Socket receive errors + uint64_t send_errors = 0; ///< Socket send errors + + void reset() { + packets_received_from_device = 0; + bytes_received_from_device = 0; + packets_stored_to_shmem = 0; + packets_queued_for_callback = 0; + packets_delivered_to_callback = 0; + packets_dropped = 0; + queue_current_depth = 0; + queue_max_depth = 0; + packets_sent_to_device = 0; + shmem_store_errors = 0; + receive_errors = 0; + send_errors = 0; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Type (for typed event callbacks) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Channel type classification for event callback filtering +enum class ChannelType { + ANY, ///< Matches all event channels (catch-all) + FRONTEND, ///< Front-end electrode channels (1..cbNUM_FE_CHANS) + ANALOG_IN, ///< Analog input channels + ANALOG_OUT, ///< Analog output channels + AUDIO, ///< Audio output channels + DIGITAL_IN, ///< Digital input channels + SERIAL, ///< Serial input channels + DIGITAL_OUT, ///< Digital output channels +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Sample Rate (maps to Cerebus sampling groups) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Sampling rate enumeration — wraps Cerebus group IDs with human-readable names. +/// +/// Each value corresponds to a hardware sampling group: +/// NONE=0 (disabled), SR_500=1, SR_1kHz=2, SR_2kHz=3, +/// SR_10kHz=4, SR_30kHz=5, SR_RAW=6. +enum class SampleRate : uint32_t { + NONE = 0, ///< Sampling disabled + SR_500 = 1, ///< 500 Hz + SR_1kHz = 2, ///< 1 kHz + SR_2kHz = 3, ///< 2 kHz + SR_10kHz = 4, ///< 10 kHz + SR_30kHz = 5, ///< 30 kHz + SR_RAW = 6 ///< 30 kHz raw (unfiltered) +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Info Field (for bulk getters) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Selects a numeric field from cbPKT_CHANINFO for bulk extraction. +enum class ChanInfoField : uint32_t { + SMPGROUP, ///< uint32_t — sampling group (0=disabled, 1–6) + SMPFILTER, ///< uint32_t — continuous-time filter ID + SPKFILTER, ///< uint32_t — spike pathway filter ID + AINPOPTS, ///< uint32_t — analog input option flags (cbAINP_*) + SPKOPTS, ///< uint32_t — spike processing option flags + SPKTHRLEVEL, ///< int32_t — spike threshold level + LNCRATE, ///< uint32_t — LNC adaptation rate + REFELECCHAN, ///< uint32_t — reference electrode channel + AMPLREJPOS, ///< int16_t — positive amplitude rejection + AMPLREJNEG, ///< int16_t — negative amplitude rejection + CHANCAPS, ///< uint32_t — channel capability flags + BANK, ///< uint32_t — bank index + TERM, ///< uint32_t — terminal index within bank +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Callback Types +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Callback handle for unregistering callbacks +using CallbackHandle = uint32_t; + +/// Generic callback for all packets (runs on callback thread, can be slow) +/// @param pkt The received packet +using PacketCallback = std::function; + +/// Event callback for spike/digital/serial event packets (chid = 1..cbMAXCHANS) +/// @param pkt The received event packet +using EventCallback = std::function; + +/// Group callback for continuous sample data packets (chid == 0) +/// @param pkt The received group packet (pkt.cbpkt_header.type is the group ID 1-6) +using GroupCallback = std::function; + +/// Config callback for system/configuration packets (chid & 0x8000) +/// @param pkt The received config packet +using ConfigCallback = std::function; + +/// Error callback for queue overflow and other errors +/// @param error_message Description of the error +using ErrorCallback = std::function; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// SdkSession - Main API +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// SDK session that orchestrates device communication and shared memory +/// +/// This class combines cbdev (device transport) and cbshm (shared memory) into a unified +/// API with a two-stage pipeline: +/// 1. Receive thread (cbdev) → stores to cbshm (fast path, microseconds) +/// 2. Callback thread → delivers to user callback (can be slow) +/// +/// Example usage: +/// @code +/// SdkConfig config; +/// config.device_type = DeviceType::HUB1; +/// +/// auto result = SdkSession::create(config); +/// if (result.isOk()) { +/// auto& session = result.value(); +/// +/// // Register typed callbacks (all run on callback thread, off the queue) +/// session.registerEventCallback(ChannelType::FRONTEND, [](const cbPKT_GENERIC& pkt) { +/// // Handle spike events +/// }); +/// session.registerGroupCallback(5, [](const cbPKT_GENERIC& pkt) { +/// // Handle 30kHz continuous data +/// }); +/// +/// session.start(); +/// // ... do work ... +/// session.stop(); +/// } +/// @endcode +class SdkSession { +public: + /// Non-copyable (owns resources) + SdkSession(const SdkSession&) = delete; + SdkSession& operator=(const SdkSession&) = delete; + + /// Movable + SdkSession(SdkSession&&) noexcept; + SdkSession& operator=(SdkSession&&) noexcept; + + /// Destructor - stops session and cleans up resources + ~SdkSession(); + + /// Create and initialize an SDK session + /// @param config SDK configuration + /// @return Result containing session on success, error message on failure + static Result create(const SdkConfig& config); + + ///-------------------------------------------------------------------------------------------- + /// Session Control + ///-------------------------------------------------------------------------------------------- + + /// Start receiving packets from device + /// Starts both receive thread (cbdev) and callback thread + /// @return Result indicating success or error + Result start(); + + /// Stop receiving packets + /// Stops both receive and callback threads, waits for clean shutdown + void stop(); + + /// Check if session is running + /// @return true if started and receiving packets + bool isRunning() const; + + ///-------------------------------------------------------------------------------------------- + /// Callbacks (all run on dedicated callback thread, off the queue — may be slow) + ///-------------------------------------------------------------------------------------------- + + /// Register callback for all packets (catch-all) + /// @param callback Function to call for every received packet + /// @return Handle for unregistration + CallbackHandle registerPacketCallback(PacketCallback callback) const; + + /// Register callback for event packets (spikes, digital events, etc.) + /// @param channel_type Channel type filter (ANY matches all event channels) + /// @param callback Function to call for matching events + /// @return Handle for unregistration + CallbackHandle registerEventCallback(ChannelType channel_type, EventCallback callback) const; + + /// Register callback for continuous sample group packets + /// @param rate Sample rate to match (SR_500 through SR_RAW) + /// @param callback Function to call for matching group packets + /// @return Handle for unregistration + CallbackHandle registerGroupCallback(SampleRate rate, GroupCallback callback) const; + + /// Register callback for config/system packets + /// @param packet_type Packet type to match (e.g. cbPKTTYPE_COMMENTREP, cbPKTTYPE_SYSREPRUNLEV) + /// @param callback Function to call for matching config packets + /// @return Handle for unregistration + CallbackHandle registerConfigCallback(uint16_t packet_type, ConfigCallback callback) const; + + /// Unregister a previously registered callback + /// @param handle Handle returned by any register*Callback method + void unregisterCallback(CallbackHandle handle) const; + + /// Set callback for errors (queue overflow, etc.) + /// @param callback Function to call when errors occur + void setErrorCallback(ErrorCallback callback); + + ///-------------------------------------------------------------------------------------------- + /// Statistics & Monitoring + ///-------------------------------------------------------------------------------------------- + + /// Get current statistics + /// @return Copy of current statistics + SdkStats getStats() const; + + /// Reset statistics counters to zero + void resetStats(); + + ///-------------------------------------------------------------------------------------------- + /// Configuration Access + ///-------------------------------------------------------------------------------------------- + + /// Get the configuration used to create this session + /// @return Reference to SDK configuration + const SdkConfig& getConfig() const; + + /// Get system information + /// @return Pointer to system info packet, or nullptr if not available + const cbPKT_SYSINFO* getSysInfo() const; + + /// Get channel information + /// @param chan_id 1-based channel ID (1 to cbMAXCHANS) + /// @return Pointer to channel info, or nullptr if invalid/unavailable + const cbPKT_CHANINFO* getChanInfo(uint32_t chan_id) const; + + /// Get sample group information + /// @param group_id Group ID (1-6) + /// @return Pointer to group info, or nullptr if invalid/unavailable + const cbPKT_GROUPINFO* getGroupInfo(uint32_t group_id) const; + + /// Get filter information + /// @param filter_id Filter ID (0 to cbMAXFILTS-1) + /// @return Pointer to filter info, or nullptr if invalid/unavailable + const cbPKT_FILTINFO* getFilterInfo(uint32_t filter_id) const; + + /// Get current device run level + /// @return Current run level (cbRUNLEVEL_*), or 0 if unknown + uint32_t getRunLevel() const; + + /// Get the protocol version used by this session + /// @return Protocol version, or UNKNOWN if not available (e.g. CLIENT mode) + uint32_t getProtocolVersion() const; + + /// Get the processor identification string (from PROCREP packet) + /// @return ident string (e.g. "Gemini Hub 1"), or empty if unavailable + std::string getProcIdent() const; + + /// Get the global spike event length (samples per spike waveform) + /// @return Spike length in samples, or 0 if unavailable + uint32_t getSpikeLength() const; + + /// Get the global spike pre-trigger length (samples before threshold crossing) + /// @return Pre-trigger length in samples, or 0 if unavailable + uint32_t getSpikePretrigger() const; + + /// Set the global spike event length and pre-trigger + /// @param spikelen Total spike waveform length in samples + /// @param spikepre Pre-trigger samples (must be < spikelen) + /// @return Result indicating success or error + Result setSpikeLength(uint32_t spikelen, uint32_t spikepre); + + /// Get most recent device timestamp from shared memory + /// On Gemini (protocol 4.0+) this is PTP nanoseconds. + /// On legacy NSP (protocol 3.x, CBPROTO_311) this is 30kHz ticks. + /// @return Raw device timestamp, or 0 if not available + uint64_t getTime() const; + + ///-------------------------------------------------------------------------------------------- + /// Bulk Channel Queries + ///-------------------------------------------------------------------------------------------- + + /// Get any numeric field from a single channel by field selector + /// @param chanId 1-based channel ID (1 to cbMAXCHANS) + /// @param field Which field to extract + /// @return Field value widened to int64_t, or error if channel invalid + Result getChannelField(uint32_t chanId, ChanInfoField field) const; + + /// Get the 1-based IDs of channels matching a type + /// @param nChans Max channels to return (cbMAXCHANS for all) + /// @param chanType Channel type filter + /// @return Vector of 1-based channel IDs + Result> getMatchingChannelIds(size_t nChans, ChannelType chanType) const; + + /// Get a numeric field from all channels matching a type + /// @param nChans Max channels to query (cbMAXCHANS for all) + /// @param chanType Channel type filter + /// @param field Which field to extract + /// @return Vector of field values (same order as getMatchingChannelIds) + Result> getChannelField(size_t nChans, ChannelType chanType, + ChanInfoField field) const; + + /// Get labels from all channels matching a type + /// @param nChans Max channels to query (cbMAXCHANS for all) + /// @param chanType Channel type filter + /// @return Vector of label strings (same order as getMatchingChannelIds) + Result> getChannelLabels(size_t nChans, ChannelType chanType) const; + + /// Get positions from all channels matching a type + /// @param nChans Max channels to query (cbMAXCHANS for all) + /// @param chanType Channel type filter + /// @return Flat vector of int32_t: [x0,y0,z0,w0, x1,y1,z1,w1, ...] (4 per channel) + Result> getChannelPositions(size_t nChans, ChannelType chanType) const; + + ///-------------------------------------------------------------------------------------------- + /// Channel Configuration + ///-------------------------------------------------------------------------------------------- + + /// Set sampling rate for channels of a specific type + /// @param nChans Number of channels to configure (cbMAXCHANS for all) + /// @param chanType Channel type filter + /// @param rate Desired sample rate (NONE to disable, SR_500 through SR_RAW) + /// @param disableOthers Disable sampling on channels not in the first nChans of type + /// @return Result indicating success or error + Result setChannelSampleGroup(size_t nChans, ChannelType chanType, + SampleRate rate, bool disableOthers = false); + + /// Set spike sorting options for channels of a specific type + /// @param nChans Number of channels to configure + /// @param chanType Channel type filter + /// @param sortOptions Spike sorting option flags (cbAINPSPK_*) + /// @return Result indicating success or error + Result setChannelSpikeSorting(size_t nChans, ChannelType chanType, + uint32_t sortOptions); + + /// Set AC input coupling (offset correction) for channels of a specific type + /// @param nChans Number of channels to configure (cbMAXCHANS for all) + /// @param chanType Channel type filter + /// @param enabled true = AC coupling (offset correction on), false = DC coupling + /// @return Result indicating success or error + Result setACInputCoupling(size_t nChans, ChannelType chanType, bool enabled); + + /// Set full channel configuration by packet + /// @param chaninfo Complete channel info packet to send + /// @return Result indicating success or error + Result setChannelConfig(const cbPKT_CHANINFO& chaninfo); + + ///-------------------------------------------------------------------------------------------- + /// Comments + ///-------------------------------------------------------------------------------------------- + + /// Send a comment string to the device (appears in recorded data) + /// @param comment Comment text (max 127 chars) + /// @param rgba Color as RGBA uint32_t (default 0 = white) + /// @param charset Character set (0 = ANSI) + /// @return Result indicating success or error + Result sendComment(const std::string& comment, uint32_t rgba = 0, uint8_t charset = 0); + + ///-------------------------------------------------------------------------------------------- + /// File Recording + ///-------------------------------------------------------------------------------------------- + + /// Open Central's File Storage dialog (required before startCentralRecording) + /// + /// The old cbsdk/cbpy required a two-step sequence to start recording: + /// 1. openCentralFileDialog() — sends cbFILECFG_OPT_OPEN + /// 2. Wait ~250ms for Central to open the dialog + /// 3. startCentralRecording() — sends recording=1 + /// + /// @return Result indicating success or error + Result openCentralFileDialog(); + + /// Close Central's File Storage dialog + /// @return Result indicating success or error + Result closeCentralFileDialog(); + + /// Start file recording on the device (Central recording) + /// @param filename Base filename (without extension) + /// @param comment Recording comment + /// @return Result indicating success or error + Result startCentralRecording(const std::string& filename, const std::string& comment = ""); + + /// Stop file recording on the device (Central recording) + /// @return Result indicating success or error + Result stopCentralRecording(); + + ///-------------------------------------------------------------------------------------------- + /// Analog/Digital Output + ///-------------------------------------------------------------------------------------------- + + /// Set digital output value + /// @param chan_id Channel ID (1-based) of a digital output channel + /// @param value Digital output value (bitmask) + /// @return Result indicating success or error + Result setDigitalOutput(uint32_t chan_id, uint16_t value); + + /// Set analog output monitoring (route a channel's audio to an analog/audio output) + /// @param aout_chan_id 1-based channel ID of the analog/audio output channel + /// @param monitor_chan_id 1-based channel ID of the channel to monitor + /// @param track_last If true, track last channel clicked in raster plot + /// @param spike_only If true, only play spikes; if false, play continuous + /// @return Result indicating success or error + Result setAnalogOutputMonitor(uint32_t aout_chan_id, uint32_t monitor_chan_id, + bool track_last = true, bool spike_only = false); + + ///-------------------------------------------------------------------------------------------- + /// Patient Information + ///-------------------------------------------------------------------------------------------- + + /// Set patient information (embedded in recorded files) + /// Must be called before starting recording for the info to be included. + /// @param id Patient identification string (max 127 chars) + /// @param firstname Patient first name (max 127 chars) + /// @param lastname Patient last name (max 127 chars) + /// @param dob_month Birth month (1-12, 0 = unset) + /// @param dob_day Birth day (1-31, 0 = unset) + /// @param dob_year Birth year (e.g. 1990, 0 = unset) + /// @return Result indicating success or error + Result setPatientInfo(const std::string& id, + const std::string& firstname = "", + const std::string& lastname = "", + uint32_t dob_month = 0, uint32_t dob_day = 0, uint32_t dob_year = 0); + + ///-------------------------------------------------------------------------------------------- + /// Channel Mapping (CMP) Files + ///-------------------------------------------------------------------------------------------- + + /// Load a channel mapping file and apply electrode positions + /// + /// CMP files define physical electrode positions on arrays. Because the device does not + /// persist the position field in chaninfo, this method stores positions locally and + /// overlays them onto channel info whenever updated config data arrives from the device. + /// + /// Can be called multiple times for different ports on a Hub device (each port may + /// have a different array with its own CMP file). Subsequent calls merge positions + /// into the existing map. + /// + /// @param filepath Path to the .cmp file + /// @param bank_offset Offset added to CMP bank indices to produce absolute bank numbers. + /// CMP bank letter A becomes absolute bank (1 + bank_offset). + /// Port 1: offset 0 (A=bank 1). Port 2: offset 4 (A=bank 5), etc. + /// @return Result indicating success or error + Result loadChannelMap(const std::string& filepath, uint32_t bank_offset = 0); + + ///-------------------------------------------------------------------------------------------- + /// CCF Configuration Files + ///-------------------------------------------------------------------------------------------- + + /// Save the current device configuration to a CCF (XML) file + /// @param filename Path to the CCF file to write + /// @return Result indicating success or error + Result saveCCF(const std::string& filename); + + /// Load a CCF file and apply its configuration to the device + /// @param filename Path to the CCF file to read + /// @return Result indicating success or error + Result loadCCF(const std::string& filename); + + ///-------------------------------------------------------------------------------------------- + /// Clock Synchronization + ///-------------------------------------------------------------------------------------------- + + /// Convert a device timestamp (nanoseconds) to the host's steady_clock time_point. + /// @param device_time_ns Device timestamp in nanoseconds + /// @return Corresponding host time, or nullopt if no sync data available + std::optional + toLocalTime(uint64_t device_time_ns) const; + + /// Convert a host steady_clock time_point to device timestamp (nanoseconds). + /// @param local_time Host time + /// @return Corresponding device timestamp in nanoseconds, or nullopt if no sync data available + std::optional + toDeviceTime(std::chrono::steady_clock::time_point local_time) const; + + /// Send a clock synchronization probe to the device. + /// @return Result indicating success or error + Result sendClockProbe(); + + /// Current offset estimate: device_ns - steady_clock_ns. + /// @return Offset in nanoseconds, or nullopt if no sync data available + std::optional getClockOffsetNs() const; + + /// Uncertainty (half-RTT) from best probe, or INT64_MAX for one-way only. + /// @return Uncertainty in nanoseconds, or nullopt if no sync data available + std::optional getClockUncertaintyNs() const; + + ///-------------------------------------------------------------------------------------------- + /// Packet Transmission + ///-------------------------------------------------------------------------------------------- + + /// Send a single packet to the device + /// Only available in STANDALONE mode (when device_session exists) + /// @param pkt Packet to send + /// @return Result indicating success or error + Result sendPacket(const cbPKT_GENERIC& pkt); + + /// Send a runlevel command packet to the device + /// @param runlevel Desired runlevel (cbRUNLEVEL_*) + /// @param resetque Channel for reset to queue on (default: 0) + /// @param runflags Lock recording after reset (default: 0) + /// @return Result indicating success or error + Result setSystemRunLevel(uint32_t runlevel, uint32_t resetque = 0, uint32_t runflags = 0); + + /// Request all configuration from the device + /// Sends cbPKTTYPE_REQCONFIGALL which triggers the device to send all config packets + /// The device will respond with > 1000 packets (PROCINFO, CHANINFO, etc.) + /// @return Result indicating success or error + Result requestConfiguration(); + + ///-------------------------------------------------------------------------------------------- + /// Device Startup & Handshake + ///-------------------------------------------------------------------------------------------- + + /// Perform complete device startup handshake sequence + /// Transitions the device from any state to RUNNING. This is automatically called during + /// create() when config.autorun = true. Users can call this manually after create() + /// with config.autorun = false to start the device on demand. + /// + /// Startup sequence: + /// 1. Quick presence check (100ms) - fails fast if device not reachable + /// 2. Check if device is already running + /// 3. If not running, send cbRUNLEVEL_HARDRESET - wait for STANDBY + /// 4. Send REQCONFIGALL - request all configuration + /// 5. Send cbRUNLEVEL_RESET - transition to RUNNING + /// + /// @param timeout_ms Maximum time to wait for each step (default: 500ms) + /// @return Result indicating success or error (clear message if device not reachable) + Result performStartupHandshake(uint32_t timeout_ms = 500); + +private: + /// Private constructor (use create() factory method) + SdkSession(); + + /// Wait for SYSREP packet (helper for handshaking) + /// @param timeout_ms Timeout in milliseconds + /// @param expected_runlevel Expected runlevel (0 = any SYSREP) + /// @return true if SYSREP received with expected runlevel, false if timeout + bool waitForSysrep(uint32_t timeout_ms, uint32_t expected_runlevel = 0) const; + + /// Send a runlevel command packet to the device (internal version with wait_for_runlevel) + /// @param runlevel Desired runlevel (cbRUNLEVEL_*) + /// @param resetque Channel for reset to queue on + /// @param runflags Lock recording after reset + /// @param wait_for_runlevel Runlevel to wait for (0 = any SYSREP) + /// @param timeout_ms Timeout in milliseconds + /// @return Result indicating success or error + Result setSystemRunLevel(uint32_t runlevel, uint32_t resetque, uint32_t runflags, + uint32_t wait_for_runlevel, uint32_t timeout_ms); + + /// Request configuration with custom timeout (internal version) + /// @param timeout_ms Timeout in milliseconds + /// @return Result indicating success or error + Result requestConfiguration(uint32_t timeout_ms); + + /// Send a FILECFG packet (shared helper for recording commands) + Result sendFileCfgPacket(uint32_t options, uint32_t recording, + const std::string& filename, const std::string& comment); + + /// Platform-specific implementation + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace cbsdk + +#endif // CBSDK_V2_SDK_SESSION_H diff --git a/src/cbsdk/src/cbsdk.cpp b/src/cbsdk/src/cbsdk.cpp new file mode 100644 index 00000000..9ed238b7 --- /dev/null +++ b/src/cbsdk/src/cbsdk.cpp @@ -0,0 +1,1496 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file cbsdk.cpp +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief C API implementation +/// +/// Wraps the C++ SdkSession API with a C interface for language bindings +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include "cbsdk/cbsdk.h" +#include "cbsdk/sdk_session.h" +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Tracking & Cleanup (forward declarations; bodies after cbsdk_session_impl) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +static void track_session(cbsdk_session_t session); +static void untrack_session(cbsdk_session_t session); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Internal Structures +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Internal session implementation (opaque to C users) +struct cbsdk_session_impl { + std::unique_ptr cpp_session; + + // C callback storage + cbsdk_packet_callback_fn packet_callback; + void* packet_callback_user_data; + cbsdk::CallbackHandle packet_callback_handle; + + cbsdk_error_callback_fn error_callback; + void* error_callback_user_data; + + cbsdk_session_impl() + : packet_callback(nullptr) + , packet_callback_user_data(nullptr) + , packet_callback_handle(0) + , error_callback(nullptr) + , error_callback_user_data(nullptr) + {} +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Tracking & Cleanup +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Track all live sessions so we can clean up shared memory on abnormal exit. +/// POSIX shared memory segments persist in the kernel namespace until shm_unlink(). +/// If the process is killed (SIGINT, SIGTERM) without running destructors, the +/// segments become orphaned and cause the next session to attach as CLIENT +/// (finding stale shmem) instead of creating a new STANDALONE session. +/// +/// Strategy: install a one-shot signal handler that destroys all tracked sessions +/// (triggering shm_unlink via the normal destructor chain), then re-raises. + +static std::mutex g_sessions_mutex; +static std::set g_sessions; +static bool g_handlers_installed = false; + +#ifdef _WIN32 + +static BOOL WINAPI console_ctrl_handler(DWORD event) { + if (event == CTRL_C_EVENT || event == CTRL_CLOSE_EVENT || event == CTRL_BREAK_EVENT) { + std::set sessions_copy; + { + std::lock_guard lock(g_sessions_mutex); + sessions_copy.swap(g_sessions); + } + for (auto* s : sessions_copy) { + delete s; + } + return FALSE; // Let the default handler run (process exit) + } + return FALSE; +} + +#else + +static struct sigaction g_prev_sigint; +static struct sigaction g_prev_sigterm; + +static void cleanup_sessions_and_reraise(int sig) { + // Destroy all tracked sessions (runs destructors → shm_unlink) + std::set sessions_copy; + { + std::lock_guard lock(g_sessions_mutex); + sessions_copy.swap(g_sessions); + } + for (auto* s : sessions_copy) { + delete s; + } + + // Restore the previous handler and re-raise so the process exits normally + const struct sigaction* prev = (sig == SIGINT) ? &g_prev_sigint : &g_prev_sigterm; + sigaction(sig, prev, nullptr); + raise(sig); +} + +#endif + +static void install_signal_handlers() { + if (g_handlers_installed) return; + g_handlers_installed = true; + +#ifdef _WIN32 + SetConsoleCtrlHandler(console_ctrl_handler, TRUE); +#else + struct sigaction sa{}; + sa.sa_handler = cleanup_sessions_and_reraise; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + sigaction(SIGINT, &sa, &g_prev_sigint); + sigaction(SIGTERM, &sa, &g_prev_sigterm); +#endif +} + +static void track_session(cbsdk_session_t session) { + std::lock_guard lock(g_sessions_mutex); + g_sessions.insert(session); + install_signal_handlers(); +} + +static void untrack_session(cbsdk_session_t session) { + std::lock_guard lock(g_sessions_mutex); + g_sessions.erase(session); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Helper Functions +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Convert C config to C++ config +static cbsdk::SdkConfig to_cpp_config(const cbsdk_config_t* c_config) { + cbsdk::SdkConfig cpp_config; + + // Map device type + switch (c_config->device_type) { + case CBPROTO_DEVICE_TYPE_LEGACY_NSP: + cpp_config.device_type = cbsdk::DeviceType::LEGACY_NSP; + break; + case CBPROTO_DEVICE_TYPE_NSP: + cpp_config.device_type = cbsdk::DeviceType::NSP; + break; + case CBPROTO_DEVICE_TYPE_HUB1: + cpp_config.device_type = cbsdk::DeviceType::HUB1; + break; + case CBPROTO_DEVICE_TYPE_HUB2: + cpp_config.device_type = cbsdk::DeviceType::HUB2; + break; + case CBPROTO_DEVICE_TYPE_HUB3: + cpp_config.device_type = cbsdk::DeviceType::HUB3; + break; + case CBPROTO_DEVICE_TYPE_NPLAY: + cpp_config.device_type = cbsdk::DeviceType::NPLAY; + break; + case CBPROTO_DEVICE_TYPE_CUSTOM: + // CUSTOM not supported in SDK config - use custom address/port fields instead + cpp_config.device_type = cbsdk::DeviceType::LEGACY_NSP; + break; + } + + cpp_config.callback_queue_depth = c_config->callback_queue_depth; + cpp_config.enable_realtime_priority = c_config->enable_realtime_priority; + cpp_config.drop_on_overflow = c_config->drop_on_overflow; + + cpp_config.recv_buffer_size = c_config->recv_buffer_size; + cpp_config.non_blocking = c_config->non_blocking; + + // Optional custom addresses/ports + if (c_config->custom_device_address != nullptr) { + cpp_config.custom_device_address = c_config->custom_device_address; + } + if (c_config->custom_client_address != nullptr) { + cpp_config.custom_client_address = c_config->custom_client_address; + } + if (c_config->custom_device_port != 0) { + cpp_config.custom_device_port = c_config->custom_device_port; + } + if (c_config->custom_client_port != 0) { + cpp_config.custom_client_port = c_config->custom_client_port; + } + + return cpp_config; +} + +/// Convert C++ stats to C stats +static void to_c_stats(const cbsdk::SdkStats& cpp_stats, cbsdk_stats_t* c_stats) { + c_stats->packets_received_from_device = cpp_stats.packets_received_from_device; + c_stats->bytes_received_from_device = cpp_stats.bytes_received_from_device; + c_stats->packets_stored_to_shmem = cpp_stats.packets_stored_to_shmem; + c_stats->packets_queued_for_callback = cpp_stats.packets_queued_for_callback; + c_stats->packets_delivered_to_callback = cpp_stats.packets_delivered_to_callback; + c_stats->packets_dropped = cpp_stats.packets_dropped; + c_stats->queue_current_depth = cpp_stats.queue_current_depth; + c_stats->queue_max_depth = cpp_stats.queue_max_depth; + c_stats->packets_sent_to_device = cpp_stats.packets_sent_to_device; + c_stats->shmem_store_errors = cpp_stats.shmem_store_errors; + c_stats->receive_errors = cpp_stats.receive_errors; + c_stats->send_errors = cpp_stats.send_errors; +} + +/// Convert C chaninfo field enum to C++ ChanInfoField enum +static cbsdk::ChanInfoField to_cpp_chaninfo_field(cbsdk_chaninfo_field_t c_field) { + switch (c_field) { + case CBSDK_CHANINFO_FIELD_SMPGROUP: return cbsdk::ChanInfoField::SMPGROUP; + case CBSDK_CHANINFO_FIELD_SMPFILTER: return cbsdk::ChanInfoField::SMPFILTER; + case CBSDK_CHANINFO_FIELD_SPKFILTER: return cbsdk::ChanInfoField::SPKFILTER; + case CBSDK_CHANINFO_FIELD_AINPOPTS: return cbsdk::ChanInfoField::AINPOPTS; + case CBSDK_CHANINFO_FIELD_SPKOPTS: return cbsdk::ChanInfoField::SPKOPTS; + case CBSDK_CHANINFO_FIELD_SPKTHRLEVEL: return cbsdk::ChanInfoField::SPKTHRLEVEL; + case CBSDK_CHANINFO_FIELD_LNCRATE: return cbsdk::ChanInfoField::LNCRATE; + case CBSDK_CHANINFO_FIELD_REFELECCHAN: return cbsdk::ChanInfoField::REFELECCHAN; + case CBSDK_CHANINFO_FIELD_AMPLREJPOS: return cbsdk::ChanInfoField::AMPLREJPOS; + case CBSDK_CHANINFO_FIELD_AMPLREJNEG: return cbsdk::ChanInfoField::AMPLREJNEG; + case CBSDK_CHANINFO_FIELD_CHANCAPS: return cbsdk::ChanInfoField::CHANCAPS; + case CBSDK_CHANINFO_FIELD_BANK: return cbsdk::ChanInfoField::BANK; + case CBSDK_CHANINFO_FIELD_TERM: return cbsdk::ChanInfoField::TERM; + default: return cbsdk::ChanInfoField::SMPGROUP; + } +} + +/// Convert C channel type enum to C++ ChannelType enum +static cbsdk::ChannelType to_cpp_channel_type(cbproto_channel_type_t c_type) { + switch (c_type) { + case CBPROTO_CHANNEL_TYPE_FRONTEND: return cbsdk::ChannelType::FRONTEND; + case CBPROTO_CHANNEL_TYPE_ANALOG_IN: return cbsdk::ChannelType::ANALOG_IN; + case CBPROTO_CHANNEL_TYPE_ANALOG_OUT: return cbsdk::ChannelType::ANALOG_OUT; + case CBPROTO_CHANNEL_TYPE_AUDIO: return cbsdk::ChannelType::AUDIO; + case CBPROTO_CHANNEL_TYPE_DIGITAL_IN: return cbsdk::ChannelType::DIGITAL_IN; + case CBPROTO_CHANNEL_TYPE_SERIAL: return cbsdk::ChannelType::SERIAL; + case CBPROTO_CHANNEL_TYPE_DIGITAL_OUT: return cbsdk::ChannelType::DIGITAL_OUT; + default: return cbsdk::ChannelType::ANY; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// C API Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +extern "C" { + +cbsdk_config_t cbsdk_config_default(void) { + cbsdk_config_t config; + config.device_type = CBPROTO_DEVICE_TYPE_LEGACY_NSP; + config.callback_queue_depth = 16384; + config.enable_realtime_priority = false; + config.drop_on_overflow = true; + config.recv_buffer_size = 6000000; + config.non_blocking = true; + config.custom_device_address = nullptr; + config.custom_client_address = nullptr; + config.custom_device_port = 0; + config.custom_client_port = 0; + return config; +} + +cbsdk_result_t cbsdk_session_create(cbsdk_session_t* session, const cbsdk_config_t* config) { + if (!session || !config) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + + try { + // Create internal implementation + auto impl = std::make_unique(); + + // Convert config and create C++ session + cbsdk::SdkConfig cpp_config = to_cpp_config(config); + auto result = cbsdk::SdkSession::create(cpp_config); + + if (result.isError()) { + // Classify error + const std::string& error = result.error(); + if (error.find("shared memory") != std::string::npos) { + return CBSDK_RESULT_SHMEM_ERROR; + } else if (error.find("device") != std::string::npos) { + return CBSDK_RESULT_DEVICE_ERROR; + } else { + return CBSDK_RESULT_INTERNAL_ERROR; + } + } + + impl->cpp_session = std::make_unique(std::move(result.value())); + + *session = impl.release(); + track_session(*session); + return CBSDK_RESULT_SUCCESS; + + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +void cbsdk_session_destroy(cbsdk_session_t session) { + if (session) { + untrack_session(session); + delete session; + } +} + +cbsdk_result_t cbsdk_session_start(cbsdk_session_t session) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + + try { + auto result = session->cpp_session->start(); + if (result.isError()) { + if (result.error().find("already running") != std::string::npos) { + // Session is already started (by create() or previous start() call) + // Return SUCCESS to make this call idempotent + return CBSDK_RESULT_SUCCESS; + } + return CBSDK_RESULT_INTERNAL_ERROR; + } + return CBSDK_RESULT_SUCCESS; + + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +void cbsdk_session_stop(cbsdk_session_t session) { + if (session && session->cpp_session) { + try { + session->cpp_session->stop(); + } catch (...) { + // Swallow exceptions in stop() + } + } +} + +bool cbsdk_session_is_running(cbsdk_session_t session) { + if (!session || !session->cpp_session) { + return false; + } + + try { + return session->cpp_session->isRunning(); + } catch (...) { + return false; + } +} + +void cbsdk_session_set_packet_callback(cbsdk_session_t session, + cbsdk_packet_callback_fn callback, + void* user_data) { + if (!session || !session->cpp_session) { + return; + } + + try { + // Unregister previous callback if any + if (session->packet_callback_handle != 0) { + session->cpp_session->unregisterCallback(session->packet_callback_handle); + session->packet_callback_handle = 0; + } + + session->packet_callback = callback; + session->packet_callback_user_data = user_data; + + if (callback) { + // Register per-packet callback, forward to C batch-style callback with count=1 + session->packet_callback_handle = session->cpp_session->registerPacketCallback( + [session](const cbPKT_GENERIC& pkt) { + if (session->packet_callback) { + session->packet_callback(&pkt, 1, session->packet_callback_user_data); + } + } + ); + } + + } catch (...) { + // Swallow exceptions + } +} + +void cbsdk_session_set_error_callback(cbsdk_session_t session, + cbsdk_error_callback_fn callback, + void* user_data) { + if (!session || !session->cpp_session) { + return; + } + + try { + // Store C callback + session->error_callback = callback; + session->error_callback_user_data = user_data; + + if (callback) { + // Wrap C callback in C++ lambda + session->cpp_session->setErrorCallback( + [session](const std::string& error) { + if (session->error_callback) { + session->error_callback(error.c_str(), session->error_callback_user_data); + } + } + ); + } else { + // Clear callback + session->cpp_session->setErrorCallback(nullptr); + } + + } catch (...) { + // Swallow exceptions + } +} + +void cbsdk_session_get_stats(cbsdk_session_t session, cbsdk_stats_t* stats) { + if (!session || !session->cpp_session || !stats) { + return; + } + + try { + cbsdk::SdkStats cpp_stats = session->cpp_session->getStats(); + to_c_stats(cpp_stats, stats); + } catch (...) { + // Return zeroed stats on error + std::memset(stats, 0, sizeof(cbsdk_stats_t)); + } +} + +void cbsdk_session_reset_stats(cbsdk_session_t session) { + if (session && session->cpp_session) { + try { + session->cpp_session->resetStats(); + } catch (...) { + // Swallow exceptions + } + } +} + +const char* cbsdk_get_error_message(cbsdk_result_t result) { + switch (result) { + case CBSDK_RESULT_SUCCESS: + return "Success"; + case CBSDK_RESULT_INVALID_PARAMETER: + return "Invalid parameter (NULL pointer or invalid value)"; + case CBSDK_RESULT_ALREADY_RUNNING: + return "Session is already running"; + case CBSDK_RESULT_NOT_RUNNING: + return "Session is not running"; + case CBSDK_RESULT_SHMEM_ERROR: + return "Shared memory error"; + case CBSDK_RESULT_DEVICE_ERROR: + return "Device connection error"; + case CBSDK_RESULT_INTERNAL_ERROR: + return "Internal error"; + default: + return "Unknown error"; + } +} + +const char* cbsdk_get_version(void) { +#ifdef CBSDK_VERSION_STRING + return CBSDK_VERSION_STRING; +#else + return "0.0.0"; +#endif +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Typed Callback Registration +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_callback_handle_t cbsdk_session_register_packet_callback( + cbsdk_session_t session, + cbsdk_packet_callback_fn callback, + void* user_data) { + if (!session || !session->cpp_session || !callback) { + return 0; + } + try { + return session->cpp_session->registerPacketCallback( + [callback, user_data](const cbPKT_GENERIC& pkt) { + callback(&pkt, 1, user_data); + } + ); + } catch (...) { + return 0; + } +} + +cbsdk_callback_handle_t cbsdk_session_register_event_callback( + cbsdk_session_t session, + cbproto_channel_type_t channel_type, + cbsdk_event_callback_fn callback, + void* user_data) { + if (!session || !session->cpp_session || !callback) { + return 0; + } + try { + // Use (int)channel_type == -1 as sentinel for ChannelType::ANY + cbsdk::ChannelType cpp_type = (static_cast(channel_type) == -1) + ? cbsdk::ChannelType::ANY + : to_cpp_channel_type(channel_type); + return session->cpp_session->registerEventCallback(cpp_type, + [callback, user_data](const cbPKT_GENERIC& pkt) { + callback(&pkt, user_data); + } + ); + } catch (...) { + return 0; + } +} + +cbsdk_callback_handle_t cbsdk_session_register_group_callback( + cbsdk_session_t session, + cbproto_group_rate_t rate, + cbsdk_group_callback_fn callback, + void* user_data) { + if (!session || !session->cpp_session || !callback) { + return 0; + } + try { + return session->cpp_session->registerGroupCallback( + static_cast(rate), + [callback, user_data](const cbPKT_GROUP& pkt) { + callback(&pkt, user_data); + } + ); + } catch (...) { + return 0; + } +} + +cbsdk_callback_handle_t cbsdk_session_register_config_callback( + cbsdk_session_t session, + uint16_t packet_type, + cbsdk_config_callback_fn callback, + void* user_data) { + if (!session || !session->cpp_session || !callback) { + return 0; + } + try { + return session->cpp_session->registerConfigCallback(packet_type, + [callback, user_data](const cbPKT_GENERIC& pkt) { + callback(&pkt, user_data); + } + ); + } catch (...) { + return 0; + } +} + +void cbsdk_session_unregister_callback(cbsdk_session_t session, + cbsdk_callback_handle_t handle) { + if (!session || !session->cpp_session || handle == 0) { + return; + } + try { + session->cpp_session->unregisterCallback(handle); + } catch (...) { + // Swallow exceptions + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Access +/////////////////////////////////////////////////////////////////////////////////////////////////// + +uint32_t cbsdk_session_get_runlevel(cbsdk_session_t session) { + if (!session || !session->cpp_session) { + return 0; + } + try { + return session->cpp_session->getRunLevel(); + } catch (...) { + return 0; + } +} + +uint32_t cbsdk_session_get_protocol_version(cbsdk_session_t session) { + if (!session || !session->cpp_session) return 0; + try { + return session->cpp_session->getProtocolVersion(); + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_proc_ident(cbsdk_session_t session, char* buf, uint32_t buf_size) { + if (!session || !session->cpp_session || !buf || buf_size == 0) return 0; + try { + std::string ident = session->cpp_session->getProcIdent(); + if (ident.empty()) { buf[0] = '\0'; return 0; } + uint32_t len = static_cast(std::min(ident.size(), buf_size - 1)); + std::memcpy(buf, ident.data(), len); + buf[len] = '\0'; + return len; + } catch (...) { buf[0] = '\0'; return 0; } +} + +uint32_t cbsdk_session_get_spike_length(cbsdk_session_t session) { + if (!session || !session->cpp_session) return 0; + try { + return session->cpp_session->getSpikeLength(); + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_spike_pretrigger(cbsdk_session_t session) { + if (!session || !session->cpp_session) return 0; + try { + return session->cpp_session->getSpikePretrigger(); + } catch (...) { return 0; } +} + +cbsdk_result_t cbsdk_session_set_spike_length( + cbsdk_session_t session, + uint32_t spike_length, + uint32_t spike_pretrigger) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setSpikeLength(spike_length, spike_pretrigger); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Information Accessors +/////////////////////////////////////////////////////////////////////////////////////////////////// + +uint32_t cbsdk_get_max_chans(void) { + return cbMAXCHANS; +} + +uint32_t cbsdk_get_num_fe_chans(void) { + return cbNUM_FE_CHANS; +} + +uint32_t cbsdk_get_num_analog_chans(void) { + return cbNUM_ANALOG_CHANS; +} + +const char* cbsdk_session_get_channel_label(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) { + return nullptr; + } + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->label : nullptr; + } catch (...) { + return nullptr; + } +} + +uint32_t cbsdk_session_get_channel_smpgroup(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) { + return 0; + } + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->smpgroup : 0; + } catch (...) { + return 0; + } +} + +uint32_t cbsdk_session_get_channel_chancaps(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) { + return 0; + } + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->chancaps : 0; + } catch (...) { + return 0; + } +} + +cbproto_channel_type_t cbsdk_session_get_channel_type(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) { + return static_cast(-1); + } + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + if (!info) return static_cast(-1); + + const uint32_t caps = info->chancaps; + if ((cbCHAN_EXISTS | cbCHAN_CONNECTED) != (caps & (cbCHAN_EXISTS | cbCHAN_CONNECTED))) + return static_cast(-1); + + if ((cbCHAN_AINP | cbCHAN_ISOLATED) == (caps & (cbCHAN_AINP | cbCHAN_ISOLATED))) + return CBPROTO_CHANNEL_TYPE_FRONTEND; + if (cbCHAN_AINP == (caps & (cbCHAN_AINP | cbCHAN_ISOLATED))) + return CBPROTO_CHANNEL_TYPE_ANALOG_IN; + if (cbCHAN_AOUT == (caps & cbCHAN_AOUT)) { + if (cbAOUT_AUDIO == (info->aoutcaps & cbAOUT_AUDIO)) + return CBPROTO_CHANNEL_TYPE_AUDIO; + return CBPROTO_CHANNEL_TYPE_ANALOG_OUT; + } + if (cbCHAN_DINP == (caps & cbCHAN_DINP)) { + if (info->dinpcaps & cbDINP_SERIALMASK) + return CBPROTO_CHANNEL_TYPE_SERIAL; + return CBPROTO_CHANNEL_TYPE_DIGITAL_IN; + } + if (cbCHAN_DOUT == (caps & cbCHAN_DOUT)) + return CBPROTO_CHANNEL_TYPE_DIGITAL_OUT; + + return static_cast(-1); + } catch (...) { + return static_cast(-1); + } +} + +uint32_t cbsdk_session_get_channel_smpfilter(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->smpfilter : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_channel_spkfilter(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->spkfilter : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_channel_spkopts(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->spkopts : 0; + } catch (...) { return 0; } +} + +int32_t cbsdk_session_get_channel_spkthrlevel(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->spkthrlevel : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_channel_ainpopts(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->ainpopts : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_channel_lncrate(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->lncrate : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_channel_refelecchan(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->refelecchan : 0; + } catch (...) { return 0; } +} + +int16_t cbsdk_session_get_channel_amplrejpos(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->amplrejpos : 0; + } catch (...) { return 0; } +} + +int16_t cbsdk_session_get_channel_amplrejneg(cbsdk_session_t session, uint32_t chan_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + return info ? info->amplrejneg : 0; + } catch (...) { return 0; } +} + +cbsdk_result_t cbsdk_session_get_channel_scaling( + cbsdk_session_t session, uint32_t chan_id, cbsdk_channel_scaling_t* scaling) { + if (!session || !session->cpp_session || !scaling) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + if (!info) return CBSDK_RESULT_INVALID_PARAMETER; + scaling->digmin = info->scalin.digmin; + scaling->digmax = info->scalin.digmax; + scaling->anamin = info->scalin.anamin; + scaling->anamax = info->scalin.anamax; + scaling->anagain = info->scalin.anagain; + std::memcpy(scaling->anaunit, info->scalin.anaunit, sizeof(scaling->anaunit)); + return CBSDK_RESULT_SUCCESS; + } catch (...) { + std::memset(scaling, 0, sizeof(cbsdk_channel_scaling_t)); + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +int64_t cbsdk_session_get_channel_field( + cbsdk_session_t session, + uint32_t chan_id, + cbsdk_chaninfo_field_t field) { + if (!session || !session->cpp_session) return 0; + try { + auto result = session->cpp_session->getChannelField( + chan_id, to_cpp_chaninfo_field(field)); + return result.isOk() ? result.value() : 0; + } catch (...) { return 0; } +} + +const char* cbsdk_session_get_group_label(cbsdk_session_t session, uint32_t group_id) { + if (!session || !session->cpp_session) { + return nullptr; + } + try { + const cbPKT_GROUPINFO* info = session->cpp_session->getGroupInfo(group_id); + return info ? info->label : nullptr; + } catch (...) { + return nullptr; + } +} + +cbsdk_result_t cbsdk_session_get_group_list( + cbsdk_session_t session, + uint32_t group_id, + uint16_t* list, + uint32_t* count) { + if (!session || !session->cpp_session || !list || !count) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + const cbPKT_GROUPINFO* info = session->cpp_session->getGroupInfo(group_id); + if (!info) { + *count = 0; + return CBSDK_RESULT_INVALID_PARAMETER; + } + uint32_t n = info->length; + if (n > *count) n = *count; + for (uint32_t i = 0; i < n; i++) { + list[i] = info->list[i]; + } + *count = n; + return CBSDK_RESULT_SUCCESS; + } catch (...) { + *count = 0; + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Configuration +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_set_channel_sample_group( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + cbproto_group_rate_t rate, + bool disable_others) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setChannelSampleGroup( + n_chans, to_cpp_channel_type(chan_type), static_cast(rate), disable_others); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_set_channel_config( + cbsdk_session_t session, + const cbPKT_CHANINFO* chaninfo) { + if (!session || !session->cpp_session || !chaninfo) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setChannelConfig(*chaninfo); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/// Helper: read-modify-write a channel config field +/// Gets current chaninfo from shared memory, calls `modify` on a copy, sends the packet. +static cbsdk_result_t modify_and_send_chaninfo( + cbsdk_session_t session, + uint32_t chan_id, + uint16_t pkt_type, + std::function modify) { + if (!session || !session->cpp_session) return CBSDK_RESULT_INVALID_PARAMETER; + try { + const cbPKT_CHANINFO* info = session->cpp_session->getChanInfo(chan_id); + if (!info) return CBSDK_RESULT_INVALID_PARAMETER; + cbPKT_CHANINFO ci = *info; + ci.chan = chan_id; + ci.cbpkt_header.type = pkt_type; + modify(ci); + auto r = session->cpp_session->sendPacket( + reinterpret_cast(ci)); + return r.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_set_channel_label( + cbsdk_session_t session, uint32_t chan_id, const char* label) { + if (!label) return CBSDK_RESULT_INVALID_PARAMETER; + return modify_and_send_chaninfo(session, chan_id, cbPKTTYPE_CHANSETLABEL, + [label](cbPKT_CHANINFO& ci) { + std::strncpy(ci.label, label, sizeof(ci.label) - 1); + ci.label[sizeof(ci.label) - 1] = '\0'; + }); +} + +cbsdk_result_t cbsdk_session_set_channel_smpfilter( + cbsdk_session_t session, uint32_t chan_id, uint32_t filter_id) { + return modify_and_send_chaninfo(session, chan_id, cbPKTTYPE_CHANSETSMP, + [filter_id](cbPKT_CHANINFO& ci) { + ci.smpfilter = filter_id; + }); +} + +cbsdk_result_t cbsdk_session_set_channel_spkfilter( + cbsdk_session_t session, uint32_t chan_id, uint32_t filter_id) { + return modify_and_send_chaninfo(session, chan_id, cbPKTTYPE_CHANSETSPK, + [filter_id](cbPKT_CHANINFO& ci) { + ci.spkfilter = filter_id; + }); +} + +cbsdk_result_t cbsdk_session_set_channel_ainpopts( + cbsdk_session_t session, uint32_t chan_id, uint32_t ainpopts) { + return modify_and_send_chaninfo(session, chan_id, cbPKTTYPE_CHANSETAINP, + [ainpopts](cbPKT_CHANINFO& ci) { + ci.ainpopts = ainpopts; + }); +} + +cbsdk_result_t cbsdk_session_set_channel_lncrate( + cbsdk_session_t session, uint32_t chan_id, uint32_t lncrate) { + return modify_and_send_chaninfo(session, chan_id, cbPKTTYPE_CHANSETAINP, + [lncrate](cbPKT_CHANINFO& ci) { + ci.lncrate = lncrate; + }); +} + +cbsdk_result_t cbsdk_session_set_channel_spkopts( + cbsdk_session_t session, uint32_t chan_id, uint32_t spkopts) { + return modify_and_send_chaninfo(session, chan_id, cbPKTTYPE_CHANSETSPKTHR, + [spkopts](cbPKT_CHANINFO& ci) { + ci.spkopts = spkopts; + }); +} + +cbsdk_result_t cbsdk_session_set_channel_spkthrlevel( + cbsdk_session_t session, uint32_t chan_id, int32_t level) { + return modify_and_send_chaninfo(session, chan_id, cbPKTTYPE_CHANSETSPKTHR, + [level](cbPKT_CHANINFO& ci) { + ci.spkthrlevel = level; + }); +} + +cbsdk_result_t cbsdk_session_set_channel_autothreshold( + cbsdk_session_t session, uint32_t chan_id, bool enabled) { + return modify_and_send_chaninfo(session, chan_id, cbPKTTYPE_CHANSETAUTOTHRESHOLD, + [enabled](cbPKT_CHANINFO& ci) { + if (enabled) + ci.spkopts |= cbAINPSPK_THRAUTO; + else + ci.spkopts &= ~cbAINPSPK_THRAUTO; + }); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Bulk Channel Queries +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_get_matching_channels( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + uint32_t* out_ids, + uint32_t* out_count) { + if (!session || !session->cpp_session || !out_ids || !out_count) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->getMatchingChannelIds( + n_chans, to_cpp_channel_type(chan_type)); + if (result.isError()) return CBSDK_RESULT_INTERNAL_ERROR; + const auto& ids = result.value(); + uint32_t n = static_cast(ids.size()); + if (n > *out_count) n = *out_count; + for (uint32_t i = 0; i < n; i++) { + out_ids[i] = ids[i]; + } + *out_count = n; + return CBSDK_RESULT_SUCCESS; + } catch (...) { + *out_count = 0; + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_get_channels_field( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + cbsdk_chaninfo_field_t field, + int64_t* out_values, + uint32_t* out_count) { + if (!session || !session->cpp_session || !out_values || !out_count) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->getChannelField( + n_chans, to_cpp_channel_type(chan_type), to_cpp_chaninfo_field(field)); + if (result.isError()) return CBSDK_RESULT_INTERNAL_ERROR; + const auto& vals = result.value(); + uint32_t n = static_cast(vals.size()); + if (n > *out_count) n = *out_count; + for (uint32_t i = 0; i < n; i++) { + out_values[i] = vals[i]; + } + *out_count = n; + return CBSDK_RESULT_SUCCESS; + } catch (...) { + *out_count = 0; + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_get_channels_labels( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + char* out_buf, + size_t label_stride, + uint32_t* out_count) { + if (!session || !session->cpp_session || !out_buf || !out_count || label_stride == 0) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->getChannelLabels( + n_chans, to_cpp_channel_type(chan_type)); + if (result.isError()) return CBSDK_RESULT_INTERNAL_ERROR; + const auto& labels = result.value(); + uint32_t n = static_cast(labels.size()); + if (n > *out_count) n = *out_count; + for (uint32_t i = 0; i < n; i++) { + char* dst = out_buf + i * label_stride; + std::strncpy(dst, labels[i].c_str(), label_stride - 1); + dst[label_stride - 1] = '\0'; + } + *out_count = n; + return CBSDK_RESULT_SUCCESS; + } catch (...) { + *out_count = 0; + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_get_channels_positions( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + int32_t* out_positions, + uint32_t* out_count) { + if (!session || !session->cpp_session || !out_positions || !out_count) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->getChannelPositions( + n_chans, to_cpp_channel_type(chan_type)); + if (result.isError()) return CBSDK_RESULT_INTERNAL_ERROR; + const auto& flat = result.value(); + uint32_t n_channels = static_cast(flat.size() / 4); + if (n_channels > *out_count) n_channels = *out_count; + std::memcpy(out_positions, flat.data(), n_channels * 4 * sizeof(int32_t)); + *out_count = n_channels; + return CBSDK_RESULT_SUCCESS; + } catch (...) { + *out_count = 0; + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Bulk Configuration Access +/////////////////////////////////////////////////////////////////////////////////////////////////// + +uint32_t cbsdk_session_get_sysfreq(cbsdk_session_t session) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_SYSINFO* info = session->cpp_session->getSysInfo(); + return info ? info->sysfreq : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_get_num_filters(void) { + return cbMAXFILTS; +} + +const char* cbsdk_session_get_filter_label(cbsdk_session_t session, uint32_t filter_id) { + if (!session || !session->cpp_session) return nullptr; + try { + const cbPKT_FILTINFO* info = session->cpp_session->getFilterInfo(filter_id); + return info ? info->label : nullptr; + } catch (...) { return nullptr; } +} + +uint32_t cbsdk_session_get_filter_hpfreq(cbsdk_session_t session, uint32_t filter_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_FILTINFO* info = session->cpp_session->getFilterInfo(filter_id); + return info ? info->hpfreq : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_filter_hporder(cbsdk_session_t session, uint32_t filter_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_FILTINFO* info = session->cpp_session->getFilterInfo(filter_id); + return info ? info->hporder : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_filter_lpfreq(cbsdk_session_t session, uint32_t filter_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_FILTINFO* info = session->cpp_session->getFilterInfo(filter_id); + return info ? info->lpfreq : 0; + } catch (...) { return 0; } +} + +uint32_t cbsdk_session_get_filter_lporder(cbsdk_session_t session, uint32_t filter_id) { + if (!session || !session->cpp_session) return 0; + try { + const cbPKT_FILTINFO* info = session->cpp_session->getFilterInfo(filter_id); + return info ? info->lporder : 0; + } catch (...) { return 0; } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Commands +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_send_comment( + cbsdk_session_t session, + const char* comment, + uint32_t rgba, + uint8_t charset) { + if (!session || !session->cpp_session || !comment) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->sendComment(comment, rgba, charset); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_send_packet( + cbsdk_session_t session, + const cbPKT_GENERIC* pkt) { + if (!session || !session->cpp_session || !pkt) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->sendPacket(*pkt); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_set_digital_output( + cbsdk_session_t session, + uint32_t chan_id, + uint16_t value) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setDigitalOutput(chan_id, value); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_set_runlevel( + cbsdk_session_t session, + uint32_t runlevel) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setSystemRunLevel(runlevel); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Mapping (CMP) Files +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_load_channel_map(cbsdk_session_t session, const char* filepath, uint32_t bank_offset) { + if (!session || !session->cpp_session || !filepath) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + auto result = session->cpp_session->loadChannelMap(filepath, bank_offset); + if (result.isOk()) { + return CBSDK_RESULT_SUCCESS; + } else { + return CBSDK_RESULT_INVALID_PARAMETER; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// CCF Configuration Files +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_save_ccf(cbsdk_session_t session, const char* filename) { + if (!session || !session->cpp_session || !filename) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->saveCCF(filename); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_load_ccf(cbsdk_session_t session, const char* filename) { + if (!session || !session->cpp_session || !filename) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->loadCCF(filename); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Instrument Time +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_get_time(cbsdk_session_t session, uint64_t* time) { + if (!session || !session->cpp_session || !time) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + *time = session->cpp_session->getTime(); + return CBSDK_RESULT_SUCCESS; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Patient Information +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_set_patient_info( + cbsdk_session_t session, + const char* id, + const char* firstname, + const char* lastname, + uint32_t dob_month, + uint32_t dob_day, + uint32_t dob_year) { + if (!session || !session->cpp_session || !id) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setPatientInfo( + id, + firstname ? firstname : "", + lastname ? lastname : "", + dob_month, dob_day, dob_year); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Analog Output +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_set_analog_output_monitor( + cbsdk_session_t session, + uint32_t aout_chan_id, + uint32_t monitor_chan_id, + bool track_last, + bool spike_only) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setAnalogOutputMonitor( + aout_chan_id, monitor_chan_id, track_last, spike_only); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Recording Control +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_start_central_recording( + cbsdk_session_t session, + const char* filename, + const char* comment) { + if (!session || !session->cpp_session || !filename) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + std::string comment_str = comment ? comment : ""; + auto result = session->cpp_session->startCentralRecording(filename, comment_str); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_stop_central_recording(cbsdk_session_t session) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->stopCentralRecording(); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_open_central_file_dialog(cbsdk_session_t session) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->openCentralFileDialog(); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_close_central_file_dialog(cbsdk_session_t session) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->closeCentralFileDialog(); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// AC Input Coupling +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_set_ac_input_coupling( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + bool enabled) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setACInputCoupling( + n_chans, to_cpp_channel_type(chan_type), enabled); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Spike Sorting +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_set_channel_spike_sorting( + cbsdk_session_t session, + size_t n_chans, + cbproto_channel_type_t chan_type, + uint32_t sort_options) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->setChannelSpikeSorting( + n_chans, to_cpp_channel_type(chan_type), sort_options); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Clock Synchronization +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbsdk_result_t cbsdk_session_get_clock_offset( + cbsdk_session_t session, + int64_t* offset_ns) { + if (!session || !session->cpp_session || !offset_ns) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->getClockOffsetNs(); + if (!result.has_value()) { + return CBSDK_RESULT_NOT_RUNNING; + } + *offset_ns = result.value(); + return CBSDK_RESULT_SUCCESS; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_get_clock_uncertainty( + cbsdk_session_t session, + int64_t* uncertainty_ns) { + if (!session || !session->cpp_session || !uncertainty_ns) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->getClockUncertaintyNs(); + if (!result.has_value()) { + return CBSDK_RESULT_NOT_RUNNING; + } + *uncertainty_ns = result.value(); + return CBSDK_RESULT_SUCCESS; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +cbsdk_result_t cbsdk_session_send_clock_probe(cbsdk_session_t session) { + if (!session || !session->cpp_session) { + return CBSDK_RESULT_INVALID_PARAMETER; + } + try { + auto result = session->cpp_session->sendClockProbe(); + return result.isOk() ? CBSDK_RESULT_SUCCESS : CBSDK_RESULT_INTERNAL_ERROR; + } catch (...) { + return CBSDK_RESULT_INTERNAL_ERROR; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Utility +/////////////////////////////////////////////////////////////////////////////////////////////////// + +int64_t cbsdk_get_steady_clock_ns(void) { + auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast( + now.time_since_epoch()).count(); +} + +} // extern "C" diff --git a/src/cbsdk/src/cmp_parser.cpp b/src/cbsdk/src/cmp_parser.cpp new file mode 100644 index 00000000..06f30670 --- /dev/null +++ b/src/cbsdk/src/cmp_parser.cpp @@ -0,0 +1,78 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file cmp_parser.cpp +/// @brief Parser for Blackrock .cmp (channel mapping) files +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "cmp_parser.h" +#include +#include +#include + +namespace cbsdk { + +cbutil::Result parseCmpFile(const std::string& filepath, uint32_t bank_offset) { + std::ifstream file(filepath); + if (!file.is_open()) { + return cbutil::Result::error("Failed to open CMP file: " + filepath); + } + + CmpPositionMap positions; + bool found_description = false; + std::string line; + + while (std::getline(file, line)) { + // Strip trailing whitespace/tabs + while (!line.empty() && (line.back() == ' ' || line.back() == '\t' || line.back() == '\r')) { + line.pop_back(); + } + + // Skip empty lines + if (line.empty()) continue; + + // Skip comment lines + if (line.size() >= 2 && line[0] == '/' && line[1] == '/') continue; + + // First non-comment line is the description — skip it + if (!found_description) { + found_description = true; + continue; + } + + // Parse data line: col row bank electrode [label] + std::istringstream iss(line); + int32_t col, row; + std::string bank_str; + int32_t electrode; + + if (!(iss >> col >> row >> bank_str >> electrode)) { + continue; // Skip malformed lines + } + + // Bank letter to 1-based index: A=1, B=2, ..., H=8 + if (bank_str.empty() || !std::isalpha(static_cast(bank_str[0]))) { + continue; // Skip lines with invalid bank + } + uint32_t bank_from_letter = static_cast( + std::toupper(static_cast(bank_str[0])) - 'A' + 1); + + // Apply bank offset for multi-port Hub configurations + uint32_t absolute_bank = bank_from_letter + bank_offset; + + // Electrode is 1-based in CMP, term is 0-based in chaninfo + uint32_t term = static_cast(electrode - 1); + + // Store position: {col, row, bank_from_letter, electrode} + // We store the original CMP values (not offset bank) in position[2] + // so the position data reflects the physical array geometry + uint64_t key = cmpKey(absolute_bank, term); + positions[key] = {col, row, static_cast(bank_from_letter), electrode}; + } + + if (positions.empty()) { + return cbutil::Result::error("No valid entries found in CMP file: " + filepath); + } + + return cbutil::Result::ok(std::move(positions)); +} + +} // namespace cbsdk diff --git a/src/cbsdk/src/cmp_parser.h b/src/cbsdk/src/cmp_parser.h new file mode 100644 index 00000000..67151b26 --- /dev/null +++ b/src/cbsdk/src/cmp_parser.h @@ -0,0 +1,48 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file cmp_parser.h +/// @brief Parser for Blackrock .cmp (channel mapping) files +/// +/// CMP files define electrode positions on NSP arrays. Format: +/// - Lines starting with // are comments +/// - First non-comment line is a description string +/// - Subsequent lines: col row bank electrode [label] +/// - col: 0-based column (left to right) +/// - row: 0-based row (bottom to top) +/// - bank: letter A-H (maps to bank index with offset) +/// - electrode: 1-based electrode within bank (1-32) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBSDK_CMP_PARSER_H +#define CBSDK_CMP_PARSER_H + +#include +#include +#include +#include +#include + +namespace cbsdk { + +/// Key for CMP position lookup: (bank, term) packed into uint64_t +/// bank is the absolute bank index (1-based, matching chaninfo.bank) +/// term is the terminal index (0-based, matching chaninfo.term) +inline uint64_t cmpKey(uint32_t bank, uint32_t term) { + return (static_cast(bank) << 32) | static_cast(term); +} + +/// Map from (bank, term) key to position[4] = {col, row, bank_idx, electrode} +using CmpPositionMap = std::unordered_map>; + +/// Parse a CMP file and return position data keyed by (absolute_bank, term). +/// +/// @param filepath Path to the .cmp file +/// @param bank_offset Offset added to bank indices from the file. +/// CMP bank letter A becomes absolute bank (1 + bank_offset). +/// Use 0 for port 1, 4 for port 2 (if 4 banks per port), etc. +/// @return Map of (bank, term) -> position[4], or error message +cbutil::Result parseCmpFile(const std::string& filepath, uint32_t bank_offset = 0); + +} // namespace cbsdk + +#endif // CBSDK_CMP_PARSER_H diff --git a/src/cbsdk/src/platform_first.h b/src/cbsdk/src/platform_first.h new file mode 100644 index 00000000..c69cc5d3 --- /dev/null +++ b/src/cbsdk/src/platform_first.h @@ -0,0 +1,33 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file platform_first.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Platform headers that MUST be included before cbproto headers +/// +/// IMPORTANT: This header must be included FIRST in any source file that uses both +/// Windows headers and cbproto headers. +/// +/// Reason: cbproto uses #pragma pack(1) for network structures. Windows SDK headers +/// (specifically winnt.h) have a static_assert that fails if packing is not at the +/// default setting when they are included. This header ensures Windows headers are +/// processed before any pack directives are active. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBSDK_PLATFORM_FIRST_H +#define CBSDK_PLATFORM_FIRST_H + +#ifdef _WIN32 + // Windows headers must be included before cbproto headers due to packing requirements + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include + #include +#endif + +#endif // CBSDK_PLATFORM_FIRST_H diff --git a/src/cbsdk/src/sdk_session.cpp b/src/cbsdk/src/sdk_session.cpp new file mode 100644 index 00000000..338380e3 --- /dev/null +++ b/src/cbsdk/src/sdk_session.cpp @@ -0,0 +1,1953 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file sdk_session.cpp +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief SDK session implementation +/// +/// Implements the two-stage pipeline: +/// Device → cbdev receive thread → cbshm (fast!) → queue → callback thread → user callback +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include "cbsdk/sdk_session.h" +#include "cmp_parser.h" +#include "cbdev/device_factory.h" +#include "cbdev/connection.h" +#include "cbshm/shmem_session.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cbsdk { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel type classification helper (capability-based) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Classify a channel using its capability flags from device config. +/// This matches the logic in cbdev::DeviceSession::channelMatchesType and pycbsdk. +static ChannelType classifyChannelByCaps(const cbPKT_CHANINFO& chaninfo) { + const uint32_t caps = chaninfo.chancaps; + + // Channel must exist and be connected + if ((cbCHAN_EXISTS | cbCHAN_CONNECTED) != (caps & (cbCHAN_EXISTS | cbCHAN_CONNECTED))) + return ChannelType::ANY; + + // Front-end: analog input + isolated + if ((cbCHAN_AINP | cbCHAN_ISOLATED) == (caps & (cbCHAN_AINP | cbCHAN_ISOLATED))) + return ChannelType::FRONTEND; + + // Analog input (not isolated) + if (cbCHAN_AINP == (caps & (cbCHAN_AINP | cbCHAN_ISOLATED))) + return ChannelType::ANALOG_IN; + + // Analog output: check audio flag to distinguish + if (cbCHAN_AOUT == (caps & cbCHAN_AOUT)) { + if (cbAOUT_AUDIO == (chaninfo.aoutcaps & cbAOUT_AUDIO)) + return ChannelType::AUDIO; + return ChannelType::ANALOG_OUT; + } + + // Digital input: check serial vs regular + if (cbCHAN_DINP == (caps & cbCHAN_DINP)) { + if (chaninfo.dinpcaps & cbDINP_SERIALMASK) + return ChannelType::SERIAL; + return ChannelType::DIGITAL_IN; + } + + // Digital output + if (cbCHAN_DOUT == (caps & cbCHAN_DOUT)) + return ChannelType::DIGITAL_OUT; + + return ChannelType::ANY; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// SdkSession::Impl - Internal Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +struct SdkSession::Impl { + // Configuration + SdkConfig config; + + // Sub-components + std::unique_ptr device_session; + std::optional shmem_session; + + // Packet queue (receive thread → callback thread) + SPSCQueue packet_queue; // Fixed size for now (TODO: make configurable) + + // Callback thread + std::unique_ptr callback_thread; + std::atomic callback_thread_running{false}; + std::atomic callback_thread_waiting{false}; + std::mutex callback_mutex; + std::condition_variable callback_cv; + + // Device send thread (STANDALONE mode only) + // Note: device receive thread is now managed by device_session->startReceiveThread() + std::unique_ptr device_send_thread; + std::atomic device_send_thread_running{false}; + + // Callback handles for device receive thread + cbdev::CallbackHandle receive_callback_handle = 0; + cbdev::CallbackHandle datagram_callback_handle = 0; + + // Shared memory receive thread (CLIENT mode only) + std::unique_ptr shmem_receive_thread; + std::atomic shmem_receive_thread_running{false}; + + // Handshake state (for performStartupHandshake) + std::atomic device_runlevel{0}; + std::atomic received_sysrep{false}; + std::mutex handshake_mutex; + std::condition_variable handshake_cv; + + // User callbacks — per-type vectors for O(1) dispatch (Phase 2, Fix 8) + // Registered rarely (user thread), dispatched at 30k/s (callback thread). + struct PacketCB { CallbackHandle handle; PacketCallback cb; }; + struct EventCB { CallbackHandle handle; ChannelType channel_type; EventCallback cb; }; + struct GroupCB { CallbackHandle handle; uint8_t group_id; GroupCallback cb; }; + struct ConfigCB { CallbackHandle handle; uint16_t packet_type; ConfigCallback cb; }; + + std::vector packet_callbacks; + std::vector event_callbacks; + std::vector group_callbacks; + std::vector config_callbacks; + + CallbackHandle next_callback_handle = 1; + ErrorCallback error_callback; + std::mutex user_callback_mutex; + + // Channel type cache — pre-computed at config time, avoids per-packet getChanInfo() (Phase 3, Fix 10) + std::array channel_type_cache; + bool channel_cache_valid = false; + + // CMP (channel mapping) position overlay + // Keyed by cmpKey(bank, term) → position[4] = {col, row, bank_letter, electrode} + CmpPositionMap cmp_positions; + std::mutex cmp_mutex; // protects cmp_positions (loaded rarely, read on every CHANREP) + + void rebuildChannelTypeCache() { + for (uint32_t ch = 0; ch < cbMAXCHANS; ++ch) { + const cbPKT_CHANINFO* ci = nullptr; + if (device_session) { + ci = device_session->getChanInfo(ch + 1); + } else if (shmem_session) { + const auto* native = shmem_session->getNativeConfigBuffer(); + if (native) ci = &native->chaninfo[ch]; + } + channel_type_cache[ch] = ci ? classifyChannelByCaps(*ci) : ChannelType::ANY; + } + channel_cache_valid = true; + } + + /// Get chaninfo pointer for a 0-based channel index (works for both STANDALONE and CLIENT) + const cbPKT_CHANINFO* getChanInfoPtr(uint32_t idx) const; + + // Clock sync periodic probing (STANDALONE mode) + std::chrono::steady_clock::time_point last_clock_probe_time{}; + + // Statistics — atomic counters, no mutex needed (Phase 2, Fix 9) + struct AtomicStats { + std::atomic packets_received_from_device{0}; + std::atomic bytes_received_from_device{0}; + std::atomic packets_stored_to_shmem{0}; + std::atomic packets_queued_for_callback{0}; + std::atomic packets_delivered_to_callback{0}; + std::atomic packets_dropped{0}; + std::atomic queue_max_depth{0}; + std::atomic packets_sent_to_device{0}; + std::atomic shmem_store_errors{0}; + std::atomic receive_errors{0}; + std::atomic send_errors{0}; + + void reset() { + packets_received_from_device.store(0, std::memory_order_relaxed); + bytes_received_from_device.store(0, std::memory_order_relaxed); + packets_stored_to_shmem.store(0, std::memory_order_relaxed); + packets_queued_for_callback.store(0, std::memory_order_relaxed); + packets_delivered_to_callback.store(0, std::memory_order_relaxed); + packets_dropped.store(0, std::memory_order_relaxed); + queue_max_depth.store(0, std::memory_order_relaxed); + packets_sent_to_device.store(0, std::memory_order_relaxed); + shmem_store_errors.store(0, std::memory_order_relaxed); + receive_errors.store(0, std::memory_order_relaxed); + send_errors.store(0, std::memory_order_relaxed); + } + + SdkStats snapshot() const { + SdkStats s; + s.packets_received_from_device = packets_received_from_device.load(std::memory_order_relaxed); + s.bytes_received_from_device = bytes_received_from_device.load(std::memory_order_relaxed); + s.packets_stored_to_shmem = packets_stored_to_shmem.load(std::memory_order_relaxed); + s.packets_queued_for_callback = packets_queued_for_callback.load(std::memory_order_relaxed); + s.packets_delivered_to_callback = packets_delivered_to_callback.load(std::memory_order_relaxed); + s.packets_dropped = packets_dropped.load(std::memory_order_relaxed); + s.queue_max_depth = queue_max_depth.load(std::memory_order_relaxed); + s.packets_sent_to_device = packets_sent_to_device.load(std::memory_order_relaxed); + s.shmem_store_errors = shmem_store_errors.load(std::memory_order_relaxed); + s.receive_errors = receive_errors.load(std::memory_order_relaxed); + s.send_errors = send_errors.load(std::memory_order_relaxed); + return s; + } + }; + AtomicStats stats; + + // Running state + std::atomic is_running{false}; + + // Shutdown guard — set during stop()/~Impl() to make callbacks bail out immediately. + // Separate from is_running because callbacks must work before is_running is set + // (e.g., during the handshake phase in start()). + std::atomic shutting_down{false}; + + /// Apply CMP position overlay to a CHANREP packet's channel in device_config. + /// Called from the receive path when a CHANREP is received. + void applyCmpOverlay(const cbPKT_CHANINFO& chaninfo) { + std::lock_guard lock(cmp_mutex); + if (cmp_positions.empty()) return; + + uint64_t key = cmpKey(chaninfo.bank, chaninfo.term); + auto it = cmp_positions.find(key); + if (it == cmp_positions.end()) return; + + const auto& pos = it->second; + + // Overlay in device_config (STANDALONE mode primary config source) + if (device_session) { + const uint32_t chan = chaninfo.chan; + if (chan >= 1 && chan <= cbMAXCHANS) { + // getChanInfo returns const, but we need to write — use getDeviceConfig + auto& config = const_cast(device_session->getDeviceConfig()); + std::memcpy(config.chaninfo[chan - 1].position, pos.data(), sizeof(int32_t) * 4); + } + } + + // Also overlay in shmem (so CLIENTs see correct positions) + if (shmem_session) { + const uint32_t chan = chaninfo.chan; + if (chan >= 1 && chan <= cbMAXCHANS) { + auto ci_result = shmem_session->getChanInfo(chan - 1); + if (ci_result.isOk()) { + auto ci = ci_result.value(); + std::memcpy(ci.position, pos.data(), sizeof(int32_t) * 4); + shmem_session->setChanInfo(chan - 1, ci); + } + } + } + } + + /// Apply CMP positions to all existing chaninfo entries. + /// Called after loading a CMP file to overlay positions immediately. + void applyCmpToAllChannels() { + for (uint32_t ch = 0; ch < cbMAXCHANS; ++ch) { + const cbPKT_CHANINFO* ci = nullptr; + if (device_session) { + ci = device_session->getChanInfo(ch + 1); + } else if (shmem_session) { + const auto* native = shmem_session->getNativeConfigBuffer(); + if (native) ci = &native->chaninfo[ch]; + } + if (ci && ci->chan > 0) { + applyCmpOverlay(*ci); + } + } + } + + /// Dispatch a single packet to all matching typed callbacks. + /// Called on the callback thread (off the queue). + /// Snapshots each callback vector under lock, then dispatches without lock (Phase 2, Fix 6). + void dispatchPacket(const cbPKT_GENERIC& pkt) { + const uint16_t chid = pkt.cbpkt_header.chid; + + // Snapshot callback vectors under lock (fast: just copies a few pointers+sizes) + std::vector snap_packet; + std::vector snap_event; + std::vector snap_group; + std::vector snap_config; + { + std::lock_guard lock(user_callback_mutex); + snap_packet = packet_callbacks; + // Only snapshot the vectors we'll actually need for this packet type + if (chid != 0 && !(chid & cbPKTCHAN_CONFIGURATION)) { + snap_event = event_callbacks; + } else if (chid == 0) { + snap_group = group_callbacks; + } else if (chid & cbPKTCHAN_CONFIGURATION) { + snap_config = config_callbacks; + } + } + + // Dispatch without holding the lock — user callbacks can take arbitrary time + for (const auto& cb : snap_packet) { + if (cb.cb) cb.cb(pkt); + } + + if (chid != 0 && !(chid & cbPKTCHAN_CONFIGURATION)) { + // Look up cached channel type (Phase 3, Fix 10) + ChannelType pkt_chan_type = ChannelType::ANY; + if (channel_cache_valid && chid >= 1 && chid <= cbMAXCHANS) { + pkt_chan_type = channel_type_cache[chid - 1]; + } + for (const auto& cb : snap_event) { + if (cb.channel_type == ChannelType::ANY || cb.channel_type == pkt_chan_type) { + if (cb.cb) cb.cb(pkt); + } + } + } else if (chid == 0) { + for (const auto& cb : snap_group) { + if (pkt.cbpkt_header.type == cb.group_id) { + if (cb.cb) cb.cb(reinterpret_cast(pkt)); + } + } + } else if (chid & cbPKTCHAN_CONFIGURATION) { + for (const auto& cb : snap_config) { + if (pkt.cbpkt_header.type == cb.packet_type) { + if (cb.cb) cb.cb(pkt); + } + } + } + } + + ~Impl() { + // Mark as shutting down so callbacks bail out immediately + shutting_down.store(true, std::memory_order_release); + is_running.store(false); + + if (device_session) { + // Unregister callbacks first (blocks until any in-progress callback completes) + if (receive_callback_handle != 0) { + device_session->unregisterCallback(receive_callback_handle); + } + if (datagram_callback_handle != 0) { + device_session->unregisterCallback(datagram_callback_handle); + } + // Then stop device receive thread + device_session->stopReceiveThread(); + } + // Stop device send thread + if (device_send_thread_running.load()) { + device_send_thread_running.store(false); + if (device_send_thread && device_send_thread->joinable()) { + device_send_thread->join(); + } + } + // Stop callback thread + if (callback_thread_running.load()) { + callback_thread_running.store(false); + callback_cv.notify_one(); + if (callback_thread && callback_thread->joinable()) { + callback_thread->join(); + } + } + // Stop shmem receive thread (CLIENT mode) + if (shmem_receive_thread_running.load()) { + shmem_receive_thread_running.store(false); + if (shmem_receive_thread && shmem_receive_thread->joinable()) { + shmem_receive_thread->join(); + } + } + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// SdkSession Implementation +/////////////////////////////////////////////////////////////////////////////////////////////////// + +SdkSession::SdkSession() + : m_impl(std::make_unique()) { +} + +SdkSession::SdkSession(SdkSession&&) noexcept = default; +SdkSession& SdkSession::operator=(SdkSession&&) noexcept = default; + +SdkSession::~SdkSession() { + if (m_impl) { // Check if moved-from + stop(); + } +} + +// Helper function to map DeviceType to shared memory instance number. +// Central creates a SINGLE shared memory instance (0) for ALL instruments in a +// Gemini system. The different instruments (Hub1-3, NSP) share the same buffers +// and are distinguished by instrument INDEX within the buffers, not by separate +// shared memory instances. Therefore all device types map to instance 0. +static int getInstanceNumber(DeviceType /*type*/) { + return 0; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Central-compatible shared memory naming +// Names match Central's naming convention: base name + optional instance suffix +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Helper function to get Central-compatible shared memory names +// Returns config buffer name (e.g., "cbCFGbuffer" or "cbCFGbuffer1") +static std::string getCentralConfigBufferName(DeviceType type) { + int instance = getInstanceNumber(type); + if (instance == 0) { + return "cbCFGbuffer"; + } else { + return "cbCFGbuffer" + std::to_string(instance); + } +} + +// Helper function to get Central-compatible transmit buffer name +// Returns transmit buffer name (e.g., "XmtGlobal" or "XmtGlobal1") +static std::string getCentralTransmitBufferName(DeviceType type) { + int instance = getInstanceNumber(type); + if (instance == 0) { + return "XmtGlobal"; + } else { + return "XmtGlobal" + std::to_string(instance); + } +} + +// Helper function to get Central-compatible receive buffer name +// Returns receive buffer name (e.g., "cbRECbuffer" or "cbRECbuffer1") +static std::string getCentralReceiveBufferName(DeviceType type) { + int instance = getInstanceNumber(type); + if (instance == 0) { + return "cbRECbuffer"; + } else { + return "cbRECbuffer" + std::to_string(instance); + } +} + +// Helper function to get Central-compatible local transmit buffer name +// Returns local transmit buffer name (e.g., "XmtLocal" or "XmtLocal1") +static std::string getCentralLocalTransmitBufferName(DeviceType type) { + int instance = getInstanceNumber(type); + if (instance == 0) { + return "XmtLocal"; + } else { + return "XmtLocal" + std::to_string(instance); + } +} + +// Helper function to get Central-compatible status buffer name +// Returns status buffer name (e.g., "cbSTATUSbuffer" or "cbSTATUSbuffer1") +static std::string getCentralStatusBufferName(DeviceType type) { + int instance = getInstanceNumber(type); + if (instance == 0) { + return "cbSTATUSbuffer"; + } else { + return "cbSTATUSbuffer" + std::to_string(instance); + } +} + +// Helper function to get Central-compatible spike cache buffer name +// Returns spike cache buffer name (e.g., "cbSPKbuffer" or "cbSPKbuffer1") +static std::string getCentralSpikeBufferName(DeviceType type) { + int instance = getInstanceNumber(type); + if (instance == 0) { + return "cbSPKbuffer"; + } else { + return "cbSPKbuffer" + std::to_string(instance); + } +} + +// Helper function to get Central-compatible signal event name +// Returns signal event name (e.g., "cbSIGNALevent" or "cbSIGNALevent1") +static std::string getCentralSignalEventName(DeviceType type) { + int instance = getInstanceNumber(type); + if (instance == 0) { + return "cbSIGNALevent"; + } else { + return "cbSIGNALevent" + std::to_string(instance); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Native-mode shared memory naming +// Names use per-device segments: "cbshm_{device}_{segment}" +/////////////////////////////////////////////////////////////////////////////////////////////////// + +static const char* getNativeDeviceName(DeviceType type) { + switch (type) { + case DeviceType::LEGACY_NSP: return "legacy_nsp"; + case DeviceType::NSP: return "nsp"; + case DeviceType::HUB1: return "hub1"; + case DeviceType::HUB2: return "hub2"; + case DeviceType::HUB3: return "hub3"; + case DeviceType::NPLAY: return "nplay"; + default: return "unknown"; + } +} + +static std::string getNativeSegmentName(DeviceType type, const std::string& segment) { + return std::string("cbshm_") + getNativeDeviceName(type) + "_" + segment; +} + +/// @brief Map DeviceType to Central's instrument index (GEMSTART==2 mapping) +/// +/// Central hardcodes instrument assignments at compile time. +/// With GEMSTART==2 (current build): Hub1=0, Hub2=1, Hub3=2, NSP=3 +/// With single-device (non-Gemini): instrument 0 +/// +static int32_t getCentralInstrumentIndex(DeviceType type) { + switch (type) { + case DeviceType::HUB1: return 0; + case DeviceType::HUB2: return 1; + case DeviceType::HUB3: return 2; + case DeviceType::NSP: return 3; + case DeviceType::LEGACY_NSP: return 0; // Non-Gemini, single instrument + default: return -1; // No filter + } +} + +Result SdkSession::create(const SdkConfig& config) { + SdkSession session; + session.m_impl->config = config; + + // Three-way shared memory detection: + // 1. Try Central compat CLIENT: attach to existing Central-named segments + // 2. Try native CLIENT: attach to existing native-named segments + // 3. Fall back to native STANDALONE: create new native-mode segments + bool is_standalone = false; + + // --- Attempt 1: Central-compatible CLIENT mode --- + // Try to attach to Central's shared memory (Central is running) + std::string central_cfg = getCentralConfigBufferName(config.device_type); + std::string central_rec = getCentralReceiveBufferName(config.device_type); + std::string central_xmt = getCentralTransmitBufferName(config.device_type); + std::string central_xmt_local = getCentralLocalTransmitBufferName(config.device_type); + std::string central_status = getCentralStatusBufferName(config.device_type); + std::string central_spk = getCentralSpikeBufferName(config.device_type); + std::string central_signal = getCentralSignalEventName(config.device_type); + + auto shmem_result = cbshm::ShmemSession::create( + central_cfg, central_rec, central_xmt, central_xmt_local, + central_status, central_spk, central_signal, + cbshm::Mode::CLIENT, cbshm::ShmemLayout::CENTRAL_COMPAT); + + if (shmem_result.isOk()) { + // Set instrument filter for CENTRAL_COMPAT mode (Central's receive buffer + // contains packets from ALL instruments; we only want our device's packets) + int32_t inst_idx = getCentralInstrumentIndex(config.device_type); + shmem_result.value().setInstrumentFilter(inst_idx); + } + + if (shmem_result.isError()) { + // --- Attempt 2: Native CLIENT mode --- + // Try to attach to an existing CereLink STANDALONE's native segments + std::string native_cfg = getNativeSegmentName(config.device_type, "config"); + std::string native_rec = getNativeSegmentName(config.device_type, "receive"); + std::string native_xmt = getNativeSegmentName(config.device_type, "xmt_global"); + std::string native_xmt_local = getNativeSegmentName(config.device_type, "xmt_local"); + std::string native_status = getNativeSegmentName(config.device_type, "status"); + std::string native_spk = getNativeSegmentName(config.device_type, "spike"); + std::string native_signal = getNativeSegmentName(config.device_type, "signal"); + + shmem_result = cbshm::ShmemSession::create( + native_cfg, native_rec, native_xmt, native_xmt_local, + native_status, native_spk, native_signal, + cbshm::Mode::CLIENT, cbshm::ShmemLayout::NATIVE); + + // Liveness check: reject stale segments from a dead STANDALONE process. + // The ShmemSession destructor (triggered by reassignment) unmaps the segments; + // the subsequent STANDALONE creation path will shm_unlink + recreate them. + if (shmem_result.isOk() && !shmem_result.value().isOwnerAlive()) { + shmem_result = cbshm::Result::error( + "Stale shared memory detected (owner process dead)"); + } + + if (shmem_result.isError()) { + // --- Attempt 3: Native STANDALONE mode --- + // No existing shared memory found, create new native-mode segments + shmem_result = cbshm::ShmemSession::create( + native_cfg, native_rec, native_xmt, native_xmt_local, + native_status, native_spk, native_signal, + cbshm::Mode::STANDALONE, cbshm::ShmemLayout::NATIVE); + + if (shmem_result.isError()) { + return Result::error("Failed to create shared memory: " + shmem_result.error()); + } + is_standalone = true; + } + } + + session.m_impl->shmem_session = std::move(shmem_result.value()); + + // Create device session only in STANDALONE mode + if (is_standalone) { + // Map SDK DeviceType to cbdev DeviceType + cbdev::DeviceType dev_type; + switch (config.device_type) { + case DeviceType::LEGACY_NSP: + dev_type = cbdev::DeviceType::LEGACY_NSP; + break; + case DeviceType::NSP: + dev_type = cbdev::DeviceType::NSP; + break; + case DeviceType::HUB1: + dev_type = cbdev::DeviceType::HUB1; + break; + case DeviceType::HUB2: + dev_type = cbdev::DeviceType::HUB2; + break; + case DeviceType::HUB3: + dev_type = cbdev::DeviceType::HUB3; + break; + case DeviceType::NPLAY: + dev_type = cbdev::DeviceType::NPLAY; + break; + default: + return Result::error("Invalid device type"); + } + + // Create device config from device type (uses predefined addresses/ports) + cbdev::ConnectionParams dev_config = cbdev::ConnectionParams::forDevice(dev_type); + + // Apply custom addresses/ports if specified (overrides device type defaults) + if (config.custom_device_address.has_value()) { + dev_config.device_address = config.custom_device_address.value(); + } + if (config.custom_client_address.has_value()) { + dev_config.client_address = config.custom_client_address.value(); + } + if (config.custom_device_port.has_value()) { + dev_config.send_port = config.custom_device_port.value(); + } + if (config.custom_client_port.has_value()) { + dev_config.recv_port = config.custom_client_port.value(); + } + + dev_config.recv_buffer_size = config.recv_buffer_size; + dev_config.non_blocking = config.non_blocking; + + auto dev_result = cbdev::createDeviceSession(dev_config); + if (dev_result.isError()) { + return Result::error("Failed to create device session: " + dev_result.error()); + } + session.m_impl->device_session = std::move(dev_result.value()); + + // TODO [Phase 3]: Config parsing now happens in SDK receive thread, not DeviceSession + // DeviceSession no longer has setConfigBuffer() method + // Config buffer management is now SDK's responsibility + + // Start the session (starts receive/send threads) + // Start session (for STANDALONE mode, this also connects to device and performs handshake) + auto start_result = session.start(); + if (start_result.isError()) { + return Result::error("Failed to start session: " + start_result.error()); + } + } else { + // CLIENT mode - start the shmem receive thread (no device session needed) + auto start_result = session.start(); + if (start_result.isError()) { + return Result::error("Failed to start CLIENT session: " + start_result.error()); + } + // Build channel type cache from existing shmem config + session.m_impl->rebuildChannelTypeCache(); + } + + return Result::ok(std::move(session)); +} + +Result SdkSession::start() { + if (m_impl->is_running.load()) { + return Result::error("Session is already running"); + } + + // Set up device callbacks (if in STANDALONE mode) + if (m_impl->device_session) { + // STANDALONE mode - start callback thread + device threads + // In STANDALONE mode, we need the callback thread to decouple fast UDP receive from slow user callbacks + + // Start callback thread + m_impl->callback_thread_running.store(true); + // Capture raw pointer to Impl so thread remains valid even if session is moved + Impl* impl = m_impl.get(); + m_impl->callback_thread = std::make_unique([impl]() { + // This is the callback thread - runs user callbacks (can be slow) + constexpr size_t MAX_BATCH = 32; + cbPKT_GENERIC packets[MAX_BATCH]; + + while (impl->callback_thread_running.load()) { + size_t count = 0; + + // Drain available packets from queue (non-blocking) + while (count < MAX_BATCH && impl->packet_queue.pop(packets[count])) { + count++; + } + + if (count > 0) { + impl->callback_thread_waiting.store(false, std::memory_order_relaxed); + + impl->stats.packets_delivered_to_callback.fetch_add(count, std::memory_order_relaxed); + + // Dispatch each packet to matching typed callbacks + for (size_t i = 0; i < count; i++) { + impl->dispatchPacket(packets[i]); + } + } else { + // No packets available - wait for notification + impl->callback_thread_waiting.store(true, std::memory_order_release); + + std::unique_lock lock(impl->callback_mutex); + impl->callback_cv.wait_for(lock, std::chrono::milliseconds(1), + [impl] { return !impl->callback_thread_running.load() || !impl->packet_queue.empty(); }); + } + } + }); + + // Register receive callback - handles each packet from device + m_impl->receive_callback_handle = m_impl->device_session->registerReceiveCallback( + [impl](const cbPKT_GENERIC& pkt) { + // Guard: bail out if session is shutting down to avoid accessing + // members during destruction (prevents intermittent SIGSEGV in debug mode) + if (impl->shutting_down.load(std::memory_order_acquire)) { + return; + } + + // Check for SYSREP packets (handshake responses) + if ((pkt.cbpkt_header.type & 0xF0) == cbPKTTYPE_SYSREP) { + const auto* sysinfo = reinterpret_cast(&pkt); + impl->device_runlevel.store(sysinfo->runlevel, std::memory_order_release); + impl->received_sysrep.store(true, std::memory_order_release); + impl->handshake_cv.notify_all(); + } + + // Store to shared memory + auto store_result = impl->shmem_session->storePacket(pkt); + + // Apply CMP position overlay for CHANREP packets + // DeviceSession::updateConfigFromBuffer already ran (before this callback), + // so device_config has the new chaninfo. We overlay positions in both + // device_config and shmem so all readers see correct positions. + if ((pkt.cbpkt_header.type & 0xF0) == cbPKTTYPE_CHANREP) { + const auto* chaninfo = reinterpret_cast(&pkt); + impl->applyCmpOverlay(*chaninfo); + } + + // Queue for callback + bool queued = impl->packet_queue.push(pkt); + + // Update stats with atomic increments (no mutex needed) + impl->stats.packets_received_from_device.fetch_add(1, std::memory_order_relaxed); + if (store_result.isOk()) { + impl->stats.packets_stored_to_shmem.fetch_add(1, std::memory_order_relaxed); + } else { + impl->stats.shmem_store_errors.fetch_add(1, std::memory_order_relaxed); + } + if (queued) { + impl->stats.packets_queued_for_callback.fetch_add(1, std::memory_order_relaxed); + uint64_t current_depth = impl->packet_queue.size(); + uint64_t prev_max = impl->stats.queue_max_depth.load(std::memory_order_relaxed); + while (current_depth > prev_max && + !impl->stats.queue_max_depth.compare_exchange_weak( + prev_max, current_depth, std::memory_order_relaxed)) {} + } else { + impl->stats.packets_dropped.fetch_add(1, std::memory_order_relaxed); + } + + if (!queued) { + std::lock_guard lock(impl->user_callback_mutex); + if (impl->error_callback) { + impl->error_callback("Packet queue overflow - dropping packets"); + } + } + }); + + // Register datagram complete callback - signals after all packets in a datagram are processed + m_impl->datagram_callback_handle = m_impl->device_session->registerDatagramCompleteCallback( + [impl]() { + // Guard: bail out if session is shutting down + if (impl->shutting_down.load(std::memory_order_acquire)) { + return; + } + + // Periodic clock sync probing + auto now = std::chrono::steady_clock::now(); + if (now - impl->last_clock_probe_time > std::chrono::seconds(5)) { + impl->device_session->sendClockProbe(); + impl->last_clock_probe_time = now; + } + + // Propagate clock sync offset to shmem for CLIENT mode readers + if (auto offset = impl->device_session->getOffsetNs()) { + auto uncertainty = impl->device_session->getUncertaintyNs().value_or(0); + impl->shmem_session->setClockSync(*offset, uncertainty); + } + + // Signal CLIENT processes that new data is available + impl->shmem_session->signalData(); + + // Wake callback thread if waiting + if (impl->callback_thread_waiting.load(std::memory_order_relaxed)) { + impl->callback_cv.notify_one(); + } + }); + + // Start device receive thread (managed by DeviceSession) + auto recv_start_result = m_impl->device_session->startReceiveThread(); + if (recv_start_result.isError()) { + // Failed to start receive thread - clean up + m_impl->callback_thread_running.store(false); + m_impl->callback_cv.notify_one(); + if (m_impl->callback_thread && m_impl->callback_thread->joinable()) { + m_impl->callback_thread->join(); + } + return Result::error("Failed to start device receive thread: " + recv_start_result.error()); + } + + // Start device send thread - dequeues from shmem and sends to device + m_impl->device_send_thread_running.store(true); + m_impl->device_send_thread = std::make_unique([impl]() { + while (impl->device_send_thread_running.load()) { + bool has_packets = false; + + // Try to dequeue and send all available packets + while (true) { + cbPKT_GENERIC pkt = {}; + + // Dequeue packet from shared memory transmit buffer + auto result = impl->shmem_session->dequeuePacket(pkt); + if (result.isError() || !result.value()) { + break; // Error or no more packets + } + + has_packets = true; + + // Send packet to device + auto send_result = impl->device_session->sendPacket(pkt); + if (send_result.isError()) { + impl->stats.send_errors.fetch_add(1, std::memory_order_relaxed); + } else { + impl->stats.packets_sent_to_device.fetch_add(1, std::memory_order_relaxed); + } + } + + if (has_packets) { + // Had packets - check again quickly + std::this_thread::yield(); + } else { + // No packets - wait briefly before checking again + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + } + }); + + // Perform handshaking based on autorun flag + Result handshake_result; + if (m_impl->config.autorun) { + // Fully start device to RUNNING state (includes requestConfiguration) + handshake_result = performStartupHandshake(500); + } else { + // Just request configuration without changing runlevel + handshake_result = requestConfiguration(500); + } + + if (handshake_result.isError()) { + // Clean up device receive thread (managed by DeviceSession) + m_impl->device_session->stopReceiveThread(); + m_impl->device_session->unregisterCallback(m_impl->receive_callback_handle); + m_impl->device_session->unregisterCallback(m_impl->datagram_callback_handle); + m_impl->receive_callback_handle = 0; + m_impl->datagram_callback_handle = 0; + // Clean up device send thread + m_impl->device_send_thread_running.store(false); + if (m_impl->device_send_thread && m_impl->device_send_thread->joinable()) { + m_impl->device_send_thread->join(); + } + // Clean up callback thread + m_impl->callback_thread_running.store(false); + m_impl->callback_cv.notify_one(); + if (m_impl->callback_thread && m_impl->callback_thread->joinable()) { + m_impl->callback_thread->join(); + } + return Result::error("Handshake failed: " + handshake_result.error()); + } + } else { + // CLIENT mode - start shared memory receive thread only + // In CLIENT mode, we don't need a separate callback thread because reading from + // cbRECbuffer is not time-critical (200MB buffer provides ample buffering) + // This eliminates an extra data copy and thread overhead + + m_impl->shmem_receive_thread_running.store(true); + Impl* impl = m_impl.get(); + m_impl->shmem_receive_thread = std::make_unique([impl]() { + // CLIENT mode receive thread: reads from Central's cbRECbuffer, dispatches to callbacks + constexpr size_t MAX_BATCH = 128; + cbPKT_GENERIC packets[MAX_BATCH]; + + while (impl->shmem_receive_thread_running.load()) { + auto wait_result = impl->shmem_session->waitForData(250); + if (wait_result.isError()) { + std::lock_guard lock(impl->user_callback_mutex); + if (impl->error_callback) { + impl->error_callback("Error waiting for shared memory signal: " + wait_result.error()); + } + continue; + } + + if (!wait_result.value()) { + continue; // Timeout + } + + // Drain all available packets from the ring buffer (not just one batch). + // This is important when the signal fires infrequently (e.g., Central + // signals ~100 times/sec) — reading only one batch per wake-up would + // cap throughput at signal_rate × batch_size. + bool had_error = false; + size_t packets_read = 0; + do { + packets_read = 0; + auto read_result = impl->shmem_session->readReceiveBuffer(packets, MAX_BATCH, packets_read); + if (read_result.isError()) { + impl->stats.shmem_store_errors.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(impl->user_callback_mutex); + if (impl->error_callback) { + impl->error_callback("Error reading from shared memory: " + read_result.error()); + } + had_error = true; + break; + } + + if (packets_read > 0) { + impl->stats.packets_delivered_to_callback.fetch_add(packets_read, std::memory_order_relaxed); + for (size_t i = 0; i < packets_read; i++) { + // CLIENT mode: apply CMP position overlay for CHANREP packets + if ((packets[i].cbpkt_header.type & 0xF0) == cbPKTTYPE_CHANREP) { + const auto* chaninfo = reinterpret_cast(&packets[i]); + impl->applyCmpOverlay(*chaninfo); + } + impl->dispatchPacket(packets[i]); + } + } + } while (packets_read == MAX_BATCH && impl->shmem_receive_thread_running.load()); + if (had_error) continue; + } + }); + } + + m_impl->is_running.store(true); + return Result::ok(); +} + +void SdkSession::stop() { + if (!m_impl || !m_impl->is_running.load()) { + return; + } + + m_impl->is_running.store(false); + m_impl->shutting_down.store(true, std::memory_order_release); + + // Stop device threads (if STANDALONE mode) + if (m_impl->device_session) { + // Unregister callbacks FIRST — this blocks until any in-progress callback + // completes (via callback_mutex), then removes them so no new invocations + // can start. Combined with the shutting_down guard in the callbacks themselves, + // this ensures no callback accesses Impl members during/after shutdown. + if (m_impl->receive_callback_handle != 0) { + m_impl->device_session->unregisterCallback(m_impl->receive_callback_handle); + m_impl->receive_callback_handle = 0; + } + if (m_impl->datagram_callback_handle != 0) { + m_impl->device_session->unregisterCallback(m_impl->datagram_callback_handle); + m_impl->datagram_callback_handle = 0; + } + // Stop device receive thread (managed by DeviceSession) + m_impl->device_session->stopReceiveThread(); + // Stop device send thread + if (m_impl->device_send_thread_running.load()) { + m_impl->device_send_thread_running.store(false); + if (m_impl->device_send_thread && m_impl->device_send_thread->joinable()) { + m_impl->device_send_thread->join(); + } + } + } + + // Stop shared memory receive thread (if CLIENT mode) + if (m_impl->shmem_receive_thread_running.load()) { + m_impl->shmem_receive_thread_running.store(false); + if (m_impl->shmem_receive_thread && m_impl->shmem_receive_thread->joinable()) { + m_impl->shmem_receive_thread->join(); + } + } + + // Stop callback thread + m_impl->callback_thread_running.store(false); + m_impl->callback_cv.notify_one(); + if (m_impl->callback_thread && m_impl->callback_thread->joinable()) { + m_impl->callback_thread->join(); + } +} + +bool SdkSession::isRunning() const { + return m_impl && m_impl->is_running.load(); +} + +CallbackHandle SdkSession::registerPacketCallback(PacketCallback callback) const { + std::lock_guard lock(m_impl->user_callback_mutex); + const auto handle = m_impl->next_callback_handle++; + m_impl->packet_callbacks.push_back({handle, std::move(callback)}); + return handle; +} + +CallbackHandle SdkSession::registerEventCallback(const ChannelType channel_type, EventCallback callback) const { + std::lock_guard lock(m_impl->user_callback_mutex); + const auto handle = m_impl->next_callback_handle++; + m_impl->event_callbacks.push_back({handle, channel_type, std::move(callback)}); + return handle; +} + +CallbackHandle SdkSession::registerGroupCallback(const SampleRate rate, GroupCallback callback) const { + const uint8_t group_id = static_cast(rate); + std::lock_guard lock(m_impl->user_callback_mutex); + const auto handle = m_impl->next_callback_handle++; + m_impl->group_callbacks.push_back({handle, group_id, std::move(callback)}); + return handle; +} + +CallbackHandle SdkSession::registerConfigCallback(const uint16_t packet_type, ConfigCallback callback) const { + std::lock_guard lock(m_impl->user_callback_mutex); + const auto handle = m_impl->next_callback_handle++; + m_impl->config_callbacks.push_back({handle, packet_type, std::move(callback)}); + return handle; +} + +void SdkSession::unregisterCallback(CallbackHandle handle) const { + std::lock_guard lock(m_impl->user_callback_mutex); + auto erase_by_handle = [handle](auto& vec) { + vec.erase(std::remove_if(vec.begin(), vec.end(), + [handle](const auto& cb) { return cb.handle == handle; }), + vec.end()); + }; + erase_by_handle(m_impl->packet_callbacks); + erase_by_handle(m_impl->event_callbacks); + erase_by_handle(m_impl->group_callbacks); + erase_by_handle(m_impl->config_callbacks); +} + +void SdkSession::setErrorCallback(ErrorCallback callback) { + std::lock_guard lock(m_impl->user_callback_mutex); + m_impl->error_callback = std::move(callback); +} + +SdkStats SdkSession::getStats() const { + SdkStats stats = m_impl->stats.snapshot(); + stats.queue_current_depth = m_impl->packet_queue.size(); + return stats; +} + +void SdkSession::resetStats() { + m_impl->stats.reset(); +} + +const SdkConfig& SdkSession::getConfig() const { + return m_impl->config; +} + +const cbPKT_SYSINFO* SdkSession::getSysInfo() const { + if (m_impl->device_session) + return &m_impl->device_session->getSysInfo(); + if (m_impl->shmem_session) { + const auto* native = m_impl->shmem_session->getNativeConfigBuffer(); + if (native) + return &native->sysinfo; + } + return nullptr; +} + +const cbPKT_CHANINFO* SdkSession::getChanInfo(const uint32_t chan_id) const { + if (m_impl->device_session) + return m_impl->device_session->getChanInfo(chan_id); + if (m_impl->shmem_session && chan_id >= 1 && chan_id <= cbMAXCHANS) { + const auto* native = m_impl->shmem_session->getNativeConfigBuffer(); + if (native) + return &native->chaninfo[chan_id - 1]; + } + return nullptr; +} + +const cbPKT_GROUPINFO* SdkSession::getGroupInfo(uint32_t group_id) const { + if (group_id == 0 || group_id > cbMAXGROUPS) + return nullptr; + if (m_impl->device_session) { + const auto& config = m_impl->device_session->getDeviceConfig(); + return &config.groupinfo[group_id - 1]; + } + if (m_impl->shmem_session) { + const auto* native = m_impl->shmem_session->getNativeConfigBuffer(); + if (native) + return &native->groupinfo[group_id - 1]; + } + return nullptr; +} + +const cbPKT_FILTINFO* SdkSession::getFilterInfo(const uint32_t filter_id) const { + if (filter_id >= cbMAXFILTS) + return nullptr; + if (m_impl->device_session) { + const auto& config = m_impl->device_session->getDeviceConfig(); + return &config.filtinfo[filter_id]; + } + if (m_impl->shmem_session) { + const auto* native = m_impl->shmem_session->getNativeConfigBuffer(); + if (native) + return &native->filtinfo[filter_id]; + } + return nullptr; +} + +uint32_t SdkSession::getRunLevel() const { + return m_impl->device_runlevel.load(std::memory_order_acquire); +} + +uint32_t SdkSession::getProtocolVersion() const { + if (m_impl->device_session) + return static_cast(m_impl->device_session->getProtocolVersion()); + return 0; // CLIENT mode — no protocol version available +} + +std::string SdkSession::getProcIdent() const { + if (m_impl->device_session) { + const auto& config = m_impl->device_session->getDeviceConfig(); + // ident is a fixed-size char array, may not be null-terminated + return std::string(config.procinfo.ident, + strnlen(config.procinfo.ident, sizeof(config.procinfo.ident))); + } + return {}; +} + +uint32_t SdkSession::getSpikeLength() const { + const auto* si = getSysInfo(); + return si ? si->spikelen : 0; +} + +uint32_t SdkSession::getSpikePretrigger() const { + const auto* si = getSysInfo(); + return si ? si->spikepre : 0; +} + +Result SdkSession::setSpikeLength(uint32_t spikelen, uint32_t spikepre) { + const auto* si = getSysInfo(); + if (!si) + return Result::error("System info not available"); + cbPKT_SYSINFO pkt = *si; + pkt.cbpkt_header.type = cbPKTTYPE_SYSSETSPKLEN; + pkt.spikelen = spikelen; + pkt.spikepre = spikepre; + return sendPacket(reinterpret_cast(pkt)); +} + +uint64_t SdkSession::getTime() const { + if (!m_impl || !m_impl->shmem_session) + return 0; + return m_impl->shmem_session->getLastTime(); +} + +///-------------------------------------------------------------------------------------------- +/// Channel Configuration +///-------------------------------------------------------------------------------------------- + +/// Helper: extract a numeric field from a cbPKT_CHANINFO +static int64_t extractChanInfoField(const cbPKT_CHANINFO& ci, ChanInfoField field) { + switch (field) { + case ChanInfoField::SMPGROUP: return ci.smpgroup; + case ChanInfoField::SMPFILTER: return ci.smpfilter; + case ChanInfoField::SPKFILTER: return ci.spkfilter; + case ChanInfoField::AINPOPTS: return ci.ainpopts; + case ChanInfoField::SPKOPTS: return ci.spkopts; + case ChanInfoField::SPKTHRLEVEL: return ci.spkthrlevel; + case ChanInfoField::LNCRATE: return ci.lncrate; + case ChanInfoField::REFELECCHAN: return ci.refelecchan; + case ChanInfoField::AMPLREJPOS: return ci.amplrejpos; + case ChanInfoField::AMPLREJNEG: return ci.amplrejneg; + case ChanInfoField::CHANCAPS: return ci.chancaps; + case ChanInfoField::BANK: return ci.bank; + case ChanInfoField::TERM: return ci.term; + default: return 0; + } +} + +/// Helper: get chaninfo pointer for a 0-based channel index +const cbPKT_CHANINFO* SdkSession::Impl::getChanInfoPtr(uint32_t idx) const { + if (device_session) { + return device_session->getChanInfo(idx + 1); + } else if (shmem_session) { + const auto* native = shmem_session->getNativeConfigBuffer(); + if (native) return &native->chaninfo[idx]; + } + return nullptr; +} + +Result SdkSession::getChannelField(uint32_t chanId, ChanInfoField field) const { + if (chanId == 0 || chanId > cbMAXCHANS) + return Result::error("Invalid channel ID"); + const auto* ci = m_impl->getChanInfoPtr(chanId - 1); + if (!ci) + return Result::error("Channel info unavailable"); + return Result::ok(extractChanInfoField(*ci, field)); +} + +Result> SdkSession::getMatchingChannelIds( + const size_t nChans, const ChannelType chanType) const { + std::vector ids; + size_t count = 0; + for (uint32_t ch = 0; ch < cbMAXCHANS && count < nChans; ++ch) { + const auto* ci = m_impl->getChanInfoPtr(ch); + if (!ci) continue; + if (classifyChannelByCaps(*ci) != chanType) continue; + ids.push_back(ch + 1); + count++; + } + return Result>::ok(std::move(ids)); +} + +Result> SdkSession::getChannelField( + const size_t nChans, const ChannelType chanType, const ChanInfoField field) const { + std::vector values; + size_t count = 0; + for (uint32_t ch = 0; ch < cbMAXCHANS && count < nChans; ++ch) { + const auto* ci = m_impl->getChanInfoPtr(ch); + if (!ci) continue; + if (classifyChannelByCaps(*ci) != chanType) continue; + values.push_back(extractChanInfoField(*ci, field)); + count++; + } + return Result>::ok(std::move(values)); +} + +Result> SdkSession::getChannelLabels( + const size_t nChans, const ChannelType chanType) const { + std::vector labels; + size_t count = 0; + for (uint32_t ch = 0; ch < cbMAXCHANS && count < nChans; ++ch) { + const auto* ci = m_impl->getChanInfoPtr(ch); + if (!ci) continue; + if (classifyChannelByCaps(*ci) != chanType) continue; + labels.emplace_back(ci->label); + count++; + } + return Result>::ok(std::move(labels)); +} + +Result> SdkSession::getChannelPositions( + const size_t nChans, const ChannelType chanType) const { + std::vector positions; + size_t count = 0; + for (uint32_t ch = 0; ch < cbMAXCHANS && count < nChans; ++ch) { + const auto* ci = m_impl->getChanInfoPtr(ch); + if (!ci) continue; + if (classifyChannelByCaps(*ci) != chanType) continue; + positions.push_back(ci->position[0]); + positions.push_back(ci->position[1]); + positions.push_back(ci->position[2]); + positions.push_back(ci->position[3]); + count++; + } + return Result>::ok(std::move(positions)); +} + +/// Helper: map cbsdk::ChannelType to cbdev::ChannelType +static cbdev::ChannelType toDevChannelType(const ChannelType chanType) { + switch (chanType) { + case ChannelType::FRONTEND: return cbdev::ChannelType::FRONTEND; + case ChannelType::ANALOG_IN: return cbdev::ChannelType::ANALOG_IN; + case ChannelType::ANALOG_OUT: return cbdev::ChannelType::ANALOG_OUT; + case ChannelType::AUDIO: return cbdev::ChannelType::AUDIO; + case ChannelType::DIGITAL_IN: return cbdev::ChannelType::DIGITAL_IN; + case ChannelType::SERIAL: return cbdev::ChannelType::SERIAL; + case ChannelType::DIGITAL_OUT: return cbdev::ChannelType::DIGITAL_OUT; + default: return cbdev::ChannelType::FRONTEND; + } +} + + +Result SdkSession::setChannelSampleGroup(const size_t nChans, const ChannelType chanType, + const SampleRate rate, const bool disableOthers) { + const uint32_t group_id = static_cast(rate); + // STANDALONE mode: delegate to device session (has full config + direct send) + if (m_impl->device_session) { + const auto r = m_impl->device_session->setChannelsGroupByType( + nChans, toDevChannelType(chanType), static_cast(group_id), disableOthers); + if (r.isError()) + return Result::error(r.error()); + return Result::ok(); + } + + // CLIENT mode: build packets from shmem chaninfo and send through shmem transmit queue + if (!m_impl->shmem_session) + return Result::error("No session available"); + + size_t count = 0; + for (uint32_t chan = 1; chan <= cbMAXCHANS; ++chan) { + if (!disableOthers && count >= nChans) + break; + + auto ci_result = m_impl->shmem_session->getChanInfo(chan - 1); + if (ci_result.isError()) + continue; + auto chaninfo = ci_result.value(); + + if (classifyChannelByCaps(chaninfo) != chanType) + continue; + + const auto grp = count < nChans ? group_id : 0u; + chaninfo.chan = chan; + + if (grp > 0 && grp < 6) { + chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETSMP; + chaninfo.smpgroup = grp; + constexpr uint32_t filter_map[] = {0, 5, 6, 7, 10, 0, 0}; + chaninfo.smpfilter = filter_map[grp]; + if (grp == 5) { + chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSET; + chaninfo.ainpopts &= ~cbAINP_RAWSTREAM; + } + } else if (grp == 6) { + chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETAINP; + chaninfo.ainpopts |= cbAINP_RAWSTREAM; + } else { + chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSET; + chaninfo.smpgroup = 0; + chaninfo.ainpopts &= ~cbAINP_RAWSTREAM; + } + + auto r = sendPacket(reinterpret_cast(chaninfo)); + if (r.isError()) + return r; + count++; + } + + if (count == 0) + return Result::error("No channels found matching type"); + return Result::ok(); +} + +Result SdkSession::setChannelSpikeSorting(const size_t nChans, const ChannelType chanType, + const uint32_t sortOptions) { + // STANDALONE mode: delegate to device session + if (m_impl->device_session) { + const auto r = m_impl->device_session->setChannelsSpikeSortingByType( + nChans, toDevChannelType(chanType), sortOptions); + if (r.isError()) + return Result::error(r.error()); + return Result::ok(); + } + + // CLIENT mode: build packets from shmem chaninfo + if (!m_impl->shmem_session) + return Result::error("No session available"); + + size_t count = 0; + for (uint32_t chan = 1; chan <= cbMAXCHANS && count < nChans; ++chan) { + auto ci_result = m_impl->shmem_session->getChanInfo(chan - 1); + if (ci_result.isError()) + continue; + auto chaninfo = ci_result.value(); + + if (classifyChannelByCaps(chaninfo) != chanType) + continue; + + chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETSPKTHR; + chaninfo.chan = chan; + chaninfo.spkopts &= ~cbAINPSPK_ALLSORT; + chaninfo.spkopts |= sortOptions; + + auto r = sendPacket(reinterpret_cast(chaninfo)); + if (r.isError()) + return r; + count++; + } + + if (count == 0) + return Result::error("No channels found matching type"); + return Result::ok(); +} + +Result SdkSession::setACInputCoupling(const size_t nChans, const ChannelType chanType, + const bool enabled) { + // STANDALONE mode: delegate to device session + if (m_impl->device_session) { + const auto r = m_impl->device_session->setChannelsACInputCouplingByType( + nChans, toDevChannelType(chanType), enabled); + if (r.isError()) + return Result::error(r.error()); + return Result::ok(); + } + + // CLIENT mode: build packets from shmem chaninfo + if (!m_impl->shmem_session) + return Result::error("No session available"); + + size_t count = 0; + for (uint32_t chan = 1; chan <= cbMAXCHANS && count < nChans; ++chan) { + auto ci_result = m_impl->shmem_session->getChanInfo(chan - 1); + if (ci_result.isError()) + continue; + auto chaninfo = ci_result.value(); + + if (classifyChannelByCaps(chaninfo) != chanType) + continue; + + chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSETAINP; + chaninfo.chan = chan; + if (enabled) + chaninfo.ainpopts |= cbAINP_OFFSET_CORRECT; + else + chaninfo.ainpopts &= ~cbAINP_OFFSET_CORRECT; + + auto r = sendPacket(reinterpret_cast(chaninfo)); + if (r.isError()) + return r; + count++; + } + + if (count == 0) + return Result::error("No channels found matching type"); + return Result::ok(); +} + +Result SdkSession::setChannelConfig(const cbPKT_CHANINFO& chaninfo) { + if (m_impl->device_session) + return m_impl->device_session->setChannelConfig(chaninfo); + return sendPacket(reinterpret_cast(chaninfo)); +} + +///-------------------------------------------------------------------------------------------- +/// Comments +///-------------------------------------------------------------------------------------------- + +Result SdkSession::sendComment(const std::string& comment, const uint32_t rgba, const uint8_t charset) { + if (m_impl->device_session) { + return m_impl->device_session->sendComment(comment, rgba, charset); + } + + // CLIENT mode fallback: build packet and route through shmem + cbPKT_COMMENT pkt = {}; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_COMMENTSET; + pkt.cbpkt_header.dlen = cbPKTDLEN_COMMENT; + pkt.info.charset = charset; + pkt.timeStarted = 0; + pkt.rgba = rgba; + + const size_t len = std::min(comment.size(), static_cast(cbMAX_COMMENT - 1)); + std::memcpy(pkt.comment, comment.c_str(), len); + pkt.comment[len] = '\0'; + + return sendPacket(reinterpret_cast(pkt)); +} + +///-------------------------------------------------------------------------------------------- +/// File Recording (Central-only commands, always routed through shmem) +///-------------------------------------------------------------------------------------------- + +Result SdkSession::sendFileCfgPacket(uint32_t options, uint32_t recording, + const std::string& filename, const std::string& comment) { + cbPKT_FILECFG pkt = {}; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_SETFILECFG; + pkt.cbpkt_header.dlen = cbPKTDLEN_FILECFG; + pkt.options = options; + pkt.extctrl = 0; + pkt.recording = recording; + + if (!filename.empty()) { + const size_t fnlen = std::min(filename.size(), sizeof(pkt.filename) - 1); + std::memcpy(pkt.filename, filename.c_str(), fnlen); + pkt.filename[fnlen] = '\0'; + } + + if (!comment.empty()) { + const size_t cmtlen = std::min(comment.size(), sizeof(pkt.comment) - 1); + std::memcpy(pkt.comment, comment.c_str(), cmtlen); + pkt.comment[cmtlen] = '\0'; + } + + // Fill username with computer name (Central uses this to identify the requester) +#ifdef _WIN32 + DWORD cchBuff = sizeof(pkt.username); + GetComputerNameA(pkt.username, &cchBuff); +#else + const char* host = getenv("HOSTNAME"); + if (host) { + strncpy(pkt.username, host, sizeof(pkt.username) - 1); + pkt.username[sizeof(pkt.username) - 1] = '\0'; + } +#endif + + cbPKT_GENERIC generic = {}; + std::memcpy(&generic, &pkt, sizeof(pkt)); + return sendPacket(generic); +} + +Result SdkSession::openCentralFileDialog() { + return sendFileCfgPacket(cbFILECFG_OPT_OPEN, 0, "", ""); +} + +Result SdkSession::closeCentralFileDialog() { + return sendFileCfgPacket(cbFILECFG_OPT_CLOSE, 0, "", ""); +} + +Result SdkSession::startCentralRecording(const std::string& filename, const std::string& comment) { + return sendFileCfgPacket(cbFILECFG_OPT_NONE, 1, filename, comment); +} + +Result SdkSession::stopCentralRecording() { + return sendFileCfgPacket(cbFILECFG_OPT_NONE, 0, "", ""); +} + +///-------------------------------------------------------------------------------------------- +/// Patient Information +///-------------------------------------------------------------------------------------------- + +Result SdkSession::setPatientInfo(const std::string& id, + const std::string& firstname, + const std::string& lastname, + uint32_t dob_month, uint32_t dob_day, uint32_t dob_year) { + cbPKT_PATIENTINFO pkt = {}; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_SETPATIENTINFO; + pkt.cbpkt_header.dlen = cbPKTDLEN_PATIENTINFO; + + std::strncpy(pkt.ID, id.c_str(), cbMAX_PATIENTSTRING - 1); + std::strncpy(pkt.firstname, firstname.c_str(), cbMAX_PATIENTSTRING - 1); + std::strncpy(pkt.lastname, lastname.c_str(), cbMAX_PATIENTSTRING - 1); + pkt.DOBMonth = dob_month; + pkt.DOBDay = dob_day; + pkt.DOBYear = dob_year; + + return sendPacket(reinterpret_cast(pkt)); +} + +///-------------------------------------------------------------------------------------------- +/// Analog Output Monitoring +///-------------------------------------------------------------------------------------------- + +Result SdkSession::setAnalogOutputMonitor(uint32_t aout_chan_id, uint32_t monitor_chan_id, + bool track_last, bool spike_only) { + if (aout_chan_id < 1 || aout_chan_id > cbMAXCHANS) + return Result::error("Invalid analog output channel ID"); + + const cbPKT_CHANINFO* info = getChanInfo(aout_chan_id); + if (!info) + return Result::error("Channel info not available for channel " + std::to_string(aout_chan_id)); + + // Copy current config and modify analog output fields + cbPKT_CHANINFO chaninfo = *info; + + // Set monitor channel + chaninfo.monchan = static_cast(monitor_chan_id); + + // Read-modify-write analog output options (preserve existing flags) + uint32_t opts = chaninfo.aoutopts; + opts &= ~(cbAOUT_MONITORSMP | cbAOUT_MONITORSPK | cbAOUT_TRACK); + if (spike_only) + opts |= cbAOUT_MONITORSPK; + else + opts |= cbAOUT_MONITORSMP; + if (track_last) + opts |= cbAOUT_TRACK; + chaninfo.aoutopts = opts; + + // Send as CHANSET + chaninfo.cbpkt_header.type = cbPKTTYPE_CHANSET; + return setChannelConfig(chaninfo); +} + +///-------------------------------------------------------------------------------------------- +/// Digital Output +///-------------------------------------------------------------------------------------------- + +Result SdkSession::setDigitalOutput(const uint32_t chan_id, const uint16_t value) { + if (m_impl->device_session) + return m_impl->device_session->setDigitalOutput(chan_id, value); + + // CLIENT mode fallback: build packet and route through shmem + cbPKT_SET_DOUT pkt = {}; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_SET_DOUTSET; + pkt.cbpkt_header.dlen = cbPKTDLEN_SET_DOUT; + pkt.chan = static_cast(chan_id); + pkt.value = value; + + return sendPacket(reinterpret_cast(pkt)); +} + +///-------------------------------------------------------------------------------------------- +/// CCF Configuration Files +///-------------------------------------------------------------------------------------------- + +/// Populate a cbCCF struct from a NativeConfigBuffer (CLIENT mode). +/// Mirrors the logic in ccf::extractDeviceConfig() but reads from NativeConfigBuffer fields. +static void extractFromNativeConfig(const cbshm::NativeConfigBuffer& native, cbCCF& ccf_data) +{ + // Digital filters: copy the 4 custom filters + for (int i = 0; i < cbNUM_DIGITAL_FILTERS; ++i) + ccf_data.filtinfo[i] = native.filtinfo[cbFIRST_DIGITAL_FILTER + i]; + + // Channel info + for (int i = 0; i < cbMAXCHANS; ++i) + ccf_data.isChan[i] = native.chaninfo[i]; + + // Adaptive filter + ccf_data.isAdaptInfo = native.adaptinfo; + + // Spike sorting + ccf_data.isSS_Detect = native.pktDetect; + ccf_data.isSS_ArtifactReject = native.pktArtifReject; + for (int i = 0; i < cbNUM_ANALOG_CHANS; ++i) + ccf_data.isSS_NoiseBoundary[i] = native.pktNoiseBoundary[i]; + ccf_data.isSS_Statistics = native.pktStatistics; + + // Spike sorting status: set elapsed minutes to 99 (Central convention) + ccf_data.isSS_Status = native.pktStatus; + ccf_data.isSS_Status.cntlNumUnits.fElapsedMinutes = 99; + ccf_data.isSS_Status.cntlUnitStats.fElapsedMinutes = 99; + + // System info: set type to SYSSETSPKLEN + ccf_data.isSysInfo = native.sysinfo; + ccf_data.isSysInfo.cbpkt_header.type = cbPKTTYPE_SYSSETSPKLEN; + + // N-trodes + for (int i = 0; i < cbMAXNTRODES; ++i) + ccf_data.isNTrodeInfo[i] = native.isNTrodeInfo[i]; + + // LNC + ccf_data.isLnc = native.isLnc; + + // Waveforms: copy and clear active flag + for (int i = 0; i < AOUT_NUM_GAIN_CHANS; ++i) + for (int j = 0; j < cbMAX_AOUT_TRIGGER; ++j) { + ccf_data.isWaveform[i][j] = native.isWaveform[i][j]; + ccf_data.isWaveform[i][j].active = 0; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Mapping (CMP) Files +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result SdkSession::loadChannelMap(const std::string& filepath, uint32_t bank_offset) { + auto parse_result = parseCmpFile(filepath, bank_offset); + if (parse_result.isError()) { + return Result::error(parse_result.error()); + } + + // Merge parsed positions into our map (allows multiple loadChannelMap calls for multi-port) + { + std::lock_guard lock(m_impl->cmp_mutex); + for (auto& [key, pos] : parse_result.value()) { + m_impl->cmp_positions[key] = pos; + } + } + + // Apply positions to all existing chaninfo entries + m_impl->applyCmpToAllChannels(); + + return Result::ok(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// CCF Configuration Files +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result SdkSession::saveCCF(const std::string& filename) { + cbCCF ccf_data{}; + + if (m_impl->device_session) { + ccf::extractDeviceConfig(m_impl->device_session->getDeviceConfig(), ccf_data); + } else if (m_impl->shmem_session) { + const auto* native = m_impl->shmem_session->getNativeConfigBuffer(); + if (!native) + return Result::error("No configuration available in shared memory"); + extractFromNativeConfig(*native, ccf_data); + } else { + return Result::error("No session available"); + } + + CCFUtils writer(false, &ccf_data); + auto result = writer.WriteCCFNoPrompt(filename.c_str()); + if (result != ccf::CCFRESULT_SUCCESS) + return Result::error("Failed to write CCF file"); + return Result::ok(); +} + +Result SdkSession::loadCCF(const std::string& filename) { + cbCCF ccf_data{}; + CCFUtils reader(false, &ccf_data); + + auto result = reader.ReadCCF(filename.c_str(), true); + if (result < ccf::CCFRESULT_SUCCESS) + return Result::error("Failed to read CCF file"); + + auto packets = ccf::buildConfigPackets(ccf_data); + for (const auto& pkt : packets) { + auto send_result = sendPacket(pkt); + if (send_result.isError()) + return send_result; + } + + return Result::ok(); +} + +///-------------------------------------------------------------------------------------------- +/// Clock Synchronization +///-------------------------------------------------------------------------------------------- + +std::optional +SdkSession::toLocalTime(uint64_t device_time_ns) const { + // STANDALONE: delegate to device_session's ClockSync + if (m_impl->device_session) + return m_impl->device_session->toLocalTime(device_time_ns); + + // CLIENT: use clock offset from shmem + if (m_impl->shmem_session) { + auto offset = m_impl->shmem_session->getClockOffsetNs(); + if (offset) { + const auto local_ns = static_cast(device_time_ns) - *offset; + return std::chrono::steady_clock::time_point(std::chrono::nanoseconds(local_ns)); + } + } + return std::nullopt; +} + +std::optional +SdkSession::toDeviceTime(std::chrono::steady_clock::time_point local_time) const { + // STANDALONE: delegate to device_session's ClockSync + if (m_impl->device_session) + return m_impl->device_session->toDeviceTime(local_time); + + // CLIENT: use clock offset from shmem + if (m_impl->shmem_session) { + auto offset = m_impl->shmem_session->getClockOffsetNs(); + if (offset) { + const auto local_ns = std::chrono::duration_cast( + local_time.time_since_epoch()).count(); + return static_cast(local_ns + *offset); + } + } + return std::nullopt; +} + +Result SdkSession::sendClockProbe() { + if (!m_impl->device_session) + return Result::error("sendClockProbe() only available in STANDALONE mode"); + auto r = m_impl->device_session->sendClockProbe(); + if (r.isError()) + return Result::error(r.error()); + return Result::ok(); +} + +std::optional SdkSession::getClockOffsetNs() const { + // STANDALONE: get from device_session's ClockSync + if (m_impl->device_session) + return m_impl->device_session->getOffsetNs(); + + // CLIENT: read from shmem + if (m_impl->shmem_session) + return m_impl->shmem_session->getClockOffsetNs(); + + return std::nullopt; +} + +std::optional SdkSession::getClockUncertaintyNs() const { + // STANDALONE: get from device_session's ClockSync + if (m_impl->device_session) + return m_impl->device_session->getUncertaintyNs(); + + // CLIENT: read from shmem + if (m_impl->shmem_session) + return m_impl->shmem_session->getClockUncertaintyNs(); + + return std::nullopt; +} + +Result SdkSession::sendPacket(const cbPKT_GENERIC& pkt) { + // Enqueue packet to shared memory transmit buffer + // Works in both STANDALONE and CLIENT modes: + // - STANDALONE: send thread will dequeue and transmit + // - CLIENT: STANDALONE process's send thread will pick it up + if (!m_impl) { + return Result::error("sendPacket: m_impl is null"); + } + if (!m_impl->shmem_session) { + return Result::error("sendPacket: shmem_session is null"); + } + + // Stamp packet time from receive buffer (Central's xmt consumer skips packets with time=0) + cbPKT_GENERIC stamped = pkt; + PROCTIME t = m_impl->shmem_session->getLastTime(); + stamped.cbpkt_header.time = (t != 0) ? t : 1; + + auto result = m_impl->shmem_session->enqueuePacket(stamped); + if (result.isOk()) { + return Result::ok(); + } else { + return Result::error(result.error()); + } +} + +///-------------------------------------------------------------------------------------------- +/// Helper: Wait for SYSREP packet +///-------------------------------------------------------------------------------------------- + +bool SdkSession::waitForSysrep(uint32_t timeout_ms, uint32_t expected_runlevel) const { + // Wait for SYSREP packet with optional expected runlevel + // If expected_runlevel is 0, accept any SYSREP + // If expected_runlevel is non-zero, wait for that specific runlevel + std::unique_lock lock(m_impl->handshake_mutex); + return m_impl->handshake_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), + [this, expected_runlevel] { + if (!m_impl->received_sysrep.load(std::memory_order_acquire)) { + return false; // Haven't received SYSREP yet + } + if (expected_runlevel == 0) { + return true; // Accept any SYSREP + } + // Check if runlevel matches + return m_impl->device_runlevel.load(std::memory_order_acquire) == expected_runlevel; + }); +} + +///-------------------------------------------------------------------------------------------- +/// Device Handshaking Methods +///-------------------------------------------------------------------------------------------- + +Result SdkSession::setSystemRunLevel(uint32_t runlevel, uint32_t resetque, uint32_t runflags) { + return setSystemRunLevel(runlevel, resetque, runflags, 0, 500); +} + +Result SdkSession::setSystemRunLevel(uint32_t runlevel, uint32_t resetque, uint32_t runflags, + uint32_t wait_for_runlevel, uint32_t timeout_ms) { + // Reset handshake state before sending + m_impl->received_sysrep.store(false, std::memory_order_relaxed); + + // Send the runlevel packet + Result send_result = Result::error("No session available"); + if (m_impl->device_session) { + send_result = m_impl->device_session->setSystemRunLevel(runlevel, resetque, runflags); + } else { + // CLIENT mode: build packet and send through shmem + cbPKT_SYSINFO pkt = {}; + pkt.cbpkt_header.time = 1; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_SYSSETRUNLEV; + pkt.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; + pkt.runlevel = runlevel; + pkt.resetque = resetque; + pkt.runflags = runflags; + send_result = sendPacket(reinterpret_cast(pkt)); + } + if (send_result.isError()) + return send_result; + + // Wait for SYSREP response + if (!waitForSysrep(timeout_ms, wait_for_runlevel)) { + if (wait_for_runlevel != 0) + return Result::error("No SYSREP response with expected runlevel " + std::to_string(wait_for_runlevel)); + return Result::error("No SYSREP response received for setSystemRunLevel"); + } + + return Result::ok(); +} + +Result SdkSession::requestConfiguration(uint32_t timeout_ms) { + // Reset handshake state before sending + m_impl->received_sysrep.store(false, std::memory_order_relaxed); + + // Send REQCONFIGALL + Result send_result = Result::error("No session available"); + if (m_impl->device_session) { + send_result = m_impl->device_session->requestConfiguration(); + } else { + cbPKT_GENERIC pkt = {}; + pkt.cbpkt_header.time = 1; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_REQCONFIGALL; + pkt.cbpkt_header.dlen = 0; + send_result = sendPacket(pkt); + } + if (send_result.isError()) + return send_result; + + // Wait for final SYSREP from config flood + if (!waitForSysrep(timeout_ms)) + return Result::error("No SYSREP response received for requestConfiguration"); + + return Result::ok(); +} + +Result SdkSession::performStartupHandshake(uint32_t timeout_ms) { + + // Complete device startup sequence to transition device from any state to RUNNING + // + // Sequence: + // 1. Quick device presence check (100ms timeout) - fail fast if device not on network + // 2. Send cbRUNLEVEL_RUNNING - check if device is already running + // 3. If not running, send cbRUNLEVEL_HARDRESET - wait for STANDBY + // 4. Send REQCONFIGALL - wait for config flood ending with SYSREP + // 5. Send cbRUNLEVEL_RESET - wait for device to transition to RUNNING + + // Reset handshake state + m_impl->received_sysrep.store(false, std::memory_order_relaxed); + m_impl->device_runlevel.store(0, std::memory_order_relaxed); + + // Quick presence check - use shorter timeout to fail fast for non-existent devices + const uint32_t presence_check_timeout = std::min(100u, timeout_ms); + + // Step 1: Quick presence check - send cbRUNLEVEL_RUNNING with short timeout to fail fast + Result result = setSystemRunLevel(cbRUNLEVEL_RUNNING, 0, 0, 0, presence_check_timeout); + if (result.isError()) { + // No response - device not on network + return Result::error("Device not reachable (no response to initial probe - check network connection and IP address)"); + } + + // Step 2: Got response - check if device is already running + if (m_impl->device_runlevel.load(std::memory_order_acquire) == cbRUNLEVEL_RUNNING) { + // Device is already running - request config and we're done + goto request_config; + } + + // Step 3: Device responded but not running - send HARDRESET and wait for STANDBY + // Device responds with HARDRESET, then STANDBY + result = setSystemRunLevel(cbRUNLEVEL_HARDRESET, 0, 0, cbRUNLEVEL_STANDBY, timeout_ms); + if (result.isError()) { + return Result::error("Failed to send HARDRESET command: " + result.error()); + } + +request_config: + // Step 4: Request all configuration (always performed) + // requestConfiguration() waits internally for final SYSREP + result = requestConfiguration(timeout_ms); + if (result.isError()) { + return Result::error("Failed to send REQCONFIGALL: " + result.error()); + } + + // Step 5: Get current runlevel and transition to RUNNING if needed + uint32_t current_runlevel = m_impl->device_runlevel.load(std::memory_order_acquire); + + if (current_runlevel != cbRUNLEVEL_RUNNING) { + // Send RESET to complete handshake + // Device is in STANDBY (30) after REQCONFIGALL - send RESET which transitions to RUNNING (50) + // The device responds first with RESET, then on next iteration with RUNNING + result = setSystemRunLevel(cbRUNLEVEL_RESET, 0, 0, cbRUNLEVEL_RUNNING, timeout_ms); + if (result.isError()) { + return Result::error("Failed to send RESET command: " + result.error()); + } + } + + // Success - device is now in RUNNING state + // Build channel type cache now that config is populated + m_impl->rebuildChannelTypeCache(); + return Result::ok(); +} + +} // namespace cbsdk diff --git a/src/cbshm/CMakeLists.txt b/src/cbshm/CMakeLists.txt new file mode 100644 index 00000000..b5456a7e --- /dev/null +++ b/src/cbshm/CMakeLists.txt @@ -0,0 +1,49 @@ +# cbshm - Shared Memory Management Module +# Handles correct indexing between standalone and client modes + +project(cbshm + DESCRIPTION "CereLink Shared Memory Layer (New Architecture)" + LANGUAGES CXX +) + +# Library sources +set(CBSHMEM_SOURCES + src/shmem_session.cpp +) + +# Build as STATIC library +add_library(cbshm STATIC ${CBSHMEM_SOURCES}) + +target_include_directories(cbshm + PUBLIC + $ + $ +) + +# Dependencies +target_link_libraries(cbshm + PUBLIC + cbproto # Protocol definitions + cbutil # Result and shared utilities +) + +# C++17 required +target_compile_features(cbshm PUBLIC cxx_std_17) + +# Platform-specific libraries +if(WIN32) + # Windows shared memory APIs (kernel32 is linked by default) + target_compile_definitions(cbshm PRIVATE _WIN32_WINNT=0x0601) + +elseif(APPLE) + # macOS shared memory APIs + target_link_libraries(cbshm PRIVATE pthread) +else() + # Linux shared memory APIs + target_link_libraries(cbshm PRIVATE rt pthread) +endif() + +# Installation +install(TARGETS cbshm + EXPORT CBSDKTargets +) diff --git a/src/cbshm/README.md b/src/cbshm/README.md new file mode 100644 index 00000000..ce13db99 --- /dev/null +++ b/src/cbshm/README.md @@ -0,0 +1,103 @@ +# cbshm - Shared Memory Management + +**Status:** Phase 2 - ✅ **COMPLETE** (2025-11-11) + +## Purpose + +Internal C++ library that manages shared memory with **Central-compatible layout**. + +**Key Responsibility:** Provide consistent shared memory layout regardless of mode! + +## Core Functionality + +1. **Mode Detection** + - Detect if Central is running (client mode) + - Or operate standalone (direct device connection) + +2. **Correct Indexing** (THE KEY FIX!) + ```cpp + // ALWAYS use packet.instrument as index (mode-independent!) + Result storePacket(const cbPKT_GENERIC& pkt) { + InstrumentId id = InstrumentId::fromPacketField(pkt.cbpkt_header.instrument); + uint8_t idx = id.toIndex(); // 0-based index for array access + + // Store at procinfo[idx], bankinfo[idx], etc. + // Works in BOTH standalone and client mode! + memcpy(&m_cfg_ptr->procinfo[idx], &pkt, sizeof(cbPKT_PROCINFO)); + } + ``` + +3. **Packet Routing** + - `storePacket()`: Route incoming packet to correct shmem location + - Uses packet.instrument field consistently (no mode switching!) + +4. **Config Management** + - Read/write PROCINFO, CHANINFO, etc. + - Uses Central-compatible layout (cbMAXPROCS=4, cbNUM_FE_CHANS=768) + +## Key Design Decisions + +- **C++ only, internal use:** Not exposed to public API +- **Encapsulates indexing logic:** Central fix for the bug +- **Platform-specific:** Different implementations for Windows/macOS/Linux +- **No user-facing discovery:** cbsdk handles "which instrument to use" logic + +## Current Status + +- [x] Directory structure created +- [x] CMake integration added +- [x] ShmemSession class API designed (2025-11-11) +- [x] Upstream shared memory structures examined +- [x] Platform-specific shared memory code implemented (Windows/POSIX) +- [x] Instrument status management implemented +- [x] Configuration read/write implemented +- [x] Packet routing implemented (THE KEY FIX!) +- [x] Build system working (475KB library) +- [x] Unit tests written and passing (18 tests) + +**Phase 2 Status:** ✅ **COMPLETE** +**Build:** ✅ Compiles successfully (475KB library) +**Test Coverage:** ✅ 18 tests, 100% passing + +## Key Insights from Upstream Analysis + +**Critical Discovery:** Central uses different constants than NSP! + +- **NSP (upstream/cbproto/cbproto.h):** + - `cbMAXPROCS = 1` (single processor) + - `cbNUM_FE_CHANS = 256` (channels for one NSP) + +- **Central (upstream/cbhwlib/cbhwlib.h):** + - `cbMAXPROCS = 4` (up to 4 processors) + - `cbNUM_FE_CHANS = 768` (channels for up to 4 NSPs) + +**Design Decision:** cbshm MUST use Central constants to ensure compatibility! + +## API (implemented in include/cbshm/shmem_session.h) + +```cpp +namespace cbshm { + class ShmemSession { + public: + static Result create(const std::string& name, Mode mode); + + // Instrument management + Result isInstrumentActive(InstrumentId id) const; + Result setInstrumentActive(InstrumentId id, bool active); + Result getFirstActiveInstrument() const; + + // Config access + Result getProcInfo(InstrumentId id) const; + Result getChanInfo(uint32_t channel) const; + + // Packet routing (THE KEY FIX!) + Result storePacket(const cbPKT_GENERIC& pkt); + Result storePackets(const cbPKT_GENERIC* pkts, size_t count); + }; +} +``` + +## References + +- Design document: `docs/refactor_plan.md` (Phase 2) +- Current shared memory: `src/cbhwlib/cbhwlib.cpp` diff --git a/src/cbshm/include/cbshm/central_types.h b/src/cbshm/include/cbshm/central_types.h new file mode 100644 index 00000000..784aaa32 --- /dev/null +++ b/src/cbshm/include/cbshm/central_types.h @@ -0,0 +1,296 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file central_types.h +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Central-compatible shared memory structure definitions +/// +/// This file defines the shared memory structures using Central's constants (cbMAXPROCS=4, +/// cbNUM_FE_CHANS=768) to ensure compatibility with Central when it creates shared memory. +/// +/// CRITICAL: These structures MUST match Central's cbhwlib.h exactly! +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBSHM_CENTRAL_TYPES_H +#define CBSHM_CENTRAL_TYPES_H + +// Include InstrumentId from protocol module +#include + +// Include packet structure definitions from cbproto +#include +#include +#include + +#include + +// Ensure tight packing for shared memory structures +#pragma pack(push, 1) + +namespace cbshm { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Central Constants +/// @{ + +// These MUST match Central's constants +constexpr uint32_t CENTRAL_cbMAXPROCS = 4; ///< Central supports up to 4 NSPs +constexpr uint32_t CENTRAL_cbNUM_FE_CHANS = 768; ///< Central supports 768 FE channels +constexpr uint32_t CENTRAL_cbMAXGROUPS = 8; ///< Sample rate groups +constexpr uint32_t CENTRAL_cbMAXFILTS = 32; ///< Digital filters + +// Channel counts +constexpr uint32_t CENTRAL_cbNUM_ANAIN_CHANS = 16 * CENTRAL_cbMAXPROCS; +constexpr uint32_t CENTRAL_cbNUM_ANALOG_CHANS = CENTRAL_cbNUM_FE_CHANS + CENTRAL_cbNUM_ANAIN_CHANS; +constexpr uint32_t CENTRAL_cbNUM_ANAOUT_CHANS = 4 * CENTRAL_cbMAXPROCS; +constexpr uint32_t CENTRAL_cbNUM_AUDOUT_CHANS = 2 * CENTRAL_cbMAXPROCS; +constexpr uint32_t CENTRAL_cbNUM_ANALOGOUT_CHANS = CENTRAL_cbNUM_ANAOUT_CHANS + CENTRAL_cbNUM_AUDOUT_CHANS; +constexpr uint32_t CENTRAL_cbNUM_DIGIN_CHANS = 1 * CENTRAL_cbMAXPROCS; +constexpr uint32_t CENTRAL_cbNUM_SERIAL_CHANS = 1 * CENTRAL_cbMAXPROCS; +constexpr uint32_t CENTRAL_cbNUM_DIGOUT_CHANS = 4 * CENTRAL_cbMAXPROCS; + +// Total channels +constexpr uint32_t CENTRAL_cbMAXCHANS = (CENTRAL_cbNUM_ANALOG_CHANS + CENTRAL_cbNUM_ANALOGOUT_CHANS + + CENTRAL_cbNUM_DIGIN_CHANS + CENTRAL_cbNUM_SERIAL_CHANS + + CENTRAL_cbNUM_DIGOUT_CHANS); + +// Bank definitions +constexpr uint32_t CENTRAL_cbCHAN_PER_BANK = 32; +constexpr uint32_t CENTRAL_cbNUM_FE_BANKS = CENTRAL_cbNUM_FE_CHANS / CENTRAL_cbCHAN_PER_BANK; +constexpr uint32_t CENTRAL_cbNUM_ANAIN_BANKS = 1; +constexpr uint32_t CENTRAL_cbNUM_ANAOUT_BANKS = 1; +constexpr uint32_t CENTRAL_cbNUM_AUDOUT_BANKS = 1; +constexpr uint32_t CENTRAL_cbNUM_DIGIN_BANKS = 1; +constexpr uint32_t CENTRAL_cbNUM_SERIAL_BANKS = 1; +constexpr uint32_t CENTRAL_cbNUM_DIGOUT_BANKS = 1; + +constexpr uint32_t CENTRAL_cbMAXBANKS = (CENTRAL_cbNUM_FE_BANKS + CENTRAL_cbNUM_ANAIN_BANKS + + CENTRAL_cbNUM_ANAOUT_BANKS + CENTRAL_cbNUM_AUDOUT_BANKS + + CENTRAL_cbNUM_DIGIN_BANKS + CENTRAL_cbNUM_SERIAL_BANKS + + CENTRAL_cbNUM_DIGOUT_BANKS); + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Buffer Size Constants (must be defined before structures) +/// @{ + +/// Max UDP packet size (from Central) +constexpr uint32_t CENTRAL_cbCER_UDP_SIZE_MAX = 58080; + +/// Transmit buffer sizes (Central-compatible) +constexpr uint32_t CENTRAL_cbXMT_GLOBAL_BUFFLEN = ((CENTRAL_cbCER_UDP_SIZE_MAX / 4) * 5000 + 2); ///< Room for 5000 packet-sized slots +constexpr uint32_t CENTRAL_cbXMT_LOCAL_BUFFLEN = ((CENTRAL_cbCER_UDP_SIZE_MAX / 4) * 2000 + 2); ///< Room for 2000 packet-sized slots + +/// N-Trode count (Central uses cbNUM_FE_CHANS / 2, not cbNUM_ANALOG_CHANS / 2) +constexpr uint32_t CENTRAL_cbMAXNTRODES = CENTRAL_cbNUM_FE_CHANS / 2; ///< = 384 + +/// Analog output gain channels (Central's multi-instrument count) +constexpr uint32_t CENTRAL_AOUT_NUM_GAIN_CHANS = CENTRAL_cbNUM_ANAOUT_CHANS + CENTRAL_cbNUM_AUDOUT_CHANS; ///< = 24 + +/// Spike cache constants +constexpr uint32_t CENTRAL_cbPKT_SPKCACHEPKTCNT = 400; ///< Packets per channel cache +constexpr uint32_t CENTRAL_cbPKT_SPKCACHELINECNT = CENTRAL_cbMAXCHANS; ///< One cache per channel (Central uses cbMAXCHANS, not cbNUM_ANALOG_CHANS) + +/// Receive buffer size +constexpr uint32_t CENTRAL_cbRECBUFFLEN = CENTRAL_cbNUM_FE_CHANS * 65536 * 4 - 1; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Instrument status flags (bit field) +/// +/// Used to track which instruments are active in shared memory +/// +enum class InstrumentStatus : uint32_t { + INACTIVE = 0x00000000, ///< Instrument slot is not in use + ACTIVE = 0x00000001, ///< Instrument is active and has data +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Central's actual binary layout (CentralLegacyCFGBUFF) +/// +/// This struct matches Central's cbCFGBUFF field order EXACTLY (from cbhwlib.h). +/// It is NOT the same as CereLink's cbConfigBuffer (which reorders fields and adds +/// instrument_status). This struct is used in CENTRAL_COMPAT mode to read Central's +/// shared memory as a CLIENT. +/// +/// Key differences from CereLink's cbConfigBuffer: +/// - optiontable/colortable: 3rd/4th fields here (after sysflags), last fields in CereLink +/// - instrument_status: absent here (Central has no such concept) +/// - isLnc: after isWaveform here, before chaninfo in CereLink +/// - hwndCentral: omitted (at end, variable size, not needed) +/// +struct CentralLegacyCFGBUFF { + uint32_t version; + uint32_t sysflags; + cbOPTIONTABLE optiontable; + cbCOLORTABLE colortable; + cbPKT_SYSINFO sysinfo; + cbPKT_PROCINFO procinfo[CENTRAL_cbMAXPROCS]; + cbPKT_BANKINFO bankinfo[CENTRAL_cbMAXPROCS][CENTRAL_cbMAXBANKS]; + cbPKT_GROUPINFO groupinfo[CENTRAL_cbMAXPROCS][CENTRAL_cbMAXGROUPS]; + cbPKT_FILTINFO filtinfo[CENTRAL_cbMAXPROCS][CENTRAL_cbMAXFILTS]; + cbPKT_ADAPTFILTINFO adaptinfo[CENTRAL_cbMAXPROCS]; + cbPKT_REFELECFILTINFO refelecinfo[CENTRAL_cbMAXPROCS]; + cbPKT_CHANINFO chaninfo[CENTRAL_cbMAXCHANS]; + cbSPIKE_SORTING isSortingOptions; + cbPKT_NTRODEINFO isNTrodeInfo[CENTRAL_cbMAXNTRODES]; + cbPKT_AOUT_WAVEFORM isWaveform[CENTRAL_AOUT_NUM_GAIN_CHANS][cbMAX_AOUT_TRIGGER]; + cbPKT_LNC isLnc[CENTRAL_cbMAXPROCS]; + cbPKT_NPLAY isNPlay; + cbVIDEOSOURCE isVideoSource[cbMAXVIDEOSOURCE]; + cbTRACKOBJ isTrackObj[cbMAXTRACKOBJ]; + cbPKT_FILECFG fileinfo; + // hwndCentral omitted (at end, variable size, not needed by CereLink) +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Transmit buffer for outgoing packets (Global - sent to device) +/// +/// Ring buffer for packets waiting to be transmitted to device via UDP. +/// Buffer stores raw packet data as uint32_t words (Central's format). +/// +/// This is stored in a separate shared memory segment (not embedded in config buffer) +/// to match Central's architecture. +/// +struct CentralTransmitBuffer { + uint32_t transmitted; ///< How many packets have been sent + uint32_t headindex; ///< First empty position (write index) + uint32_t tailindex; ///< One past last emptied position (read index) + uint32_t last_valid_index; ///< Greatest valid starting index + uint32_t bufferlen; ///< Number of indices in buffer + uint32_t buffer[CENTRAL_cbXMT_GLOBAL_BUFFLEN]; ///< Ring buffer for packet data +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Central-compatible configuration buffer +/// +/// This is now an alias to cbConfigBuffer (defined in cbproto/config_buffer.h). +/// The structure maintains Central's cbCFGBUFF layout compatibility. +/// +/// The CENTRAL_* constants are kept for backward compatibility in this module, +/// while cbConfigBuffer uses cbCONFIG_* constants (which have the same values). +/// +/// Static asserts below verify the constants match. +/// +using CentralConfigBuffer = cbConfigBuffer; + +// Verify that CENTRAL_* constants match cbCONFIG_* constants +static_assert(CENTRAL_cbMAXPROCS == cbCONFIG_MAXPROCS, "CENTRAL_cbMAXPROCS must equal cbCONFIG_MAXPROCS"); +static_assert(CENTRAL_cbMAXGROUPS == cbCONFIG_MAXGROUPS, "CENTRAL_cbMAXGROUPS must equal cbCONFIG_MAXGROUPS"); +static_assert(CENTRAL_cbMAXFILTS == cbCONFIG_MAXFILTS, "CENTRAL_cbMAXFILTS must equal cbCONFIG_MAXFILTS"); +static_assert(CENTRAL_cbMAXCHANS == cbCONFIG_MAXCHANS, "CENTRAL_cbMAXCHANS must equal cbCONFIG_MAXCHANS"); +static_assert(CENTRAL_cbMAXBANKS == cbCONFIG_MAXBANKS, "CENTRAL_cbMAXBANKS must equal cbCONFIG_MAXBANKS"); +static_assert(CENTRAL_cbMAXNTRODES == cbCONFIG_MAXNTRODES, "CENTRAL_cbMAXNTRODES must equal cbCONFIG_MAXNTRODES"); +static_assert(CENTRAL_AOUT_NUM_GAIN_CHANS == cbCONFIG_AOUT_NUM_GAIN_CHANS, "CENTRAL_AOUT_NUM_GAIN_CHANS must equal cbCONFIG_AOUT_NUM_GAIN_CHANS"); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Local transmit buffer (IPC-only packets) +/// +/// Smaller than Global buffer, used for cbSendLoopbackPacket() - packets meant only for +/// local processes, not sent to device. +/// +struct CentralTransmitBufferLocal { + uint32_t transmitted; ///< How many packets have been sent + uint32_t headindex; ///< First empty position (write index) + uint32_t tailindex; ///< One past last emptied position (read index) + uint32_t last_valid_index; ///< Greatest valid starting index + uint32_t bufferlen; ///< Number of indices in buffer + uint32_t buffer[CENTRAL_cbXMT_LOCAL_BUFFLEN]; ///< Ring buffer for packet data +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Spike cache buffer +/// +/// Caches recent spike packets for each channel to allow quick access without +/// scanning the entire receive buffer. +/// +struct CentralSpikeCache { + uint32_t chid; ///< Channel ID + uint32_t pktcnt; ///< Number of packets that can be saved + uint32_t pktsize; ///< Size of individual packet + uint32_t head; ///< Where to place next packet (circular) + uint32_t valid; ///< How many packets since last config + cbPKT_SPK spkpkt[CENTRAL_cbPKT_SPKCACHEPKTCNT]; ///< Circular buffer of cached spikes +}; + +struct CentralSpikeBuffer { + uint32_t flags; ///< Status flags + uint32_t chidmax; ///< Maximum channel ID + uint32_t linesize; ///< Size of each cache line + uint32_t spkcount; ///< Total spike count + CentralSpikeCache cache[CENTRAL_cbPKT_SPKCACHELINECNT]; ///< Per-channel spike caches +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief PC Status buffer (flattened from cbPcStatus class) +/// +/// IMPROVEMENT: Flattened to C struct for ABI stability and cross-compiler compatibility. +/// Central uses a C++ class which is fragile across different build environments. +/// +enum class NSPStatus : uint32_t { + NSP_INIT = 0, + NSP_NOIPADDR = 1, + NSP_NOREPLY = 2, + NSP_FOUND = 3, + NSP_INVALID = 4 +}; + +/// @brief App workspace entry (matches Central's APP_WORKSPACE from Launching.h) +/// +/// Central uses `enLaunchView` (C++ enum, sizeof(int) = 4 bytes) for the application field. +/// We use uint32_t for ABI compatibility. +/// +constexpr uint32_t CENTRAL_cbMAXAPPWORKSPACES = 10; + +struct CentralAppWorkspace { + uint32_t m_nWorkspace; ///< Workspace number (1-based) + uint32_t m_nApplication; ///< Application index (enLaunchView in Central, uint32_t for ABI compat) + uint32_t m_nChannel; ///< Channel number displayed (1-based) + uint32_t m_nLeft; + uint32_t m_nTop; + uint32_t m_nRight; + uint32_t m_nBottom; +}; + +struct CentralPCStatus { + // Public data + cbPKT_UNIT_SELECTION isSelection[CENTRAL_cbMAXPROCS]; ///< Unit selection per instrument + + // Status fields (was private in cbPcStatus) + int32_t m_iBlockRecording; ///< Recording block counter + uint32_t m_nPCStatusFlags; ///< PC status flags + uint32_t m_nNumFEChans; ///< Number of FE channels + uint32_t m_nNumAnainChans; ///< Number of analog input channels + uint32_t m_nNumAnalogChans; ///< Number of analog channels + uint32_t m_nNumAoutChans; ///< Number of analog output channels + uint32_t m_nNumAudioChans; ///< Number of audio channels + uint32_t m_nNumAnalogoutChans; ///< Number of analog output channels + uint32_t m_nNumDiginChans; ///< Number of digital input channels + uint32_t m_nNumSerialChans; ///< Number of serial channels + uint32_t m_nNumDigoutChans; ///< Number of digital output channels + uint32_t m_nNumTotalChans; ///< Total channel count + NSPStatus m_nNspStatus[CENTRAL_cbMAXPROCS]; ///< NSP status per instrument + uint32_t m_nNumNTrodesPerInstrument[CENTRAL_cbMAXPROCS];///< NTrode count per instrument + uint32_t m_nGeminiSystem; ///< Gemini system flag + CentralAppWorkspace m_icAppWorkspace[CENTRAL_cbMAXAPPWORKSPACES]; ///< App workspace config +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Receive buffer for incoming packets (simplified for Phase 2) +/// +struct CentralReceiveBuffer { + uint32_t received; ///< Number of packets received + PROCTIME lasttime; ///< Last timestamp + uint32_t headwrap; ///< Head wrap counter + uint32_t headindex; ///< Current head index + uint32_t buffer[CENTRAL_cbRECBUFFLEN]; ///< Packet buffer +}; + +} // namespace cbshm + +#pragma pack(pop) + +#endif // CBSHMEM_CENTRAL_TYPES_H diff --git a/src/cbshm/include/cbshm/config_buffer.h b/src/cbshm/include/cbshm/config_buffer.h new file mode 100644 index 00000000..30926ade --- /dev/null +++ b/src/cbshm/include/cbshm/config_buffer.h @@ -0,0 +1,207 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file config_buffer.h +/// @author CereLink Development Team +/// @date 2025-11-14 +/// +/// @brief Device configuration buffer structure +/// +/// This file defines the configuration buffer structure used to store device state. +/// It supports up to 4 instruments (NSPs) to match Central's capabilities. +/// +/// This structure is used by cbshm: +/// For shared memory (multiple clients access same config) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBPROTO_CONFIG_BUFFER_H +#define CBPROTO_CONFIG_BUFFER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Configuration Buffer Constants +/// @{ + +/// Maximum number of instruments (NSPs) supported +/// This matches Central's multi-instrument capability (up to 4 NSPs) +#define cbCONFIG_MAXPROCS 4 + +/// Maximum number of sample rate groups per instrument +#define cbCONFIG_MAXGROUPS 8 + +/// Maximum number of digital filters per instrument +#define cbCONFIG_MAXFILTS 32 + +/// Number of front-end channels per instrument (Gemini = 768) +#define cbCONFIG_NUM_FE_CHANS 768 + +/// Number of analog input channels per instrument +#define cbCONFIG_NUM_ANAIN_CHANS (16 * cbCONFIG_MAXPROCS) + +/// Total analog channels +#define cbCONFIG_NUM_ANALOG_CHANS (cbCONFIG_NUM_FE_CHANS + cbCONFIG_NUM_ANAIN_CHANS) + +/// Number of analog output channels +#define cbCONFIG_NUM_ANAOUT_CHANS (4 * cbCONFIG_MAXPROCS) + +/// Number of audio output channels +#define cbCONFIG_NUM_AUDOUT_CHANS (2 * cbCONFIG_MAXPROCS) + +/// Total analog output channels +#define cbCONFIG_NUM_ANALOGOUT_CHANS (cbCONFIG_NUM_ANAOUT_CHANS + cbCONFIG_NUM_AUDOUT_CHANS) + +/// Number of digital input channels +#define cbCONFIG_NUM_DIGIN_CHANS (1 * cbCONFIG_MAXPROCS) + +/// Number of serial channels +#define cbCONFIG_NUM_SERIAL_CHANS (1 * cbCONFIG_MAXPROCS) + +/// Number of digital output channels +#define cbCONFIG_NUM_DIGOUT_CHANS (4 * cbCONFIG_MAXPROCS) + +/// Total channels supported +#define cbCONFIG_MAXCHANS (cbCONFIG_NUM_ANALOG_CHANS + cbCONFIG_NUM_ANALOGOUT_CHANS + \ + cbCONFIG_NUM_DIGIN_CHANS + cbCONFIG_NUM_SERIAL_CHANS + \ + cbCONFIG_NUM_DIGOUT_CHANS) + +/// Channels per bank +#define cbCONFIG_CHAN_PER_BANK 32 + +/// Number of front-end banks +#define cbCONFIG_NUM_FE_BANKS (cbCONFIG_NUM_FE_CHANS / cbCONFIG_CHAN_PER_BANK) + +/// Number of banks per type +#define cbCONFIG_NUM_ANAIN_BANKS 1 +#define cbCONFIG_NUM_ANAOUT_BANKS 1 +#define cbCONFIG_NUM_AUDOUT_BANKS 1 +#define cbCONFIG_NUM_DIGIN_BANKS 1 +#define cbCONFIG_NUM_SERIAL_BANKS 1 +#define cbCONFIG_NUM_DIGOUT_BANKS 1 + +/// Total banks per instrument +#define cbCONFIG_MAXBANKS (cbCONFIG_NUM_FE_BANKS + cbCONFIG_NUM_ANAIN_BANKS + \ + cbCONFIG_NUM_ANAOUT_BANKS + cbCONFIG_NUM_AUDOUT_BANKS + \ + cbCONFIG_NUM_DIGIN_BANKS + cbCONFIG_NUM_SERIAL_BANKS + \ + cbCONFIG_NUM_DIGOUT_BANKS) + +/// Maximum n-trodes (stereotrode minimum) for multi-instrument config buffer +#define cbCONFIG_MAXNTRODES (cbCONFIG_NUM_FE_CHANS / 2) + +/// Analog output gain channels for multi-instrument config buffer +#define cbCONFIG_AOUT_NUM_GAIN_CHANS (cbCONFIG_NUM_ANAOUT_CHANS + cbCONFIG_NUM_AUDOUT_CHANS) + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Instrument status enumeration +/// +/// Used to track which instruments are active in the config buffer +/// +typedef enum { + cbINSTRUMENT_STATUS_INACTIVE = 0x00000000, ///< Instrument slot is not in use + cbINSTRUMENT_STATUS_ACTIVE = 0x00000001, ///< Instrument is active and has data +} cbInstrumentStatus; + +#ifdef __cplusplus +/// C++ type-safe version +enum class InstrumentStatus : uint32_t { + INACTIVE = cbINSTRUMENT_STATUS_INACTIVE, + ACTIVE = cbINSTRUMENT_STATUS_ACTIVE, +}; +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Spike Sorting Combined Structure (Config Buffer Version) +/// +/// This structure aggregates all spike sorting configuration across all channels. +/// This is NOT a packet structure - it's a config storage structure used by Central +/// to maintain the complete spike sorting state. +/// +/// IMPORTANT: Uses cbCONFIG_MAXCHANS (multi-device, 848 channels) not cbMAXCHANS (single-device, 256 channels) +/// +/// Ground truth from upstream/cbhwlib/cbhwlib.h lines 1012-1025 +/// Modified to use config buffer channel counts +/// +typedef struct { + // ***** THESE MUST BE 1ST IN THE STRUCTURE WITH MODELSET LAST OF THESE *** + // ***** SEE WriteCCFNoPrompt() *** + cbPKT_FS_BASIS asBasis[cbCONFIG_MAXCHANS]; ///< All PCA basis values (config buffer size) + cbPKT_SS_MODELSET asSortModel[cbCONFIG_MAXCHANS][cbMAXUNITS + 2]; ///< All spike sorting models (config buffer size) + + //////// Spike sorting options (not channel-specific) + cbPKT_SS_DETECT pktDetect; ///< Detection parameters + cbPKT_SS_ARTIF_REJECT pktArtifReject; ///< Artifact rejection + cbPKT_SS_NOISE_BOUNDARY pktNoiseBoundary[cbCONFIG_MAXCHANS]; ///< Noise boundaries (config buffer size) + cbPKT_SS_STATISTICS pktStatistics; ///< Statistics information + cbPKT_SS_STATUS pktStatus; ///< Spike sorting status + +} cbSPIKE_SORTING; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Device configuration buffer +/// +/// This structure stores the complete configuration state for up to 4 instruments (NSPs). +/// Configuration packets received from the device update this buffer, providing a queryable +/// "database" of the current system configuration. +/// +/// Memory layout matches Central's cbCFGBUFF for compatibility. +/// +/// Size: ~4MB (large structure, typically heap-allocated or in shared memory) +/// +typedef struct { + uint32_t version; ///< Buffer structure version + uint32_t sysflags; ///< System-wide flags + + // Instrument status (multi-instrument tracking) + uint32_t instrument_status[cbCONFIG_MAXPROCS]; ///< Active status for each instrument + + // System configuration + cbPKT_SYSINFO sysinfo; ///< System information + + // Per-instrument configuration + cbPKT_PROCINFO procinfo[cbCONFIG_MAXPROCS]; ///< Processor info + cbPKT_BANKINFO bankinfo[cbCONFIG_MAXPROCS][cbCONFIG_MAXBANKS]; ///< Bank info + cbPKT_GROUPINFO groupinfo[cbCONFIG_MAXPROCS][cbCONFIG_MAXGROUPS]; ///< Sample group info + cbPKT_FILTINFO filtinfo[cbCONFIG_MAXPROCS][cbCONFIG_MAXFILTS]; ///< Filter info + cbPKT_ADAPTFILTINFO adaptinfo[cbCONFIG_MAXPROCS]; ///< Adaptive filter settings + cbPKT_REFELECFILTINFO refelecinfo[cbCONFIG_MAXPROCS]; ///< Reference electrode filter + cbPKT_LNC isLnc[cbCONFIG_MAXPROCS]; ///< Line noise cancellation + + // Channel configuration (shared across all instruments) + cbPKT_CHANINFO chaninfo[cbCONFIG_MAXCHANS]; ///< Channel configuration + + // Spike sorting configuration + cbSPIKE_SORTING isSortingOptions; ///< Spike sorting parameters + + // N-Trode configuration (stereotrode, tetrode, etc.) + cbPKT_NTRODEINFO isNTrodeInfo[cbCONFIG_MAXNTRODES]; ///< N-Trode information + + // Analog output waveform configuration + cbPKT_AOUT_WAVEFORM isWaveform[cbCONFIG_AOUT_NUM_GAIN_CHANS][cbMAX_AOUT_TRIGGER]; ///< Waveform params + + // nPlay file playback configuration + cbPKT_NPLAY isNPlay; ///< nPlay information + + // Video tracking (NeuroMotive) + cbVIDEOSOURCE isVideoSource[cbMAXVIDEOSOURCE]; ///< Video source configuration + cbTRACKOBJ isTrackObj[cbMAXTRACKOBJ]; ///< Trackable objects + + // File recording status + cbPKT_FILECFG fileinfo; ///< File recording configuration + + // Central application UI configuration (option/color tables) + cbOPTIONTABLE optiontable; ///< Option table (32 values) + cbCOLORTABLE colortable; ///< Color table (96 values) + + // Note: hwndCentral (HANDLE) is omitted - platform-specific and only used by Central +} cbConfigBuffer; + +#ifdef __cplusplus +} +#endif + +#endif // CBPROTO_CONFIG_BUFFER_H diff --git a/src/cbshm/include/cbshm/native_types.h b/src/cbshm/include/cbshm/native_types.h new file mode 100644 index 00000000..d1e8b417 --- /dev/null +++ b/src/cbshm/include/cbshm/native_types.h @@ -0,0 +1,211 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file native_types.h +/// @author CereLink Development Team +/// @date 2026-02-08 +/// +/// @brief Native-mode shared memory structure definitions +/// +/// This file defines shared memory structures sized for a single instrument (284 channels) +/// rather than Central's 4-instrument layout (848 channels, ~1.2 GB). +/// +/// Native mode uses per-device segments named "cbshm_{device}_{segment}" and is +/// right-sized for the common case of one device per client. +/// +/// Key differences from Central layout: +/// - Config buffer: single instrument (scalar procinfo, adaptinfo, etc.) +/// - Receive buffer: reuses cbReceiveBuffer from receive_buffer.h (256 FE channels) +/// - Transmit buffers: slots sized for cbPKT_MAX_SIZE (1024 bytes) not cbCER_UDP_SIZE_MAX +/// - Spike cache: 272 analog channels (not 832) +/// - PC Status: single instrument arrays (not [4]) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBSHM_NATIVE_TYPES_H +#define CBSHM_NATIVE_TYPES_H + +#include +#include // For cbMAXBANKS +#include // For CentralSpikeCache reuse +#include + +#pragma pack(push, 1) + +namespace cbshm { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Native Mode Constants +/// @{ + +/// Native mode supports a single instrument with cbMAXPROCS=1 channel counts +constexpr uint32_t NATIVE_NUM_FE_CHANS = cbNUM_FE_CHANS; // 256 +constexpr uint32_t NATIVE_NUM_ANALOG_CHANS = cbNUM_ANALOG_CHANS; // 272 +constexpr uint32_t NATIVE_MAXCHANS = cbMAXCHANS; // 284 +constexpr uint32_t NATIVE_MAXGROUPS = 8; +constexpr uint32_t NATIVE_MAXFILTS = 32; +constexpr uint32_t NATIVE_MAXBANKS = cbMAXBANKS; + +/// Receive buffer length (same formula as cbproto, using 256 FE channels) +/// This reuses cbRECBUFFLEN from receive_buffer.h +constexpr uint32_t NATIVE_cbRECBUFFLEN = cbRECBUFFLEN; + +/// Transmit buffer sizes - slots sized for cbPKT_MAX_SIZE (1024 bytes = 256 uint32_t words) +/// instead of Central's cbCER_UDP_SIZE_MAX (58080 bytes = 14520 uint32_t words) +constexpr uint32_t NATIVE_XMT_SLOT_WORDS = cbPKT_MAX_SIZE / sizeof(uint32_t); // 256 +constexpr uint32_t NATIVE_cbXMT_GLOBAL_BUFFLEN = (NATIVE_XMT_SLOT_WORDS * 5000 + 2); +constexpr uint32_t NATIVE_cbXMT_LOCAL_BUFFLEN = (NATIVE_XMT_SLOT_WORDS * 2000 + 2); + +/// Spike cache constants (one cache per native analog channel) +constexpr uint32_t NATIVE_cbPKT_SPKCACHEPKTCNT = CENTRAL_cbPKT_SPKCACHEPKTCNT; // 400 +constexpr uint32_t NATIVE_cbPKT_SPKCACHELINECNT = NATIVE_NUM_ANALOG_CHANS; // 272 + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Native-mode configuration buffer (single instrument) +/// +/// This structure stores the complete configuration state for a single instrument. +/// It uses scalar fields instead of arrays for per-instrument data, and uses +/// cbMAXCHANS (284) instead of cbCONFIG_MAXCHANS (848) for channel arrays. +/// +typedef struct { + uint32_t version; ///< Buffer structure version + uint32_t sysflags; ///< System-wide flags + + // Single instrument status + uint32_t instrument_status; ///< Active status for this instrument + + // System configuration + cbPKT_SYSINFO sysinfo; ///< System information + + // Single-instrument configuration (scalar, not arrays) + cbPKT_PROCINFO procinfo; ///< Processor info + cbPKT_BANKINFO bankinfo[NATIVE_MAXBANKS]; ///< Bank info + cbPKT_GROUPINFO groupinfo[NATIVE_MAXGROUPS]; ///< Sample group info + cbPKT_FILTINFO filtinfo[NATIVE_MAXFILTS]; ///< Filter info + cbPKT_ADAPTFILTINFO adaptinfo; ///< Adaptive filter settings + cbPKT_REFELECFILTINFO refelecinfo; ///< Reference electrode filter + cbPKT_LNC isLnc; ///< Line noise cancellation + + // Channel configuration (single instrument's channels) + cbPKT_CHANINFO chaninfo[NATIVE_MAXCHANS]; ///< Channel configuration + + // Spike sorting configuration (uses native channel counts) + cbPKT_FS_BASIS asBasis[NATIVE_MAXCHANS]; ///< PCA basis values + cbPKT_SS_MODELSET asSortModel[NATIVE_MAXCHANS][cbMAXUNITS + 2]; ///< Spike sorting models + cbPKT_SS_DETECT pktDetect; ///< Detection parameters + cbPKT_SS_ARTIF_REJECT pktArtifReject; ///< Artifact rejection + cbPKT_SS_NOISE_BOUNDARY pktNoiseBoundary[NATIVE_MAXCHANS]; ///< Noise boundaries + cbPKT_SS_STATISTICS pktStatistics; ///< Statistics information + cbPKT_SS_STATUS pktStatus; ///< Spike sorting status + + // N-Trode configuration + cbPKT_NTRODEINFO isNTrodeInfo[cbMAXNTRODES]; ///< N-Trode information + + // Analog output waveform configuration + cbPKT_AOUT_WAVEFORM isWaveform[AOUT_NUM_GAIN_CHANS][cbMAX_AOUT_TRIGGER]; ///< Waveform params + + // nPlay file playback configuration + cbPKT_NPLAY isNPlay; ///< nPlay information + + // Video tracking (NeuroMotive) + cbVIDEOSOURCE isVideoSource[cbMAXVIDEOSOURCE]; ///< Video source configuration + cbTRACKOBJ isTrackObj[cbMAXTRACKOBJ]; ///< Trackable objects + + // File recording status + cbPKT_FILECFG fileinfo; ///< File recording configuration + + // Application UI configuration + cbOPTIONTABLE optiontable; ///< Option table + cbCOLORTABLE colortable; ///< Color table + + // Clock synchronization (written by STANDALONE, read by CLIENT) + int64_t clock_offset_ns; ///< device_ns - steady_clock_ns (0 if unknown) + int64_t clock_uncertainty_ns; ///< Half-RTT uncertainty in nanoseconds (0 if unknown) + uint32_t clock_sync_valid; ///< Non-zero if clock_offset_ns is valid + uint32_t clock_sync_reserved; ///< Reserved for alignment + + // Ownership tracking (written by STANDALONE at creation, read by CLIENT for liveness check) + uint32_t owner_pid; ///< PID of STANDALONE process that created this segment (0 = unknown) +} NativeConfigBuffer; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Native-mode transmit buffer (slots sized for cbPKT_MAX_SIZE) +/// +struct NativeTransmitBuffer { + uint32_t transmitted; ///< How many packets have been sent + uint32_t headindex; ///< First empty position (write index) + uint32_t tailindex; ///< One past last emptied position (read index) + uint32_t last_valid_index; ///< Greatest valid starting index + uint32_t bufferlen; ///< Number of indices in buffer + uint32_t buffer[NATIVE_cbXMT_GLOBAL_BUFFLEN]; ///< Ring buffer for packet data +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Native-mode local transmit buffer (IPC-only packets) +/// +struct NativeTransmitBufferLocal { + uint32_t transmitted; ///< How many packets have been sent + uint32_t headindex; ///< First empty position (write index) + uint32_t tailindex; ///< One past last emptied position (read index) + uint32_t last_valid_index; ///< Greatest valid starting index + uint32_t bufferlen; ///< Number of indices in buffer + uint32_t buffer[NATIVE_cbXMT_LOCAL_BUFFLEN]; ///< Ring buffer for packet data +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Native-mode spike cache (272 analog channels) +/// +struct NativeSpikeCache { + uint32_t chid; ///< Channel ID + uint32_t pktcnt; ///< Number of packets that can be saved + uint32_t pktsize; ///< Size of individual packet + uint32_t head; ///< Where to place next packet (circular) + uint32_t valid; ///< How many packets since last config + cbPKT_SPK spkpkt[NATIVE_cbPKT_SPKCACHEPKTCNT]; ///< Circular buffer of cached spikes +}; + +struct NativeSpikeBuffer { + uint32_t flags; ///< Status flags + uint32_t chidmax; ///< Maximum channel ID + uint32_t linesize; ///< Size of each cache line + uint32_t spkcount; ///< Total spike count + NativeSpikeCache cache[NATIVE_cbPKT_SPKCACHELINECNT]; ///< Per-channel spike caches +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Native-mode PC status (single instrument) +/// +struct NativePCStatus { + cbPKT_UNIT_SELECTION isSelection; ///< Unit selection (single instrument) + + int32_t m_iBlockRecording; ///< Recording block counter + uint32_t m_nPCStatusFlags; ///< PC status flags + uint32_t m_nNumFEChans; ///< Number of FE channels + uint32_t m_nNumAnainChans; ///< Number of analog input channels + uint32_t m_nNumAnalogChans; ///< Number of analog channels + uint32_t m_nNumAoutChans; ///< Number of analog output channels + uint32_t m_nNumAudioChans; ///< Number of audio channels + uint32_t m_nNumAnalogoutChans; ///< Number of analog output channels + uint32_t m_nNumDiginChans; ///< Number of digital input channels + uint32_t m_nNumSerialChans; ///< Number of serial channels + uint32_t m_nNumDigoutChans; ///< Number of digital output channels + uint32_t m_nNumTotalChans; ///< Total channel count + NSPStatus m_nNspStatus; ///< NSP status (single instrument) + uint32_t m_nNumNTrodesPerInstrument; ///< NTrode count (single instrument) + uint32_t m_nGeminiSystem; ///< Gemini system flag +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Native-mode receive buffer (reuses cbReceiveBuffer from receive_buffer.h) +/// +/// The cbReceiveBuffer struct defined in receive_buffer.h already uses cbNUM_FE_CHANS=256, +/// so it's already native-sized. We use it directly for native mode. +/// +/// For convenience, create a type alias: +using NativeReceiveBuffer = cbReceiveBuffer; + +} // namespace cbshm + +#pragma pack(pop) + +#endif // CBSHM_NATIVE_TYPES_H diff --git a/src/cbshm/include/cbshm/receive_buffer.h b/src/cbshm/include/cbshm/receive_buffer.h new file mode 100644 index 00000000..25d9e5ac --- /dev/null +++ b/src/cbshm/include/cbshm/receive_buffer.h @@ -0,0 +1,57 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file receive_buffer.h +/// @author CereLink Development Team +/// @date 2025-11-15 +/// +/// @brief Receive buffer structure for incoming packets +/// +/// Central-compatible receive buffer that can be shared between cbdev and cbshm. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBPROTO_RECEIVE_BUFFER_H +#define CBPROTO_RECEIVE_BUFFER_H + +#include +#include + +// Ensure tight packing for shared memory structures +#pragma pack(push, 1) + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Receive Buffer Constants +/// @{ + +/// Maximum number of frontend channels (used for buffer size calculation) +#ifndef cbNUM_FE_CHANS +#define cbNUM_FE_CHANS 256 +#endif + +/// Receive buffer length (matches Central's calculation) +/// Formula: cbNUM_FE_CHANS * 65536 * 4 - 1 +/// This provides enough space for a ring buffer of packets from all FE channels +constexpr uint32_t cbRECBUFFLEN = cbNUM_FE_CHANS * 65536 * 4 - 1; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Receive buffer for incoming packets +/// +/// Ring buffer that stores raw packet data received from the device. +/// Matches Central's cbRECBUFF structure exactly. +/// +/// The buffer stores packets as uint32_t words, and the headindex tracks where +/// new data should be written. Packets are modified in-place (instrument ID, proc, bank) +/// before being stored. +/// +struct cbReceiveBuffer { + uint32_t received; ///< Number of packets received + PROCTIME lasttime; ///< Last timestamp seen + uint32_t headwrap; ///< Head wrap counter (for detecting buffer wraps) + uint32_t headindex; ///< Current head index (write position) + uint32_t buffer[cbRECBUFFLEN]; ///< Ring buffer for packet data +}; + +#pragma pack(pop) + +#endif // CBPROTO_RECEIVE_BUFFER_H diff --git a/src/cbshm/include/cbshm/shmem_session.h b/src/cbshm/include/cbshm/shmem_session.h new file mode 100644 index 00000000..9cdad75b --- /dev/null +++ b/src/cbshm/include/cbshm/shmem_session.h @@ -0,0 +1,511 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file shmem_session.h +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Shared memory session management for Cerebus protocol +/// +/// This module provides cross-platform shared memory management for configuration and data +/// buffers used by Central and cbsdk clients. +/// +/// Key Design Principles: +/// - Uses Central-compatible buffer layout (cbMAXPROCS=4, not 1) +/// - Mode-independent indexing (always uses packet.instrument) +/// - Thread-safe for concurrent access +/// - Platform-abstracted (Windows/macOS/Linux) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBSHM_SHMEM_SESSION_H +#define CBSHM_SHMEM_SESSION_H + +// Include Central-compatible types which bring in protocol definitions +#include +#include +#include +#include +#include +#include +#include + +namespace cbshm { + +template +using Result = cbutil::Result; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Operating mode for shared memory session +/// +enum class Mode { + STANDALONE, ///< First client, creates shared memory (owns device) + CLIENT ///< Subsequent client, attaches to existing shared memory +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Buffer layout for shared memory session +/// +/// Controls buffer sizes, struct types, and bounds checking. +/// +enum class ShmemLayout { + CENTRAL, ///< CereLink's own Central-compatible layout (cbConfigBuffer) + CENTRAL_COMPAT, ///< Central's actual binary layout (CentralLegacyCFGBUFF) + NATIVE ///< Native single-instrument layout (NativeConfigBuffer) +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Shared memory session for Cerebus configuration and data buffers +/// +/// Manages lifecycle of shared memory buffers that are compatible with Central's layout. +/// Implements correct indexing for multi-instrument systems. +/// +/// CRITICAL: Even in STANDALONE mode, uses Central-compatible layout (cbMAXPROCS=4) +/// so that subsequent CLIENT connections work correctly. +/// +class ShmemSession { +public: + /////////////////////////////////////////////////////////////////////////// + /// @name Lifecycle + /// @{ + + /// @brief Create a new shared memory session + /// @param cfg_name Config buffer shared memory name (e.g., "cbCFGbuffer") + /// @param rec_name Receive buffer shared memory name (e.g., "cbRECbuffer") + /// @param xmt_name Transmit buffer shared memory name (e.g., "XmtGlobal") + /// @param xmt_local_name Local transmit buffer shared memory name (e.g., "XmtLocal") + /// @param status_name PC status buffer shared memory name (e.g., "cbSTATUSbuffer") + /// @param spk_name Spike cache buffer shared memory name (e.g., "cbSPKbuffer") + /// @param signal_event_name Signal event name (e.g., "cbSIGNALevent") + /// @param mode Operating mode (STANDALONE or CLIENT) + /// @param layout Buffer layout (CENTRAL or NATIVE, default CENTRAL for backward compat) + /// @return Result containing ShmemSession on success, error message on failure + static Result create(const std::string& cfg_name, const std::string& rec_name, + const std::string& xmt_name, const std::string& xmt_local_name, + const std::string& status_name, const std::string& spk_name, + const std::string& signal_event_name, Mode mode, + ShmemLayout layout = ShmemLayout::CENTRAL); + + /// @brief Destructor - closes shared memory and releases resources + ~ShmemSession(); + + // Move-only type (no copying of shared memory sessions) + ShmemSession(ShmemSession&& other) noexcept; + ShmemSession& operator=(ShmemSession&& other) noexcept; + ShmemSession(const ShmemSession&) = delete; + ShmemSession& operator=(const ShmemSession&) = delete; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Status + /// @{ + + /// @brief Check if session is open and ready + /// @return true if session is open, false otherwise + bool isOpen() const; + + /// @brief Get the current operating mode + /// @return STANDALONE or CLIENT + Mode getMode() const; + + /// @brief Get the current buffer layout + /// @return CENTRAL or NATIVE + ShmemLayout getLayout() const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Instrument Status (CRITICAL for multi-instrument) + /// @{ + + /// @brief Get instrument active status + /// @param id Instrument ID (1-based) + /// @return true if instrument is active in shared memory + Result isInstrumentActive(cbproto::InstrumentId id) const; + + /// @brief Set instrument active status + /// @param id Instrument ID (1-based) + /// @param active true to mark active, false to mark inactive + /// @return Result indicating success or failure + Result setInstrumentActive(cbproto::InstrumentId id, bool active); + + /// @brief Get first active instrument ID + /// @return InstrumentId of first active instrument, or error if none active + Result getFirstActiveInstrument() const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Configuration Read Operations + /// @{ + + /// @brief Get processor information for specified instrument + /// @param id Instrument ID (1-based, e.g., cbNSP1) + /// @return cbPKT_PROCINFO structure on success + Result getProcInfo(cbproto::InstrumentId id) const; + + /// @brief Get bank information + /// @param id Instrument ID (1-based) + /// @param bank Bank number (1-based, as used in cbPKT_BANKINFO) + /// @return cbPKT_BANKINFO structure on success + Result getBankInfo(cbproto::InstrumentId id, uint32_t bank) const; + + /// @brief Get filter information + /// @param id Instrument ID (1-based) + /// @param filter Filter number (1-based, as used in cbPKT_FILTINFO) + /// @return cbPKT_FILTINFO structure on success + Result getFilterInfo(cbproto::InstrumentId id, uint32_t filter) const; + + /// @brief Get channel information + /// @param channel Channel number (0-based, global across all instruments) + /// @return cbPKT_CHANINFO structure on success + Result getChanInfo(uint32_t channel) const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Configuration Write Operations + /// @{ + + /// @brief Set processor information for specified instrument + /// @param id Instrument ID (1-based) + /// @param info cbPKT_PROCINFO structure to write + /// @return Result indicating success or failure + Result setProcInfo(cbproto::InstrumentId id, const cbPKT_PROCINFO& info); + + /// @brief Set bank information + /// @param id Instrument ID (1-based) + /// @param bank Bank number (0-based) + /// @param info cbPKT_BANKINFO structure to write + /// @return Result indicating success or failure + Result setBankInfo(cbproto::InstrumentId id, uint32_t bank, const cbPKT_BANKINFO& info); + + /// @brief Set filter information + /// @param id Instrument ID (1-based) + /// @param filter Filter number (0-based) + /// @param info cbPKT_FILTINFO structure to write + /// @return Result indicating success or failure + Result setFilterInfo(cbproto::InstrumentId id, uint32_t filter, const cbPKT_FILTINFO& info); + + /// @brief Set channel information + /// @param channel Channel number (0-based) + /// @param info cbPKT_CHANINFO structure to write + /// @return Result indicating success or failure + Result setChanInfo(uint32_t channel, const cbPKT_CHANINFO& info); + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Configuration Buffer Direct Access + /// @{ + + /// @brief Get direct pointer to Central configuration buffer + /// + /// Provides direct access to the shared memory config buffer for zero-copy + /// operations. Used by SdkSession to connect DeviceSession's config buffer + /// to shared memory. + /// + /// @return Pointer to configuration buffer, or nullptr if not CENTRAL layout + cbConfigBuffer* getConfigBuffer(); + + /// @brief Get direct pointer to Central configuration buffer (const version) + /// @return Const pointer to configuration buffer, or nullptr if not CENTRAL layout + const cbConfigBuffer* getConfigBuffer() const; + + /// @brief Get direct pointer to native configuration buffer + /// @return Pointer to native configuration buffer, or nullptr if not NATIVE layout + NativeConfigBuffer* getNativeConfigBuffer(); + + /// @brief Get direct pointer to native configuration buffer (const version) + /// @return Const pointer to native configuration buffer, or nullptr if not NATIVE layout + const NativeConfigBuffer* getNativeConfigBuffer() const; + + /// @brief Get direct pointer to Central legacy configuration buffer + /// @return Pointer to legacy config buffer, or nullptr if not CENTRAL_COMPAT layout + CentralLegacyCFGBUFF* getLegacyConfigBuffer(); + + /// @brief Get direct pointer to Central legacy configuration buffer (const version) + /// @return Const pointer to legacy config buffer, or nullptr if not CENTRAL_COMPAT layout + const CentralLegacyCFGBUFF* getLegacyConfigBuffer() const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Clock Synchronization + /// @{ + + /// @brief Set clock sync offset (called by STANDALONE mode) + /// @param offset_ns device_ns - steady_clock_ns + /// @param uncertainty_ns Half-RTT uncertainty + void setClockSync(int64_t offset_ns, int64_t uncertainty_ns); + + /// @brief Get clock sync offset (readable by CLIENT mode) + /// @return offset in nanoseconds, or nullopt if no sync data + std::optional getClockOffsetNs() const; + + /// @brief Get clock sync uncertainty + /// @return uncertainty in nanoseconds, or nullopt if no sync data + std::optional getClockUncertaintyNs() const; + + /// @brief Check if the STANDALONE owner of these segments is still alive + /// + /// For NATIVE CLIENT mode, reads owner_pid from the config buffer and checks + /// if that process still exists. Returns true in all other cases (STANDALONE mode, + /// non-NATIVE layout, or unknown PID). + /// + /// @return false if the owner process is confirmed dead (stale segments), true otherwise + bool isOwnerAlive() const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Packet Routing (THE KEY FIX) + /// @{ + + /// @brief Store a packet in shared memory using correct indexing + /// + /// CRITICAL FIX: This method ALWAYS uses packet.cbpkt_header.instrument + /// as the array index, regardless of mode. This ensures: + /// - Standalone mode: packets go to correct slot for later CLIENT access + /// - Client mode: packets go to same slot Central would use + /// + /// @param pkt Generic packet to store + /// @return Result indicating success or failure + Result storePacket(const cbPKT_GENERIC& pkt); + + /// @brief Store multiple packets in batch + /// @param pkts Array of packets + /// @param count Number of packets + /// @return Result indicating success or failure + Result storePackets(const cbPKT_GENERIC* pkts, size_t count); + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Transmit Queue (for sending packets to device) + /// @{ + + /// @brief Enqueue a packet to be sent to the device + /// + /// Writes packet to shared memory transmit buffer. In STANDALONE mode, + /// the device thread will dequeue and send it. In CLIENT mode, the + /// STANDALONE process will dequeue and send it. + /// + /// @param pkt Packet to enqueue for transmission + /// @return Result indicating success or failure (buffer full returns error) + Result enqueuePacket(const cbPKT_GENERIC& pkt); + + /// @brief Dequeue a packet from the transmit buffer + /// + /// Used by STANDALONE mode to get packets to send to device. + /// Returns error if queue is empty. + /// + /// @param pkt Output parameter to receive dequeued packet + /// @return Result - true if packet was dequeued, false if queue empty + Result dequeuePacket(cbPKT_GENERIC& pkt); + + /// @brief Check if transmit queue has packets waiting + /// @return true if queue has packets, false if empty + bool hasTransmitPackets() const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Local Transmit Queue (IPC-only packets) + /// @{ + + /// @brief Enqueue a packet to local transmit buffer (IPC only, not sent to device) + /// + /// Writes packet to XmtLocal buffer for local client communication. + /// These packets are NOT sent to the device - they're only visible to + /// local processes via shared memory. + /// + /// This is the shared memory equivalent of cbSendLoopbackPacket(). + /// + /// @param pkt Packet to enqueue for local IPC + /// @return Result indicating success or failure (buffer full returns error) + Result enqueueLocalPacket(const cbPKT_GENERIC& pkt); + + /// @brief Dequeue a packet from the local transmit buffer + /// + /// Used by local clients to receive IPC-only packets. + /// Returns error if queue is empty. + /// + /// @param pkt Output parameter to receive dequeued packet + /// @return Result - true if packet was dequeued, false if queue empty + Result dequeueLocalPacket(cbPKT_GENERIC& pkt); + + /// @brief Check if local transmit queue has packets waiting + /// @return true if queue has packets, false if empty + bool hasLocalTransmitPackets() const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name PC Status Buffer Access + /// @{ + + /// @brief Get total number of channels in the system + /// @return Total channel count + Result getNumTotalChans() const; + + /// @brief Get NSP status for specified instrument + /// @param id Instrument ID (1-based) + /// @return NSP status (INIT, NOIPADDR, NOREPLY, FOUND, INVALID) + Result getNspStatus(cbproto::InstrumentId id) const; + + /// @brief Set NSP status for specified instrument + /// @param id Instrument ID (1-based) + /// @param status NSP status to set + /// @return Result indicating success or failure + Result setNspStatus(cbproto::InstrumentId id, NSPStatus status); + + /// @brief Check if system is configured as Gemini + /// @return true if Gemini system, false otherwise + Result isGeminiSystem() const; + + /// @brief Set Gemini system flag + /// @param is_gemini true for Gemini system, false otherwise + /// @return Result indicating success or failure + Result setGeminiSystem(bool is_gemini); + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Spike Cache Buffer Access + /// @{ + + /// @brief Get spike cache for a specific channel + /// + /// The spike cache stores the most recent spikes for each channel, + /// allowing tools like Raster to quickly access recent spikes without + /// scanning the entire receive buffer. + /// + /// @param channel Channel number (0-based) + /// @param cache Output parameter to receive spike cache + /// @return Result indicating success or failure + Result getSpikeCache(uint32_t channel, CentralSpikeCache& cache) const; + + /// @brief Get most recent spike packet from cache + /// + /// Returns the most recently cached spike for a channel. This is faster + /// than scanning the receive buffer. + /// + /// @param channel Channel number (0-based) + /// @param spike Output parameter to receive spike packet + /// @return Result - true if spike available, false if cache empty + Result getRecentSpike(uint32_t channel, cbPKT_SPK& spike) const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Instrument Filtering (CENTRAL_COMPAT mode) + /// @{ + + /// @brief Set instrument filter for receive buffer reads + /// + /// In CENTRAL_COMPAT mode, Central's receive buffer contains packets from ALL + /// instruments. This filter causes readReceiveBuffer() to only return packets + /// matching the specified instrument index. + /// + /// @param instrument_index 0-based instrument index to filter for, or -1 for no filter (default) + void setInstrumentFilter(int32_t instrument_index); + + /// @brief Get current instrument filter + /// @return Current filter (-1 = no filter) + int32_t getInstrumentFilter() const; + + /// @brief Get detected protocol version for CENTRAL_COMPAT mode + /// + /// In CENTRAL_COMPAT mode, Central may store packets in an older protocol format. + /// This returns the detected protocol version based on procinfo[0].version. + /// Returns CBPROTO_PROTOCOL_CURRENT for CENTRAL and NATIVE layouts. + /// + /// @return Detected protocol version + cbproto_protocol_version_t getCompatProtocolVersion() const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Receive Buffer Access (Ring Buffer for Incoming Packets) + /// @{ + + /// @brief Read available packets from receive buffer + /// + /// Reads packets that have been written since last read. + /// Uses ring buffer with wrap-around tracking. + /// + /// @param packets Output buffer to receive packets (must be pre-allocated) + /// @param max_packets Maximum number of packets to read + /// @param packets_read Output: actual number of packets read + /// @return Result indicating success or failure + Result readReceiveBuffer(cbPKT_GENERIC* packets, size_t max_packets, size_t& packets_read); + + /// @brief Get current receive buffer statistics + /// + /// Returns information about the receive buffer state for monitoring. + /// + /// @param received Total packets received by writer + /// @param available Packets available to read (not yet consumed) + /// @return Result indicating success or failure + Result getReceiveBufferStats(uint32_t& received, uint32_t& available) const; + + /// @} + + /////////////////////////////////////////////////////////////////////////// + /// @name Data Synchronization (Signal Event) + /// @{ + + /// @brief Wait for data availability signal + /// + /// Blocks until Central signals new data is available, or timeout occurs. + /// This is the shared memory equivalent of cbWaitforData(). + /// + /// @param timeout_ms Timeout in milliseconds (default 250ms) + /// @return Result - true if signal received, false if timeout + Result waitForData(uint32_t timeout_ms = 250) const; + + /// @brief Signal that new data is available + /// + /// Used by STANDALONE mode to notify CLIENT processes that new data + /// has been written to shared memory buffers. + /// + /// On Windows: SetEvent() to signal manual-reset event + /// On POSIX: sem_post() to increment semaphore + /// + /// @return Result indicating success or failure + Result signalData(); + + /// @brief Reset the data signal + /// + /// Used by STANDALONE mode after clients have consumed data. + /// Only applicable to Windows (manual-reset events). + /// + /// On Windows: ResetEvent() to clear the event + /// On POSIX: No-op (semaphore is auto-reset by sem_wait) + /// + /// @return Result indicating success or failure + Result resetSignal(); + + /// @brief Get last timestamp from receive buffer + /// + /// Returns the most recent packet timestamp written to the receive buffer. + /// Used by sendPacket to stamp outgoing packets with a non-zero time + /// (Central's xmt consumer skips packets with time=0). + /// + /// @return Last timestamp, or 0 if receive buffer not initialized + PROCTIME getLastTime() const; + + /// @} + +private: + /// @brief Private constructor (use create() factory method) + ShmemSession(); + + /// @brief Platform-specific implementation (pimpl idiom) + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace cbshm + +#endif // CBSHMEM_SHMEM_SESSION_H diff --git a/src/cbshm/src/platform_first.h b/src/cbshm/src/platform_first.h new file mode 100644 index 00000000..50b64b0d --- /dev/null +++ b/src/cbshm/src/platform_first.h @@ -0,0 +1,32 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file platform_first.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Platform headers that MUST be included before cbproto headers +/// +/// IMPORTANT: This header must be included FIRST in any source file that uses both +/// Windows headers and cbproto headers. +/// +/// Reason: cbproto uses #pragma pack(1) for network structures. Windows SDK headers +/// (specifically winnt.h) have a static_assert that fails if packing is not at the +/// default setting when they are included. This header ensures Windows headers are +/// processed before any pack directives are active. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBSHM_PLATFORM_FIRST_H +#define CBSHM_PLATFORM_FIRST_H + +#ifdef _WIN32 + // Windows headers must be included before cbproto headers due to packing requirements + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif + +#endif // CBSHM_PLATFORM_FIRST_H diff --git a/src/cbshm/src/shmem_session.cpp b/src/cbshm/src/shmem_session.cpp new file mode 100644 index 00000000..b2815ca7 --- /dev/null +++ b/src/cbshm/src/shmem_session.cpp @@ -0,0 +1,1990 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file shmem_session.cpp +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Shared memory session implementation +/// +/// Implements cross-platform shared memory management with dual layout support: +/// - CENTRAL layout: Central-compatible (4 instruments, 848 channels, ~1.2 GB) +/// - NATIVE layout: Per-device (1 instrument, 284 channels, ~265 MB) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#ifndef _WIN32 + #include + #include + #include + #include + #include + #include + #include + #include +#endif + +#include +#include +#include +#include +#include + +namespace cbshm { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Platform-specific implementation details (Pimpl idiom) +/// +struct ShmemSession::Impl { + Mode mode; + ShmemLayout layout; + std::string cfg_name; // Config buffer name (e.g., "cbCFGbuffer") + std::string rec_name; // Receive buffer name (e.g., "cbRECbuffer") + std::string xmt_name; // Transmit buffer name (e.g., "XmtGlobal") + std::string xmt_local_name; // Local transmit buffer name (e.g., "XmtLocal") + std::string status_name; // PC status buffer name (e.g., "cbSTATUSbuffer") + std::string spk_name; // Spike cache buffer name (e.g., "cbSPKbuffer") + std::string signal_event_name; // Signal event name (e.g., "cbSIGNALevent") + bool is_open; + + // Platform-specific handles (separate segments for each buffer) +#ifdef _WIN32 + HANDLE cfg_file_mapping; + HANDLE rec_file_mapping; + HANDLE xmt_file_mapping; + HANDLE xmt_local_file_mapping; + HANDLE status_file_mapping; + HANDLE spk_file_mapping; + HANDLE signal_event; // Named Event for data availability signaling +#else + int cfg_shm_fd; + int rec_shm_fd; + int xmt_shm_fd; + int xmt_local_shm_fd; + int status_shm_fd; + int spk_shm_fd; + sem_t* signal_event; // Named semaphore for data availability signaling +#endif + + // Pointers to shared memory buffers (void* to support dual layout) + void* cfg_buffer_raw; + void* rec_buffer_raw; + void* xmt_buffer_raw; + void* xmt_local_buffer_raw; + void* status_buffer_raw; + void* spike_buffer_raw; + + // Buffer sizes (set during open(), used for munmap and bounds checks) + size_t cfg_buffer_size; + size_t rec_buffer_size; + size_t xmt_buffer_size; + size_t xmt_local_buffer_size; + size_t status_buffer_size; + size_t spike_buffer_size; + + // Runtime receive buffer length (replaces hardcoded CENTRAL_cbRECBUFFLEN) + uint32_t rec_buffer_len; + + // Receive buffer read tracking (for CLIENT mode reading) + uint32_t rec_tailindex; // Our read position in receive buffer + uint32_t rec_tailwrap; // Our wrap counter + + // Instrument filter for CENTRAL_COMPAT mode (-1 = no filter) + int32_t instrument_filter; + + // Detected protocol version for CENTRAL_COMPAT mode + cbproto_protocol_version_t compat_protocol; + + // Typed accessors for config buffer + CentralConfigBuffer* centralCfg() { return static_cast(cfg_buffer_raw); } + const CentralConfigBuffer* centralCfg() const { return static_cast(cfg_buffer_raw); } + NativeConfigBuffer* nativeCfg() { return static_cast(cfg_buffer_raw); } + const NativeConfigBuffer* nativeCfg() const { return static_cast(cfg_buffer_raw); } + CentralLegacyCFGBUFF* legacyCfg() { return static_cast(cfg_buffer_raw); } + const CentralLegacyCFGBUFF* legacyCfg() const { return static_cast(cfg_buffer_raw); } + + // Generic receive buffer header access (header fields are at identical offsets in both layouts) + uint32_t& recReceived() { + return *static_cast(rec_buffer_raw); + } + PROCTIME& recLasttime() { + // lasttime is at offset sizeof(uint32_t) in both CentralReceiveBuffer and NativeReceiveBuffer + return *reinterpret_cast(static_cast(rec_buffer_raw) + sizeof(uint32_t)); + } + uint32_t& recHeadwrap() { + return *reinterpret_cast(static_cast(rec_buffer_raw) + sizeof(uint32_t) + sizeof(PROCTIME)); + } + uint32_t& recHeadindex() { + return *reinterpret_cast(static_cast(rec_buffer_raw) + sizeof(uint32_t) + sizeof(PROCTIME) + sizeof(uint32_t)); + } + uint32_t* recBuffer() { + return reinterpret_cast(static_cast(rec_buffer_raw) + sizeof(uint32_t) + sizeof(PROCTIME) + sizeof(uint32_t) + sizeof(uint32_t)); + } + + // Transmit buffer accessors (header fields are identical between Central and Native) + struct XmtHeader { + uint32_t transmitted; + uint32_t headindex; + uint32_t tailindex; + uint32_t last_valid_index; + uint32_t bufferlen; + }; + XmtHeader* xmtGlobal() { return static_cast(xmt_buffer_raw); } + uint32_t* xmtGlobalBuffer() { return reinterpret_cast(static_cast(xmt_buffer_raw) + sizeof(XmtHeader)); } + XmtHeader* xmtLocal() { return static_cast(xmt_local_buffer_raw); } + uint32_t* xmtLocalBuffer() { return reinterpret_cast(static_cast(xmt_local_buffer_raw) + sizeof(XmtHeader)); } + + Impl() + : mode(Mode::STANDALONE) + , layout(ShmemLayout::CENTRAL) + , is_open(false) +#ifdef _WIN32 + , cfg_file_mapping(nullptr) + , rec_file_mapping(nullptr) + , xmt_file_mapping(nullptr) + , xmt_local_file_mapping(nullptr) + , status_file_mapping(nullptr) + , spk_file_mapping(nullptr) + , signal_event(nullptr) +#else + , cfg_shm_fd(-1) + , rec_shm_fd(-1) + , xmt_shm_fd(-1) + , xmt_local_shm_fd(-1) + , status_shm_fd(-1) + , spk_shm_fd(-1) + , signal_event(SEM_FAILED) +#endif + , cfg_buffer_raw(nullptr) + , rec_buffer_raw(nullptr) + , xmt_buffer_raw(nullptr) + , xmt_local_buffer_raw(nullptr) + , status_buffer_raw(nullptr) + , spike_buffer_raw(nullptr) + , cfg_buffer_size(0) + , rec_buffer_size(0) + , xmt_buffer_size(0) + , xmt_local_buffer_size(0) + , status_buffer_size(0) + , spike_buffer_size(0) + , rec_buffer_len(0) + , rec_tailindex(0) + , rec_tailwrap(0) + , instrument_filter(-1) + , compat_protocol(CBPROTO_PROTOCOL_CURRENT) + {} + + ~Impl() { + close(); + } + + /// @brief Compute buffer sizes based on layout + void computeBufferSizes() { + if (layout == ShmemLayout::NATIVE) { + cfg_buffer_size = sizeof(NativeConfigBuffer); + rec_buffer_size = sizeof(NativeReceiveBuffer); + xmt_buffer_size = sizeof(NativeTransmitBuffer); + xmt_local_buffer_size = sizeof(NativeTransmitBufferLocal); + status_buffer_size = sizeof(NativePCStatus); + spike_buffer_size = sizeof(NativeSpikeBuffer); + rec_buffer_len = NATIVE_cbRECBUFFLEN; + } else if (layout == ShmemLayout::CENTRAL_COMPAT) { + cfg_buffer_size = sizeof(CentralLegacyCFGBUFF); + // All other buffers use Central sizes (receive, xmt, spike, status are compatible) + rec_buffer_size = sizeof(CentralReceiveBuffer); + xmt_buffer_size = sizeof(CentralTransmitBuffer); + xmt_local_buffer_size = sizeof(CentralTransmitBufferLocal); + status_buffer_size = sizeof(CentralPCStatus); + spike_buffer_size = sizeof(CentralSpikeBuffer); + rec_buffer_len = CENTRAL_cbRECBUFFLEN; + } else { + cfg_buffer_size = sizeof(CentralConfigBuffer); + rec_buffer_size = sizeof(CentralReceiveBuffer); + xmt_buffer_size = sizeof(CentralTransmitBuffer); + xmt_local_buffer_size = sizeof(CentralTransmitBufferLocal); + status_buffer_size = sizeof(CentralPCStatus); + spike_buffer_size = sizeof(CentralSpikeBuffer); + rec_buffer_len = CENTRAL_cbRECBUFFLEN; + } + } + + void close() { + if (!is_open) return; + + // Unmap shared memory +#ifdef _WIN32 + if (cfg_buffer_raw) UnmapViewOfFile(cfg_buffer_raw); + if (rec_buffer_raw) UnmapViewOfFile(rec_buffer_raw); + if (xmt_buffer_raw) UnmapViewOfFile(xmt_buffer_raw); + if (xmt_local_buffer_raw) UnmapViewOfFile(xmt_local_buffer_raw); + if (status_buffer_raw) UnmapViewOfFile(status_buffer_raw); + if (spike_buffer_raw) UnmapViewOfFile(spike_buffer_raw); + if (cfg_file_mapping) CloseHandle(cfg_file_mapping); + if (rec_file_mapping) CloseHandle(rec_file_mapping); + if (xmt_file_mapping) CloseHandle(xmt_file_mapping); + if (xmt_local_file_mapping) CloseHandle(xmt_local_file_mapping); + if (status_file_mapping) CloseHandle(status_file_mapping); + if (spk_file_mapping) CloseHandle(spk_file_mapping); + if (signal_event) CloseHandle(signal_event); + cfg_file_mapping = nullptr; + rec_file_mapping = nullptr; + xmt_file_mapping = nullptr; + xmt_local_file_mapping = nullptr; + status_file_mapping = nullptr; + spk_file_mapping = nullptr; + signal_event = nullptr; +#else + // POSIX requires shared memory names to start with "/" + std::string posix_cfg_name = (cfg_name[0] == '/') ? cfg_name : ("/" + cfg_name); + std::string posix_rec_name = (rec_name[0] == '/') ? rec_name : ("/" + rec_name); + std::string posix_xmt_name = (xmt_name[0] == '/') ? xmt_name : ("/" + xmt_name); + std::string posix_xmt_local_name = (xmt_local_name[0] == '/') ? xmt_local_name : ("/" + xmt_local_name); + std::string posix_status_name = (status_name[0] == '/') ? status_name : ("/" + status_name); + std::string posix_spk_name = (spk_name[0] == '/') ? spk_name : ("/" + spk_name); + std::string posix_signal_name = (signal_event_name[0] == '/') ? signal_event_name : ("/" + signal_event_name); + + if (cfg_buffer_raw) munmap(cfg_buffer_raw, cfg_buffer_size); + if (rec_buffer_raw) munmap(rec_buffer_raw, rec_buffer_size); + if (xmt_buffer_raw) munmap(xmt_buffer_raw, xmt_buffer_size); + if (xmt_local_buffer_raw) munmap(xmt_local_buffer_raw, xmt_local_buffer_size); + if (status_buffer_raw) munmap(status_buffer_raw, status_buffer_size); + if (spike_buffer_raw) munmap(spike_buffer_raw, spike_buffer_size); + + if (cfg_shm_fd >= 0) { + ::close(cfg_shm_fd); + if (mode == Mode::STANDALONE) shm_unlink(posix_cfg_name.c_str()); + } + if (rec_shm_fd >= 0) { + ::close(rec_shm_fd); + if (mode == Mode::STANDALONE) shm_unlink(posix_rec_name.c_str()); + } + if (xmt_shm_fd >= 0) { + ::close(xmt_shm_fd); + if (mode == Mode::STANDALONE) shm_unlink(posix_xmt_name.c_str()); + } + if (xmt_local_shm_fd >= 0) { + ::close(xmt_local_shm_fd); + if (mode == Mode::STANDALONE) shm_unlink(posix_xmt_local_name.c_str()); + } + if (status_shm_fd >= 0) { + ::close(status_shm_fd); + if (mode == Mode::STANDALONE) shm_unlink(posix_status_name.c_str()); + } + if (spk_shm_fd >= 0) { + ::close(spk_shm_fd); + if (mode == Mode::STANDALONE) shm_unlink(posix_spk_name.c_str()); + } + if (signal_event != SEM_FAILED) { + sem_close(signal_event); + if (mode == Mode::STANDALONE) sem_unlink(posix_signal_name.c_str()); + } + + cfg_shm_fd = -1; + rec_shm_fd = -1; + xmt_shm_fd = -1; + xmt_local_shm_fd = -1; + status_shm_fd = -1; + spk_shm_fd = -1; + signal_event = SEM_FAILED; +#endif + + cfg_buffer_raw = nullptr; + rec_buffer_raw = nullptr; + xmt_buffer_raw = nullptr; + xmt_local_buffer_raw = nullptr; + status_buffer_raw = nullptr; + spike_buffer_raw = nullptr; + is_open = false; + } + + /// @brief Write a packet to the receive buffer ring + Result writeToReceiveBuffer(const cbPKT_GENERIC& pkt) { + if (!rec_buffer_raw) { + return Result::error("Receive buffer not initialized"); + } + + uint32_t pkt_size_words = cbPKT_HEADER_32SIZE + pkt.cbpkt_header.dlen; + + if (pkt_size_words > rec_buffer_len) { + return Result::error("Packet too large for receive buffer"); + } + + uint32_t head = recHeadindex(); + + if (head + pkt_size_words > rec_buffer_len) { + head = 0; + recHeadwrap()++; + } + + uint32_t* buf = recBuffer(); + const uint32_t* pkt_data = reinterpret_cast(&pkt); + std::memcpy(&buf[head], pkt_data, pkt_size_words * sizeof(uint32_t)); + + recHeadindex() = head + pkt_size_words; + recReceived()++; + recLasttime() = pkt.cbpkt_header.time; + + return Result::ok(); + } + +#ifndef _WIN32 + /// @brief POSIX helper: open/create one shared memory segment and mmap it + /// @return Result with mapped pointer on success + Result openPosixSegment(const std::string& name, size_t size, int& fd_out, + int flags, mode_t perms, int prot) { + std::string posix_name = (name[0] == '/') ? name : ("/" + name); + + if (mode == Mode::STANDALONE) { + shm_unlink(posix_name.c_str()); // Clean up any previous + } + + fd_out = shm_open(posix_name.c_str(), flags, perms); + if (fd_out < 0) { + return Result::error("Failed to open shared memory '" + name + "': " + strerror(errno)); + } + + if (mode == Mode::STANDALONE) { + if (ftruncate(fd_out, size) < 0) { + std::string err = "Failed to set size for '" + name + "': " + strerror(errno); + ::close(fd_out); + fd_out = -1; + return Result::error(err); + } + } + + void* ptr = mmap(nullptr, size, prot, MAP_SHARED, fd_out, 0); + if (ptr == MAP_FAILED) { + ::close(fd_out); + fd_out = -1; + return Result::error("Failed to map shared memory '" + name + "'"); + } + + return Result::ok(ptr); + } +#endif + + Result open() { + if (is_open) { + return Result::error("Session already open"); + } + + // Compute buffer sizes based on layout + computeBufferSizes(); + +#ifdef _WIN32 + // Windows implementation + // Helper lambda to create/map a segment + // writable: when true, segment is opened read-write even in CLIENT mode + // (needed for transmit buffers so clients can enqueue packets) + auto createSegment = [&](const std::string& name, size_t size, HANDLE& mapping, void*& buffer, bool writable = false) -> Result { + DWORD size_high = static_cast((static_cast(size)) >> 32); + DWORD size_low = static_cast(size & 0xFFFFFFFF); + + bool need_write = (mode == Mode::STANDALONE) || writable; + DWORD seg_access = need_write ? PAGE_READWRITE : PAGE_READONLY; + DWORD seg_map = need_write ? FILE_MAP_ALL_ACCESS : FILE_MAP_READ; + + if (mode == Mode::STANDALONE) { + mapping = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, seg_access, size_high, size_low, name.c_str()); + } else { + mapping = OpenFileMappingA(seg_map, FALSE, name.c_str()); + } + if (!mapping) { + DWORD err = GetLastError(); + return Result::error("Failed to create/open file mapping '" + name + + "' (size=" + std::to_string(size) + ", err=" + std::to_string(err) + ")"); + } + buffer = MapViewOfFile(mapping, seg_map, 0, 0, (mode == Mode::STANDALONE) ? size : 0); + if (!buffer) { + DWORD err = GetLastError(); + CloseHandle(mapping); + mapping = nullptr; + return Result::error("Failed to map view of file '" + name + + "' (size=" + std::to_string(size) + ", err=" + std::to_string(err) + ")"); + } + return Result::ok(); + }; + + auto r = createSegment(cfg_name, cfg_buffer_size, cfg_file_mapping, cfg_buffer_raw); + if (r.isError()) { close(); return r; } + + r = createSegment(rec_name, rec_buffer_size, rec_file_mapping, rec_buffer_raw); + if (r.isError()) { close(); return r; } + + // Transmit buffers need write access in CLIENT mode too (clients enqueue packets) + r = createSegment(xmt_name, xmt_buffer_size, xmt_file_mapping, xmt_buffer_raw, true); + if (r.isError()) { close(); return r; } + + r = createSegment(xmt_local_name, xmt_local_buffer_size, xmt_local_file_mapping, xmt_local_buffer_raw, true); + if (r.isError()) { close(); return r; } + + r = createSegment(status_name, status_buffer_size, status_file_mapping, status_buffer_raw); + if (r.isError()) { close(); return r; } + + r = createSegment(spk_name, spike_buffer_size, spk_file_mapping, spike_buffer_raw); + if (r.isError()) { close(); return r; } + + // Create/open signal event + if (mode == Mode::STANDALONE) { + signal_event = CreateEventA(nullptr, TRUE, FALSE, signal_event_name.c_str()); + } else { + signal_event = OpenEventA(SYNCHRONIZE, FALSE, signal_event_name.c_str()); + } + if (!signal_event) { + close(); + return Result::error("Failed to create/open signal event"); + } + +#else + // POSIX (macOS/Linux) implementation + int flags = (mode == Mode::STANDALONE) ? (O_CREAT | O_RDWR) : O_RDONLY; + mode_t perms = (mode == Mode::STANDALONE) ? 0644 : 0; + int prot = (mode == Mode::STANDALONE) ? (PROT_READ | PROT_WRITE) : PROT_READ; + + // Transmit buffers need write access in CLIENT mode too (clients enqueue packets) + int xmt_flags = (mode == Mode::STANDALONE) ? (O_CREAT | O_RDWR) : O_RDWR; + int xmt_prot = PROT_READ | PROT_WRITE; + + auto r1 = openPosixSegment(cfg_name, cfg_buffer_size, cfg_shm_fd, flags, perms, prot); + if (r1.isError()) { close(); return Result::error(r1.error()); } + cfg_buffer_raw = r1.value(); + + auto r2 = openPosixSegment(rec_name, rec_buffer_size, rec_shm_fd, flags, perms, prot); + if (r2.isError()) { close(); return Result::error(r2.error()); } + rec_buffer_raw = r2.value(); + + auto r3 = openPosixSegment(xmt_name, xmt_buffer_size, xmt_shm_fd, xmt_flags, perms, xmt_prot); + if (r3.isError()) { close(); return Result::error(r3.error()); } + xmt_buffer_raw = r3.value(); + + auto r4 = openPosixSegment(xmt_local_name, xmt_local_buffer_size, xmt_local_shm_fd, xmt_flags, perms, xmt_prot); + if (r4.isError()) { close(); return Result::error(r4.error()); } + xmt_local_buffer_raw = r4.value(); + + auto r5 = openPosixSegment(status_name, status_buffer_size, status_shm_fd, flags, perms, prot); + if (r5.isError()) { close(); return Result::error(r5.error()); } + status_buffer_raw = r5.value(); + + auto r6 = openPosixSegment(spk_name, spike_buffer_size, spk_shm_fd, flags, perms, prot); + if (r6.isError()) { close(); return Result::error(r6.error()); } + spike_buffer_raw = r6.value(); + + // Create/open signal event (named semaphore) + std::string posix_signal_name = (signal_event_name[0] == '/') ? signal_event_name : ("/" + signal_event_name); + if (mode == Mode::STANDALONE) { + sem_unlink(posix_signal_name.c_str()); + signal_event = sem_open(posix_signal_name.c_str(), O_CREAT | O_EXCL, 0666, 0); + if (signal_event == SEM_FAILED) { + signal_event = sem_open(posix_signal_name.c_str(), O_CREAT, 0666, 0); + } + } else { + signal_event = sem_open(posix_signal_name.c_str(), 0); + } + + if (signal_event == SEM_FAILED) { + close(); + return Result::error("Failed to create/open signal semaphore: " + std::string(strerror(errno))); + } +#endif + + // Initialize buffers in standalone mode + if (mode == Mode::STANDALONE) { + initBuffers(); + } + + is_open = true; + + // In CLIENT mode, sync our read position to the current head so we only + // read NEW packets, not stale data that was already in the ring buffer. + if (mode == Mode::CLIENT) { + rec_tailindex = recHeadindex(); + rec_tailwrap = recHeadwrap(); + } + + // Detect protocol version for CENTRAL_COMPAT mode + detectCompatProtocol(); + + return Result::ok(); + } + + /// @brief Initialize buffers for STANDALONE mode + void initBuffers() { + if (layout == ShmemLayout::NATIVE) { + initNativeBuffers(); + } else if (layout == ShmemLayout::CENTRAL_COMPAT) { + initLegacyBuffers(); + } else { + initCentralBuffers(); + } + } + + void initCentralBuffers() { + auto* cfg = centralCfg(); + std::memset(cfg, 0, cfg_buffer_size); + cfg->version = cbVERSION_MAJOR * 100 + cbVERSION_MINOR; + for (int i = 0; i < CENTRAL_cbMAXPROCS; ++i) { + cfg->instrument_status[i] = static_cast(InstrumentStatus::INACTIVE); + } + + // Initialize receive buffer + std::memset(rec_buffer_raw, 0, rec_buffer_size); + + // Initialize transmit buffers + auto* xmt = static_cast(xmt_buffer_raw); + std::memset(xmt, 0, xmt_buffer_size); + xmt->last_valid_index = CENTRAL_cbXMT_GLOBAL_BUFFLEN - 1; + xmt->bufferlen = CENTRAL_cbXMT_GLOBAL_BUFFLEN; + + auto* xmt_local = static_cast(xmt_local_buffer_raw); + std::memset(xmt_local, 0, xmt_local_buffer_size); + xmt_local->last_valid_index = CENTRAL_cbXMT_LOCAL_BUFFLEN - 1; + xmt_local->bufferlen = CENTRAL_cbXMT_LOCAL_BUFFLEN; + + // Initialize status buffer + auto* status = static_cast(status_buffer_raw); + std::memset(status, 0, status_buffer_size); + status->m_nNumFEChans = CENTRAL_cbNUM_FE_CHANS; + status->m_nNumAnainChans = CENTRAL_cbNUM_ANAIN_CHANS; + status->m_nNumAnalogChans = CENTRAL_cbNUM_ANALOG_CHANS; + status->m_nNumAoutChans = CENTRAL_cbNUM_ANAOUT_CHANS; + status->m_nNumAudioChans = CENTRAL_cbNUM_AUDOUT_CHANS; + status->m_nNumAnalogoutChans = CENTRAL_cbNUM_ANALOGOUT_CHANS; + status->m_nNumDiginChans = CENTRAL_cbNUM_DIGIN_CHANS; + status->m_nNumSerialChans = CENTRAL_cbNUM_SERIAL_CHANS; + status->m_nNumDigoutChans = CENTRAL_cbNUM_DIGOUT_CHANS; + status->m_nNumTotalChans = CENTRAL_cbMAXCHANS; + for (int i = 0; i < CENTRAL_cbMAXPROCS; ++i) { + status->m_nNspStatus[i] = NSPStatus::NSP_INIT; + } + + // Initialize spike cache buffer + auto* spike = static_cast(spike_buffer_raw); + std::memset(spike, 0, spike_buffer_size); + spike->chidmax = CENTRAL_cbNUM_ANALOG_CHANS; + spike->linesize = sizeof(CentralSpikeCache); + for (uint32_t ch = 0; ch < CENTRAL_cbPKT_SPKCACHELINECNT; ++ch) { + spike->cache[ch].chid = ch; + spike->cache[ch].pktcnt = CENTRAL_cbPKT_SPKCACHEPKTCNT; + spike->cache[ch].pktsize = sizeof(cbPKT_SPK); + } + } + + void initLegacyBuffers() { + auto* cfg = legacyCfg(); + std::memset(cfg, 0, cfg_buffer_size); + cfg->version = cbVERSION_MAJOR * 100 + cbVERSION_MINOR; + + // Set procinfo version so detectCompatProtocol() identifies current format. + // In STANDALONE mode, CereLink owns the memory and writes current-format packets. + // MAKELONG(minor, major) = (major << 16) | minor + for (int i = 0; i < CENTRAL_cbMAXPROCS; ++i) { + cfg->procinfo[i].version = (cbVERSION_MAJOR << 16) | cbVERSION_MINOR; + } + + // Initialize receive buffer + std::memset(rec_buffer_raw, 0, rec_buffer_size); + + // Initialize transmit buffers (same struct as Central) + auto* xmt = static_cast(xmt_buffer_raw); + std::memset(xmt, 0, xmt_buffer_size); + xmt->last_valid_index = CENTRAL_cbXMT_GLOBAL_BUFFLEN - 1; + xmt->bufferlen = CENTRAL_cbXMT_GLOBAL_BUFFLEN; + + auto* xmt_local = static_cast(xmt_local_buffer_raw); + std::memset(xmt_local, 0, xmt_local_buffer_size); + xmt_local->last_valid_index = CENTRAL_cbXMT_LOCAL_BUFFLEN - 1; + xmt_local->bufferlen = CENTRAL_cbXMT_LOCAL_BUFFLEN; + + // Initialize status buffer (same struct as Central) + auto* status = static_cast(status_buffer_raw); + std::memset(status, 0, status_buffer_size); + status->m_nNumFEChans = CENTRAL_cbNUM_FE_CHANS; + status->m_nNumAnainChans = CENTRAL_cbNUM_ANAIN_CHANS; + status->m_nNumAnalogChans = CENTRAL_cbNUM_ANALOG_CHANS; + status->m_nNumAoutChans = CENTRAL_cbNUM_ANAOUT_CHANS; + status->m_nNumAudioChans = CENTRAL_cbNUM_AUDOUT_CHANS; + status->m_nNumAnalogoutChans = CENTRAL_cbNUM_ANALOGOUT_CHANS; + status->m_nNumDiginChans = CENTRAL_cbNUM_DIGIN_CHANS; + status->m_nNumSerialChans = CENTRAL_cbNUM_SERIAL_CHANS; + status->m_nNumDigoutChans = CENTRAL_cbNUM_DIGOUT_CHANS; + status->m_nNumTotalChans = CENTRAL_cbMAXCHANS; + for (int i = 0; i < CENTRAL_cbMAXPROCS; ++i) { + status->m_nNspStatus[i] = NSPStatus::NSP_INIT; + } + + // Initialize spike cache buffer (same struct as Central) + auto* spike = static_cast(spike_buffer_raw); + std::memset(spike, 0, spike_buffer_size); + spike->chidmax = CENTRAL_cbNUM_ANALOG_CHANS; + spike->linesize = sizeof(CentralSpikeCache); + for (uint32_t ch = 0; ch < CENTRAL_cbPKT_SPKCACHELINECNT; ++ch) { + spike->cache[ch].chid = ch; + spike->cache[ch].pktcnt = CENTRAL_cbPKT_SPKCACHEPKTCNT; + spike->cache[ch].pktsize = sizeof(cbPKT_SPK); + } + } + + void initNativeBuffers() { + auto* cfg = nativeCfg(); + std::memset(cfg, 0, cfg_buffer_size); + cfg->version = cbVERSION_MAJOR * 100 + cbVERSION_MINOR; + cfg->instrument_status = static_cast(InstrumentStatus::INACTIVE); +#ifdef _WIN32 + cfg->owner_pid = GetCurrentProcessId(); +#else + cfg->owner_pid = static_cast(getpid()); +#endif + + // Initialize receive buffer + std::memset(rec_buffer_raw, 0, rec_buffer_size); + + // Initialize transmit buffers + auto* xmt = static_cast(xmt_buffer_raw); + std::memset(xmt, 0, xmt_buffer_size); + xmt->last_valid_index = NATIVE_cbXMT_GLOBAL_BUFFLEN - 1; + xmt->bufferlen = NATIVE_cbXMT_GLOBAL_BUFFLEN; + + auto* xmt_local = static_cast(xmt_local_buffer_raw); + std::memset(xmt_local, 0, xmt_local_buffer_size); + xmt_local->last_valid_index = NATIVE_cbXMT_LOCAL_BUFFLEN - 1; + xmt_local->bufferlen = NATIVE_cbXMT_LOCAL_BUFFLEN; + + // Initialize status buffer + auto* status = static_cast(status_buffer_raw); + std::memset(status, 0, status_buffer_size); + status->m_nNumFEChans = NATIVE_NUM_FE_CHANS; + status->m_nNumAnainChans = cbNUM_ANAIN_CHANS; + status->m_nNumAnalogChans = NATIVE_NUM_ANALOG_CHANS; + status->m_nNumAoutChans = cbNUM_ANAOUT_CHANS; + status->m_nNumAudioChans = cbNUM_AUDOUT_CHANS; + status->m_nNumAnalogoutChans = cbNUM_ANALOGOUT_CHANS; + status->m_nNumDiginChans = cbNUM_DIGIN_CHANS; + status->m_nNumSerialChans = cbNUM_SERIAL_CHANS; + status->m_nNumDigoutChans = cbNUM_DIGOUT_CHANS; + status->m_nNumTotalChans = NATIVE_MAXCHANS; + status->m_nNspStatus = NSPStatus::NSP_INIT; + + // Initialize spike cache buffer + auto* spike = static_cast(spike_buffer_raw); + std::memset(spike, 0, spike_buffer_size); + spike->chidmax = NATIVE_NUM_ANALOG_CHANS; + spike->linesize = sizeof(NativeSpikeCache); + for (uint32_t ch = 0; ch < NATIVE_cbPKT_SPKCACHELINECNT; ++ch) { + spike->cache[ch].chid = ch; + spike->cache[ch].pktcnt = NATIVE_cbPKT_SPKCACHEPKTCNT; + spike->cache[ch].pktsize = sizeof(cbPKT_SPK); + } + } + + /// @brief Detect protocol version from config buffer (CENTRAL_COMPAT only) + void detectCompatProtocol() { + if (layout != ShmemLayout::CENTRAL_COMPAT) { + compat_protocol = CBPROTO_PROTOCOL_CURRENT; + return; + } + + auto* cfg = legacyCfg(); + if (!cfg) { + compat_protocol = CBPROTO_PROTOCOL_CURRENT; + return; + } + + // procinfo[0].version = MAKELONG(minor, major) = (major << 16) | minor + uint32_t ver = cfg->procinfo[0].version; + uint16_t major = (ver >> 16) & 0xFFFF; + uint16_t minor = ver & 0xFFFF; + + if (major < 4) { + compat_protocol = CBPROTO_PROTOCOL_311; + } else if (major == 4 && minor == 0) { + compat_protocol = CBPROTO_PROTOCOL_400; + } else if (major == 4 && minor == 1) { + compat_protocol = CBPROTO_PROTOCOL_410; + } else { + compat_protocol = CBPROTO_PROTOCOL_CURRENT; + } + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// ShmemSession public API implementation + +ShmemSession::ShmemSession() + : m_impl(std::make_unique()) +{} + +ShmemSession::~ShmemSession() = default; + +ShmemSession::ShmemSession(ShmemSession&& other) noexcept = default; +ShmemSession& ShmemSession::operator=(ShmemSession&& other) noexcept = default; + +Result ShmemSession::create(const std::string& cfg_name, const std::string& rec_name, + const std::string& xmt_name, const std::string& xmt_local_name, + const std::string& status_name, const std::string& spk_name, + const std::string& signal_event_name, Mode mode, + ShmemLayout layout) { + ShmemSession session; + session.m_impl->cfg_name = cfg_name; + session.m_impl->rec_name = rec_name; + session.m_impl->xmt_name = xmt_name; + session.m_impl->xmt_local_name = xmt_local_name; + session.m_impl->status_name = status_name; + session.m_impl->spk_name = spk_name; + session.m_impl->signal_event_name = signal_event_name; + session.m_impl->mode = mode; + session.m_impl->layout = layout; + + auto result = session.m_impl->open(); + if (result.isError()) { + return Result::error(result.error()); + } + + return Result::ok(std::move(session)); +} + +bool ShmemSession::isOpen() const { + return m_impl && m_impl->is_open; +} + +Mode ShmemSession::getMode() const { + return m_impl->mode; +} + +ShmemLayout ShmemSession::getLayout() const { + return m_impl->layout; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Instrument Status Management + +Result ShmemSession::isInstrumentActive(cbproto::InstrumentId id) const { + if (!isOpen()) { + return Result::error("Session not open"); + } + if (!id.isValid()) { + return Result::error("Invalid instrument ID"); + } + + uint8_t idx = id.toIndex(); + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (idx != 0) { + return Result::error("Native mode: single instrument only (index 0)"); + } + bool active = (m_impl->nativeCfg()->instrument_status == static_cast(InstrumentStatus::ACTIVE)); + return Result::ok(active); + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + // CentralLegacyCFGBUFF has no instrument_status field; + // if the shared memory exists, instruments are as Central configured them + return Result::ok(true); + } else { + bool active = (m_impl->centralCfg()->instrument_status[idx] == static_cast(InstrumentStatus::ACTIVE)); + return Result::ok(active); + } +} + +Result ShmemSession::setInstrumentActive(cbproto::InstrumentId id, bool active) { + if (!isOpen()) { + return Result::error("Session not open"); + } + if (!id.isValid()) { + return Result::error("Invalid instrument ID"); + } + + uint8_t idx = id.toIndex(); + uint32_t val = active ? static_cast(InstrumentStatus::ACTIVE) : static_cast(InstrumentStatus::INACTIVE); + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (idx != 0) { + return Result::error("Native mode: single instrument only (index 0)"); + } + m_impl->nativeCfg()->instrument_status = val; + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + return Result::error("CENTRAL_COMPAT mode: instrument status is read-only (no instrument_status field in Central's layout)"); + } else { + m_impl->centralCfg()->instrument_status[idx] = val; + } + + return Result::ok(); +} + +Result ShmemSession::getFirstActiveInstrument() const { + if (!isOpen()) { + return Result::error("Session not open"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (m_impl->nativeCfg()->instrument_status == static_cast(InstrumentStatus::ACTIVE)) { + return Result::ok(cbproto::InstrumentId::fromIndex(0)); + } + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + // No instrument_status in legacy layout; return first instrument (always "active") + return Result::ok(cbproto::InstrumentId::fromIndex(0)); + } else { + for (uint8_t i = 0; i < CENTRAL_cbMAXPROCS; ++i) { + if (m_impl->centralCfg()->instrument_status[i] == static_cast(InstrumentStatus::ACTIVE)) { + return Result::ok(cbproto::InstrumentId::fromIndex(i)); + } + } + } + + return Result::error("No active instruments"); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Read Operations + +Result ShmemSession::getProcInfo(cbproto::InstrumentId id) const { + if (!isOpen()) { + return Result::error("Session not open"); + } + if (!id.isValid()) { + return Result::error("Invalid instrument ID"); + } + + uint8_t idx = id.toIndex(); + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (idx != 0) { + return Result::error("Native mode: single instrument only"); + } + return Result::ok(m_impl->nativeCfg()->procinfo); + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + if (idx >= CENTRAL_cbMAXPROCS) return Result::error("instrument index out of range"); + return Result::ok(m_impl->legacyCfg()->procinfo[idx]); + } else { + return Result::ok(m_impl->centralCfg()->procinfo[idx]); + } +} + +Result ShmemSession::getBankInfo(cbproto::InstrumentId id, uint32_t bank) const { + if (!isOpen()) { + return Result::error("Session not open"); + } + if (!id.isValid()) { + return Result::error("Invalid instrument ID"); + } + + uint8_t idx = id.toIndex(); + uint32_t max_banks = (m_impl->layout == ShmemLayout::NATIVE) ? NATIVE_MAXBANKS : CENTRAL_cbMAXBANKS; + + if (bank == 0 || bank > max_banks) { + return Result::error("Bank number out of range"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (idx != 0) { + return Result::error("Native mode: single instrument only"); + } + return Result::ok(m_impl->nativeCfg()->bankinfo[bank - 1]); + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + if (idx >= CENTRAL_cbMAXPROCS) return Result::error("instrument index out of range"); + return Result::ok(m_impl->legacyCfg()->bankinfo[idx][bank - 1]); + } else { + return Result::ok(m_impl->centralCfg()->bankinfo[idx][bank - 1]); + } +} + +Result ShmemSession::getFilterInfo(cbproto::InstrumentId id, uint32_t filter) const { + if (!isOpen()) { + return Result::error("Session not open"); + } + if (!id.isValid()) { + return Result::error("Invalid instrument ID"); + } + + uint8_t idx = id.toIndex(); + uint32_t max_filts = (m_impl->layout == ShmemLayout::NATIVE) ? NATIVE_MAXFILTS : CENTRAL_cbMAXFILTS; + + if (filter == 0 || filter > max_filts) { + return Result::error("Filter number out of range"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (idx != 0) { + return Result::error("Native mode: single instrument only"); + } + return Result::ok(m_impl->nativeCfg()->filtinfo[filter - 1]); + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + if (idx >= CENTRAL_cbMAXPROCS) return Result::error("instrument index out of range"); + return Result::ok(m_impl->legacyCfg()->filtinfo[idx][filter - 1]); + } else { + return Result::ok(m_impl->centralCfg()->filtinfo[idx][filter - 1]); + } +} + +Result ShmemSession::getChanInfo(uint32_t channel) const { + if (!isOpen()) { + return Result::error("Session not open"); + } + + uint32_t max_chans = (m_impl->layout == ShmemLayout::NATIVE) ? NATIVE_MAXCHANS : CENTRAL_cbMAXCHANS; + + if (channel >= max_chans) { + return Result::error("Channel index out of range"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + return Result::ok(m_impl->nativeCfg()->chaninfo[channel]); + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + return Result::ok(m_impl->legacyCfg()->chaninfo[channel]); + } else { + return Result::ok(m_impl->centralCfg()->chaninfo[channel]); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Write Operations + +Result ShmemSession::setProcInfo(cbproto::InstrumentId id, const cbPKT_PROCINFO& info) { + if (!isOpen()) { + return Result::error("Session not open"); + } + if (!id.isValid()) { + return Result::error("Invalid instrument ID"); + } + + uint8_t idx = id.toIndex(); + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (idx != 0) { + return Result::error("Native mode: single instrument only"); + } + m_impl->nativeCfg()->procinfo = info; + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + if (idx >= CENTRAL_cbMAXPROCS) return Result::error("instrument index out of range"); + m_impl->legacyCfg()->procinfo[idx] = info; + } else { + m_impl->centralCfg()->procinfo[idx] = info; + } + + return Result::ok(); +} + +Result ShmemSession::setBankInfo(cbproto::InstrumentId id, uint32_t bank, const cbPKT_BANKINFO& info) { + if (!isOpen()) { + return Result::error("Session not open"); + } + if (!id.isValid()) { + return Result::error("Invalid instrument ID"); + } + + uint8_t idx = id.toIndex(); + uint32_t max_banks = (m_impl->layout == ShmemLayout::NATIVE) ? NATIVE_MAXBANKS : CENTRAL_cbMAXBANKS; + + if (bank == 0 || bank > max_banks) { + return Result::error("Bank number out of range"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (idx != 0) { + return Result::error("Native mode: single instrument only"); + } + m_impl->nativeCfg()->bankinfo[bank - 1] = info; + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + if (idx >= CENTRAL_cbMAXPROCS) return Result::error("instrument index out of range"); + m_impl->legacyCfg()->bankinfo[idx][bank - 1] = info; + } else { + m_impl->centralCfg()->bankinfo[idx][bank - 1] = info; + } + + return Result::ok(); +} + +Result ShmemSession::setFilterInfo(cbproto::InstrumentId id, uint32_t filter, const cbPKT_FILTINFO& info) { + if (!isOpen()) { + return Result::error("Session not open"); + } + if (!id.isValid()) { + return Result::error("Invalid instrument ID"); + } + + uint8_t idx = id.toIndex(); + uint32_t max_filts = (m_impl->layout == ShmemLayout::NATIVE) ? NATIVE_MAXFILTS : CENTRAL_cbMAXFILTS; + + if (filter == 0 || filter > max_filts) { + return Result::error("Filter number out of range"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (idx != 0) { + return Result::error("Native mode: single instrument only"); + } + m_impl->nativeCfg()->filtinfo[filter - 1] = info; + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + if (idx >= CENTRAL_cbMAXPROCS) return Result::error("instrument index out of range"); + m_impl->legacyCfg()->filtinfo[idx][filter - 1] = info; + } else { + m_impl->centralCfg()->filtinfo[idx][filter - 1] = info; + } + + return Result::ok(); +} + +Result ShmemSession::setChanInfo(uint32_t channel, const cbPKT_CHANINFO& info) { + if (!isOpen()) { + return Result::error("Session not open"); + } + + uint32_t max_chans = (m_impl->layout == ShmemLayout::NATIVE) ? NATIVE_MAXCHANS : CENTRAL_cbMAXCHANS; + + if (channel >= max_chans) { + return Result::error("Channel index out of range"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + m_impl->nativeCfg()->chaninfo[channel] = info; + } else if (m_impl->layout == ShmemLayout::CENTRAL_COMPAT) { + m_impl->legacyCfg()->chaninfo[channel] = info; + } else { + m_impl->centralCfg()->chaninfo[channel] = info; + } + + return Result::ok(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Buffer Direct Access + +cbConfigBuffer* ShmemSession::getConfigBuffer() { + if (!isOpen() || m_impl->layout != ShmemLayout::CENTRAL) { + return nullptr; + } + return m_impl->centralCfg(); +} + +const cbConfigBuffer* ShmemSession::getConfigBuffer() const { + if (!isOpen() || m_impl->layout != ShmemLayout::CENTRAL) { + return nullptr; + } + return m_impl->centralCfg(); +} + +NativeConfigBuffer* ShmemSession::getNativeConfigBuffer() { + if (!isOpen() || m_impl->layout != ShmemLayout::NATIVE) { + return nullptr; + } + return m_impl->nativeCfg(); +} + +const NativeConfigBuffer* ShmemSession::getNativeConfigBuffer() const { + if (!isOpen() || m_impl->layout != ShmemLayout::NATIVE) { + return nullptr; + } + return m_impl->nativeCfg(); +} + +CentralLegacyCFGBUFF* ShmemSession::getLegacyConfigBuffer() { + if (!isOpen() || m_impl->layout != ShmemLayout::CENTRAL_COMPAT) { + return nullptr; + } + return m_impl->legacyCfg(); +} + +const CentralLegacyCFGBUFF* ShmemSession::getLegacyConfigBuffer() const { + if (!isOpen() || m_impl->layout != ShmemLayout::CENTRAL_COMPAT) { + return nullptr; + } + return m_impl->legacyCfg(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Packet Routing (THE KEY FIX!) + +Result ShmemSession::storePacket(const cbPKT_GENERIC& pkt) { + if (!isOpen()) { + return Result::error("Session not open"); + } + + // CRITICAL: Write ALL packets to receive buffer ring (Central's architecture) + auto rec_result = m_impl->writeToReceiveBuffer(pkt); + if (rec_result.isError()) { + // Log error but don't fail - config updates may still work + } + + // NOTE: Config parsing (PROCINFO, BANKINFO, etc.) is NOT done here. + // Config parsing belongs in the device session (DeviceSession::updateConfigFromBuffer), + // which owns the device_config struct. shmem is a transport layer only. + // Use setProcInfo()/setBankInfo()/etc. directly to update the config buffer. + + return Result::ok(); +} + +Result ShmemSession::storePackets(const cbPKT_GENERIC* pkts, size_t count) { + if (!isOpen()) { + return Result::error("Session not open"); + } + + for (size_t i = 0; i < count; ++i) { + auto result = storePacket(pkts[i]); + if (result.isError()) { + return result; + } + } + + return Result::ok(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Transmit Queue Operations +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result ShmemSession::enqueuePacket(const cbPKT_GENERIC& pkt) { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->xmt_buffer_raw) { + return Result::error("Transmit buffer not initialized"); + } + + // In CENTRAL_COMPAT mode with an older protocol, translate to the legacy format + const bool needs_translation = (m_impl->layout == ShmemLayout::CENTRAL_COMPAT && + m_impl->compat_protocol != CBPROTO_PROTOCOL_CURRENT); + + const uint8_t* write_data; + uint32_t write_size_bytes; + + uint8_t translated_buf[cbPKT_MAX_SIZE]; + + if (needs_translation) { + if (m_impl->compat_protocol == CBPROTO_PROTOCOL_311) { + // Translate header: current (16 bytes) → 3.11 (8 bytes) + auto& dest_hdr = *reinterpret_cast(translated_buf); + dest_hdr.time = static_cast(pkt.cbpkt_header.time * 30000ULL / 1000000000ULL); + dest_hdr.chid = pkt.cbpkt_header.chid; + dest_hdr.type = static_cast(pkt.cbpkt_header.type); + dest_hdr.dlen = static_cast(pkt.cbpkt_header.dlen); + // Translate payload + size_t dest_dlen = cbproto::PacketTranslator::translatePayload_current_to_311(pkt, translated_buf); + dest_hdr.dlen = static_cast(dest_dlen); + write_size_bytes = cbproto::HEADER_SIZE_311 + dest_dlen * 4; + } else if (m_impl->compat_protocol == CBPROTO_PROTOCOL_400) { + // Translate header: current (16 bytes) → 4.0 (16 bytes, different field layout) + auto& dest_hdr = *reinterpret_cast(translated_buf); + dest_hdr.time = pkt.cbpkt_header.time; + dest_hdr.chid = pkt.cbpkt_header.chid; + dest_hdr.type = static_cast(pkt.cbpkt_header.type); + dest_hdr.dlen = pkt.cbpkt_header.dlen; + dest_hdr.instrument = pkt.cbpkt_header.instrument; + dest_hdr.reserved = 0; + // Translate payload + size_t dest_dlen = cbproto::PacketTranslator::translatePayload_current_to_400(pkt, translated_buf); + dest_hdr.dlen = static_cast(dest_dlen); + write_size_bytes = cbproto::HEADER_SIZE_400 + dest_dlen * 4; + } else { + // 4.1 (CBPROTO_PROTOCOL_410): header identical, only some payloads differ + std::memcpy(translated_buf, &pkt, cbPKT_HEADER_SIZE + pkt.cbpkt_header.dlen * 4); + size_t dest_dlen = cbproto::PacketTranslator::translatePayload_current_to_410(pkt, translated_buf); + auto& dest_hdr = *reinterpret_cast(translated_buf); + dest_hdr.dlen = static_cast(dest_dlen); + write_size_bytes = cbPKT_HEADER_SIZE + dest_dlen * 4; + } + write_data = translated_buf; + } else { + write_data = reinterpret_cast(&pkt); + write_size_bytes = (cbPKT_HEADER_32SIZE + pkt.cbpkt_header.dlen) * sizeof(uint32_t); + } + + // Round up to dword-aligned size for ring buffer + uint32_t pkt_size_words = (write_size_bytes + 3) / 4; + + auto* xmt = m_impl->xmtGlobal(); + uint32_t* buf = m_impl->xmtGlobalBuffer(); + + uint32_t head = xmt->headindex; + uint32_t tail = xmt->tailindex; + uint32_t last_valid = xmt->last_valid_index; + + // Linear buffer with wrap-to-zero (matches Central's cbSendPacket): + // Packets are always written CONTIGUOUSLY. If the next packet doesn't fit + // before last_valid_index, headindex jumps to 0 (not per-word modulo). + uint32_t new_head = head + pkt_size_words; + + if (new_head > last_valid) { + // Wrap to start of buffer + new_head = pkt_size_words; + head = 0; + // Check room between 0 and tail + if (new_head >= tail) { + return Result::error("Transmit buffer full (wrap)"); + } + } else if (tail > head) { + // Tail is ahead, check room + if (new_head >= tail) { + return Result::error("Transmit buffer full"); + } + } + // else: tail <= head, no wrap issues, plenty of room + + // Two-pass write protocol (matches Central's cbSendPacket): + // Central's consumer skips entries where the first uint32_t (time field) is 0. + // 1. Write everything EXCEPT the first uint32_t (time=0 means "not ready") + // 2. Atomically write the first uint32_t (time field) to signal "packet ready" + // + // The time field (first uint32_t) MUST be non-zero. If the caller didn't set it, + // stamp it from the receive buffer (like old cbSendPacket did). + const uint32_t* pkt_words = reinterpret_cast(write_data); + uint32_t time_word = pkt_words[0]; + if (time_word == 0) { + PROCTIME t = m_impl->recLasttime(); + time_word = (t != 0) ? static_cast(t) : 1; + } + + // Pass 1: write payload contiguously, skip first word (leave time=0) + std::memcpy(&buf[head + 1], &pkt_words[1], (pkt_size_words - 1) * sizeof(uint32_t)); + + // Advance head index + xmt->headindex = new_head; + + // Pass 2: atomically write the time field to mark packet as ready +#ifdef _WIN32 + InterlockedExchange(reinterpret_cast(&buf[head]), + static_cast(time_word)); +#else + __atomic_store_n(&buf[head], time_word, __ATOMIC_RELEASE); +#endif + return Result::ok(); +} + +Result ShmemSession::dequeuePacket(cbPKT_GENERIC& pkt) { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->xmt_buffer_raw) { + return Result::error("Transmit buffer not initialized"); + } + + auto* xmt = m_impl->xmtGlobal(); + uint32_t* buf = m_impl->xmtGlobalBuffer(); + + uint32_t head = xmt->headindex; + uint32_t tail = xmt->tailindex; + + if (head == tail) { + return Result::ok(false); // Queue is empty + } + + // Linear contiguous read (packets never straddle buffer boundary) + // If tail > head, it means the writer wrapped — skip to 0 + if (tail > head) { + tail = 0; + } + + // Wait for the time field to become non-zero (two-pass write protocol) + if (buf[tail] == 0) { + return Result::ok(false); // Packet not yet ready + } + + uint32_t* pkt_data = reinterpret_cast(&pkt); + + // Read header contiguously + pkt_data[0] = buf[tail]; + pkt_data[1] = buf[tail + 1]; + pkt_data[2] = buf[tail + 2]; + pkt_data[3] = buf[tail + 3]; + + uint32_t pkt_size_words = cbPKT_HEADER_32SIZE + pkt.cbpkt_header.dlen; + + // Read remaining payload contiguously + std::memcpy(&pkt_data[4], &buf[tail + 4], (pkt_size_words - 4) * sizeof(uint32_t)); + + // Clear the time field to 0 so it's clean for next use + buf[tail] = 0; + + xmt->tailindex = tail + pkt_size_words; + xmt->transmitted++; + + return Result::ok(true); +} + +bool ShmemSession::hasTransmitPackets() const { + if (!m_impl || !m_impl->is_open || !m_impl->xmt_buffer_raw) { + return false; + } + auto* xmt = m_impl->xmtGlobal(); + return xmt->headindex != xmt->tailindex; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Local Transmit Queue Operations (IPC-only packets) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +Result ShmemSession::enqueueLocalPacket(const cbPKT_GENERIC& pkt) { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->xmt_local_buffer_raw) { + return Result::error("Local transmit buffer not initialized"); + } + + auto* xmt_local = m_impl->xmtLocal(); + uint32_t* buf = m_impl->xmtLocalBuffer(); + + uint32_t pkt_size_words = cbPKT_HEADER_32SIZE + pkt.cbpkt_header.dlen; + + uint32_t head = xmt_local->headindex; + uint32_t tail = xmt_local->tailindex; + uint32_t last_valid = xmt_local->last_valid_index; + + // Linear buffer with wrap-to-zero (matches Central's cbSendLoopbackPacket) + uint32_t new_head = head + pkt_size_words; + + if (new_head > last_valid) { + new_head = pkt_size_words; + head = 0; + if (new_head >= tail) { + return Result::error("Local transmit buffer full (wrap)"); + } + } else if (tail > head) { + if (new_head >= tail) { + return Result::error("Local transmit buffer full"); + } + } + + // Two-pass write: payload first, then atomically write time field + const uint32_t* pkt_data = reinterpret_cast(&pkt); + uint32_t time_word = pkt_data[0]; + if (time_word == 0) { + PROCTIME t = m_impl->recLasttime(); + time_word = (t != 0) ? static_cast(t) : 1; + } + + std::memcpy(&buf[head + 1], &pkt_data[1], (pkt_size_words - 1) * sizeof(uint32_t)); + xmt_local->headindex = new_head; + +#ifdef _WIN32 + InterlockedExchange(reinterpret_cast(&buf[head]), + static_cast(time_word)); +#else + __atomic_store_n(&buf[head], time_word, __ATOMIC_RELEASE); +#endif + return Result::ok(); +} + +Result ShmemSession::dequeueLocalPacket(cbPKT_GENERIC& pkt) { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->xmt_local_buffer_raw) { + return Result::error("Local transmit buffer not initialized"); + } + + auto* xmt_local = m_impl->xmtLocal(); + uint32_t* buf = m_impl->xmtLocalBuffer(); + + uint32_t head = xmt_local->headindex; + uint32_t tail = xmt_local->tailindex; + + if (head == tail) { + return Result::ok(false); // Queue is empty + } + + // Linear contiguous read (packets never straddle buffer boundary) + // If tail > head, it means the writer wrapped — skip to 0 + if (tail > head) { + tail = 0; + } + + // Wait for the time field to become non-zero (two-pass write protocol) + if (buf[tail] == 0) { + return Result::ok(false); // Packet not yet ready + } + + uint32_t* pkt_data = reinterpret_cast(&pkt); + + // Read header contiguously + pkt_data[0] = buf[tail]; + pkt_data[1] = buf[tail + 1]; + pkt_data[2] = buf[tail + 2]; + pkt_data[3] = buf[tail + 3]; + + uint32_t pkt_size_words = cbPKT_HEADER_32SIZE + pkt.cbpkt_header.dlen; + + // Read remaining payload contiguously + std::memcpy(&pkt_data[4], &buf[tail + 4], (pkt_size_words - 4) * sizeof(uint32_t)); + + // Clear the time field to 0 so it's clean for next use + buf[tail] = 0; + + xmt_local->tailindex = tail + pkt_size_words; + xmt_local->transmitted++; + + return Result::ok(true); +} + +bool ShmemSession::hasLocalTransmitPackets() const { + if (!m_impl || !m_impl->is_open || !m_impl->xmt_local_buffer_raw) { + return false; + } + auto* xmt_local = m_impl->xmtLocal(); + return xmt_local->headindex != xmt_local->tailindex; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// PC Status Buffer Access + +Result ShmemSession::getNumTotalChans() const { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->status_buffer_raw) { + return Result::error("Status buffer not initialized"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + return Result::ok(static_cast(m_impl->status_buffer_raw)->m_nNumTotalChans); + } else { + // CENTRAL and CENTRAL_COMPAT share the same CentralPCStatus struct + return Result::ok(static_cast(m_impl->status_buffer_raw)->m_nNumTotalChans); + } +} + +Result ShmemSession::getNspStatus(cbproto::InstrumentId id) const { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->status_buffer_raw) { + return Result::error("Status buffer not initialized"); + } + + uint32_t index = id.toIndex(); + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (index != 0) { + return Result::error("Native mode: single instrument only"); + } + return Result::ok(static_cast(m_impl->status_buffer_raw)->m_nNspStatus); + } else { + if (index >= CENTRAL_cbMAXPROCS) { + return Result::error("Invalid instrument ID"); + } + return Result::ok(static_cast(m_impl->status_buffer_raw)->m_nNspStatus[index]); + } +} + +Result ShmemSession::setNspStatus(cbproto::InstrumentId id, NSPStatus status) { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->status_buffer_raw) { + return Result::error("Status buffer not initialized"); + } + + uint32_t index = id.toIndex(); + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (index != 0) { + return Result::error("Native mode: single instrument only"); + } + static_cast(m_impl->status_buffer_raw)->m_nNspStatus = status; + } else { + if (index >= CENTRAL_cbMAXPROCS) { + return Result::error("Invalid instrument ID"); + } + static_cast(m_impl->status_buffer_raw)->m_nNspStatus[index] = status; + } + + return Result::ok(); +} + +Result ShmemSession::isGeminiSystem() const { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->status_buffer_raw) { + return Result::error("Status buffer not initialized"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + return Result::ok(static_cast(m_impl->status_buffer_raw)->m_nGeminiSystem != 0); + } else { + return Result::ok(static_cast(m_impl->status_buffer_raw)->m_nGeminiSystem != 0); + } +} + +Result ShmemSession::setGeminiSystem(bool is_gemini) { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->status_buffer_raw) { + return Result::error("Status buffer not initialized"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + static_cast(m_impl->status_buffer_raw)->m_nGeminiSystem = is_gemini ? 1 : 0; + } else { + static_cast(m_impl->status_buffer_raw)->m_nGeminiSystem = is_gemini ? 1 : 0; + } + + return Result::ok(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Spike Cache Buffer Access + +Result ShmemSession::getSpikeCache(uint32_t channel, CentralSpikeCache& cache) const { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->spike_buffer_raw) { + return Result::error("Spike buffer not initialized"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (channel >= NATIVE_cbPKT_SPKCACHELINECNT) { + return Result::error("Invalid channel number"); + } + // Copy from NativeSpikeCache to CentralSpikeCache (same field layout) + auto* spike = static_cast(m_impl->spike_buffer_raw); + auto& src = spike->cache[channel]; + cache.chid = src.chid; + cache.pktcnt = src.pktcnt; + cache.pktsize = src.pktsize; + cache.head = src.head; + cache.valid = src.valid; + std::memcpy(cache.spkpkt, src.spkpkt, sizeof(cbPKT_SPK) * src.pktcnt); + } else { + if (channel >= CENTRAL_cbPKT_SPKCACHELINECNT) { + return Result::error("Invalid channel number"); + } + auto* spike = static_cast(m_impl->spike_buffer_raw); + cache = spike->cache[channel]; + } + + return Result::ok(); +} + +Result ShmemSession::getRecentSpike(uint32_t channel, cbPKT_SPK& spike) const { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->spike_buffer_raw) { + return Result::error("Spike buffer not initialized"); + } + + if (m_impl->layout == ShmemLayout::NATIVE) { + if (channel >= NATIVE_cbPKT_SPKCACHELINECNT) { + return Result::error("Invalid channel number"); + } + auto* buf = static_cast(m_impl->spike_buffer_raw); + const auto& cache = buf->cache[channel]; + if (cache.valid == 0) { + return Result::ok(false); + } + uint32_t recent_idx = (cache.head == 0) ? (cache.pktcnt - 1) : (cache.head - 1); + spike = cache.spkpkt[recent_idx]; + return Result::ok(true); + } else { + if (channel >= CENTRAL_cbPKT_SPKCACHELINECNT) { + return Result::error("Invalid channel number"); + } + auto* buf = static_cast(m_impl->spike_buffer_raw); + const auto& cache = buf->cache[channel]; + if (cache.valid == 0) { + return Result::ok(false); + } + uint32_t recent_idx = (cache.head == 0) ? (cache.pktcnt - 1) : (cache.head - 1); + spike = cache.spkpkt[recent_idx]; + return Result::ok(true); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Synchronization methods + +Result ShmemSession::waitForData(uint32_t timeout_ms) const { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + +#ifdef _WIN32 + if (!m_impl->signal_event) { + return Result::error("Signal event not initialized"); + } + + DWORD result = WaitForSingleObject(m_impl->signal_event, timeout_ms); + if (result == WAIT_OBJECT_0) { + return Result::ok(true); + } else if (result == WAIT_TIMEOUT) { + return Result::ok(false); + } else { + return Result::error("WaitForSingleObject failed"); + } + +#else + if (m_impl->signal_event == SEM_FAILED) { + return Result::error("Signal event not initialized"); + } + + timespec ts; +#ifdef __APPLE__ + struct timeval tv; + gettimeofday(&tv, nullptr); + ts.tv_sec = tv.tv_sec; + ts.tv_nsec = tv.tv_usec * 1000; +#else + clock_gettime(CLOCK_REALTIME, &ts); +#endif + + long ns = timeout_ms * 1000000L; + const long NANOSECONDS_PER_SEC = 1000000000L; + ts.tv_nsec += ns; + if (ts.tv_nsec >= NANOSECONDS_PER_SEC) { + ts.tv_sec += ts.tv_nsec / NANOSECONDS_PER_SEC; + ts.tv_nsec = ts.tv_nsec % NANOSECONDS_PER_SEC; + } + +#ifdef __APPLE__ + int retries = timeout_ms / 10; + for (int i = 0; i < retries; ++i) { + if (sem_trywait(m_impl->signal_event) == 0) { + return Result::ok(true); + } + usleep(10000); + } + if (sem_trywait(m_impl->signal_event) == 0) { + return Result::ok(true); + } + return Result::ok(false); +#else + int result = sem_timedwait(m_impl->signal_event, &ts); + if (result == 0) { + return Result::ok(true); + } else if (errno == ETIMEDOUT) { + return Result::ok(false); + } else { + return Result::error("sem_timedwait failed: " + std::string(strerror(errno))); + } +#endif +#endif +} + +Result ShmemSession::signalData() { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + +#ifdef _WIN32 + if (!m_impl->signal_event) { + return Result::error("Signal event not initialized"); + } + if (!SetEvent(m_impl->signal_event)) { + return Result::error("SetEvent failed"); + } + return Result::ok(); +#else + if (m_impl->signal_event == SEM_FAILED) { + return Result::error("Signal event not initialized"); + } + if (sem_post(m_impl->signal_event) != 0) { + return Result::error("sem_post failed: " + std::string(strerror(errno))); + } + return Result::ok(); +#endif +} + +Result ShmemSession::resetSignal() { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + +#ifdef _WIN32 + if (!m_impl->signal_event) { + return Result::error("Signal event not initialized"); + } + if (!ResetEvent(m_impl->signal_event)) { + return Result::error("ResetEvent failed"); + } + return Result::ok(); +#else + if (m_impl->signal_event == SEM_FAILED) { + return Result::error("Signal event not initialized"); + } + while (sem_trywait(m_impl->signal_event) == 0) { + // Drain all pending signals + } + return Result::ok(); +#endif +} + +PROCTIME ShmemSession::getLastTime() const { + if (!m_impl || !m_impl->is_open || !m_impl->rec_buffer_raw) { + return 0; + } + return m_impl->recLasttime(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Instrument Filtering + +void ShmemSession::setInstrumentFilter(int32_t instrument_index) { + m_impl->instrument_filter = instrument_index; +} + +int32_t ShmemSession::getInstrumentFilter() const { + return m_impl->instrument_filter; +} + +cbproto_protocol_version_t ShmemSession::getCompatProtocolVersion() const { + return m_impl->compat_protocol; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Receive buffer reading methods + +Result ShmemSession::readReceiveBuffer(cbPKT_GENERIC* packets, size_t max_packets, size_t& packets_read) { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->rec_buffer_raw) { + return Result::error("Receive buffer not initialized"); + } + if (!packets || max_packets == 0) { + return Result::error("Invalid parameters"); + } + + packets_read = 0; + uint32_t* buf = m_impl->recBuffer(); + uint32_t buflen = m_impl->rec_buffer_len; + + uint32_t head_index = m_impl->recHeadindex(); + uint32_t head_wrap = m_impl->recHeadwrap(); + + if (m_impl->rec_tailwrap == head_wrap && m_impl->rec_tailindex == head_index) { + return Result::ok(); + } + + const bool needs_translation = (m_impl->layout == ShmemLayout::CENTRAL_COMPAT && + m_impl->compat_protocol != CBPROTO_PROTOCOL_CURRENT); + + uint8_t raw_buf[cbPKT_MAX_SIZE]; + + while (packets_read < max_packets) { + if (m_impl->rec_tailwrap == head_wrap && m_impl->rec_tailindex == head_index) { + break; + } + + if ((m_impl->rec_tailwrap + 1 == head_wrap && m_impl->rec_tailindex < head_index) || + (m_impl->rec_tailwrap + 1 < head_wrap)) { + m_impl->rec_tailindex = head_index; + m_impl->rec_tailwrap = head_wrap; + return Result::error("Receive buffer overrun - data lost"); + } + + // Parse the packet header to determine packet size based on protocol version. + // Central writes raw device packets; the header format depends on the protocol. + uint32_t raw_header_32size; + uint32_t raw_dlen; + + if (needs_translation && m_impl->compat_protocol == CBPROTO_PROTOCOL_311) { + raw_header_32size = cbproto::HEADER_SIZE_311 / sizeof(uint32_t); // 2 + auto* hdr = reinterpret_cast(&buf[m_impl->rec_tailindex]); + raw_dlen = hdr->dlen; + } else if (needs_translation && m_impl->compat_protocol == CBPROTO_PROTOCOL_400) { + raw_header_32size = cbproto::HEADER_SIZE_400 / sizeof(uint32_t); // 4 + auto* hdr = reinterpret_cast(&buf[m_impl->rec_tailindex]); + raw_dlen = hdr->dlen; + } else { + // 4.1+ / current / NATIVE / CENTRAL layouts: use current header format + raw_header_32size = cbPKT_HEADER_32SIZE; // 4 + auto* hdr = reinterpret_cast(&buf[m_impl->rec_tailindex]); + raw_dlen = hdr->dlen; + } + + uint32_t pkt_size_dwords = raw_header_32size + raw_dlen; + + if (pkt_size_dwords == 0 || pkt_size_dwords > (sizeof(cbPKT_GENERIC) / sizeof(uint32_t))) { + m_impl->rec_tailindex++; + if (m_impl->rec_tailindex >= buflen) { + m_impl->rec_tailindex = 0; + m_impl->rec_tailwrap++; + } + continue; + } + + if (needs_translation) { + // Copy raw bytes from ring buffer into a temp buffer for translation. + // Packets never straddle the buffer boundary (Central wraps before that), + // but we handle it defensively. + uint32_t raw_bytes = pkt_size_dwords * sizeof(uint32_t); + uint32_t end_index = m_impl->rec_tailindex + pkt_size_dwords; + + if (end_index <= buflen) { + std::memcpy(raw_buf, &buf[m_impl->rec_tailindex], raw_bytes); + } else { + uint32_t first_part = (buflen - m_impl->rec_tailindex) * sizeof(uint32_t); + uint32_t second_part = raw_bytes - first_part; + std::memcpy(raw_buf, &buf[m_impl->rec_tailindex], first_part); + std::memcpy(raw_buf + first_part, &buf[0], second_part); + } + + // Translate header + payload into the output slot + uint8_t* dest = reinterpret_cast(&packets[packets_read]); + auto& dest_hdr = packets[packets_read].cbpkt_header; + + if (m_impl->compat_protocol == CBPROTO_PROTOCOL_311) { + auto* src_hdr = reinterpret_cast(raw_buf); + // Translate header: 3.11 (8 bytes) → current (16 bytes) + dest_hdr.time = static_cast(src_hdr->time) * 1000000000ULL / 30000ULL; + dest_hdr.chid = src_hdr->chid; + dest_hdr.type = src_hdr->type; + dest_hdr.dlen = src_hdr->dlen; + dest_hdr.instrument = 0; + dest_hdr.reserved = 0; + // Translate payload + cbproto::PacketTranslator::translatePayload_311_to_current(raw_buf, dest); + } else if (m_impl->compat_protocol == CBPROTO_PROTOCOL_400) { + auto* src_hdr = reinterpret_cast(raw_buf); + // Translate header: 4.0 (16 bytes, different field layout) → current (16 bytes) + dest_hdr.time = src_hdr->time; + dest_hdr.chid = src_hdr->chid; + dest_hdr.type = src_hdr->type; // 8-bit type → 16-bit (zero-extended) + dest_hdr.dlen = src_hdr->dlen; + dest_hdr.instrument = src_hdr->instrument; + dest_hdr.reserved = 0; + // Translate payload + cbproto::PacketTranslator::translatePayload_400_to_current(raw_buf, dest); + } else { + // 4.1 (CBPROTO_PROTOCOL_410): header is identical, only some payloads differ + std::memcpy(dest, raw_buf, raw_bytes); + cbproto::PacketTranslator::translatePayload_410_to_current(dest, dest); + } + } else { + // No translation needed (4.2+, NATIVE, or CENTRAL layout). + // Copy directly from ring buffer to output. + uint32_t end_index = m_impl->rec_tailindex + pkt_size_dwords; + + if (end_index <= buflen) { + std::memcpy(&packets[packets_read], + &buf[m_impl->rec_tailindex], + pkt_size_dwords * sizeof(uint32_t)); + } else { + uint32_t first_part_size = buflen - m_impl->rec_tailindex; + uint32_t second_part_size = pkt_size_dwords - first_part_size; + + std::memcpy(&packets[packets_read], + &buf[m_impl->rec_tailindex], + first_part_size * sizeof(uint32_t)); + + std::memcpy(reinterpret_cast(&packets[packets_read]) + first_part_size, + &buf[0], + second_part_size * sizeof(uint32_t)); + } + } + + // Advance tail past this packet (consumed from ring buffer regardless of filter) + m_impl->rec_tailindex += pkt_size_dwords; + if (m_impl->rec_tailindex >= buflen) { + m_impl->rec_tailindex -= buflen; + m_impl->rec_tailwrap++; + } + + // Apply instrument filter: skip packets not matching our instrument + if (m_impl->instrument_filter >= 0) { + uint8_t pkt_instrument = packets[packets_read].cbpkt_header.instrument; + if (pkt_instrument != static_cast(m_impl->instrument_filter)) { + continue; // Skip this packet, don't increment packets_read + } + } + + packets_read++; + } + + return Result::ok(); +} + +Result ShmemSession::getReceiveBufferStats(uint32_t& received, uint32_t& available) const { + if (!m_impl || !m_impl->is_open) { + return Result::error("Session is not open"); + } + if (!m_impl->rec_buffer_raw) { + return Result::error("Receive buffer not initialized"); + } + + received = m_impl->recReceived(); + uint32_t buflen = m_impl->rec_buffer_len; + + uint32_t head_index = m_impl->recHeadindex(); + uint32_t head_wrap = m_impl->recHeadwrap(); + + if (m_impl->rec_tailwrap == head_wrap) { + if (head_index >= m_impl->rec_tailindex) { + available = head_index - m_impl->rec_tailindex; + } else { + available = 0; + } + } else if (m_impl->rec_tailwrap + 1 == head_wrap) { + available = (buflen - m_impl->rec_tailindex) + head_index; + } else { + available = 0; + } + + return Result::ok(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Clock Synchronization + +void ShmemSession::setClockSync(int64_t offset_ns, int64_t uncertainty_ns) { + if (!isOpen()) + return; + + if (m_impl->layout == ShmemLayout::NATIVE) { + auto* cfg = m_impl->nativeCfg(); + cfg->clock_offset_ns = offset_ns; + cfg->clock_uncertainty_ns = uncertainty_ns; + cfg->clock_sync_valid = 1; + } + // CENTRAL and CENTRAL_COMPAT layouts don't have clock sync fields +} + +std::optional ShmemSession::getClockOffsetNs() const { + if (!isOpen()) + return std::nullopt; + + if (m_impl->layout == ShmemLayout::NATIVE) { + const auto* cfg = m_impl->nativeCfg(); + if (cfg->clock_sync_valid) + return cfg->clock_offset_ns; + } + return std::nullopt; +} + +std::optional ShmemSession::getClockUncertaintyNs() const { + if (!isOpen()) + return std::nullopt; + + if (m_impl->layout == ShmemLayout::NATIVE) { + const auto* cfg = m_impl->nativeCfg(); + if (cfg->clock_sync_valid) + return cfg->clock_uncertainty_ns; + } + return std::nullopt; +} + +bool ShmemSession::isOwnerAlive() const { + if (!isOpen() || m_impl->mode != Mode::CLIENT) + return true; + if (m_impl->layout != ShmemLayout::NATIVE) + return true; + + uint32_t pid = m_impl->nativeCfg()->owner_pid; + if (pid == 0) + return true; // Pre-liveness segments or unknown — assume alive + +#ifdef _WIN32 + HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (h) { + CloseHandle(h); + return true; + } + return false; +#else + // kill(pid, 0) checks existence without sending a signal. + // EPERM means the process exists but we lack permission to signal it. + return kill(static_cast(pid), 0) == 0 || errno == EPERM; +#endif +} + +} // namespace cbshm diff --git a/src/cbutil/CMakeLists.txt b/src/cbutil/CMakeLists.txt new file mode 100644 index 00000000..75d006b0 --- /dev/null +++ b/src/cbutil/CMakeLists.txt @@ -0,0 +1,10 @@ +add_library(cbutil INTERFACE) +target_include_directories(cbutil INTERFACE + $ + $ +) +target_compile_features(cbutil INTERFACE cxx_std_17) + +install(TARGETS cbutil + EXPORT CBSDKTargets +) diff --git a/src/cbutil/include/cbutil/result.h b/src/cbutil/include/cbutil/result.h new file mode 100644 index 00000000..93fa4f21 --- /dev/null +++ b/src/cbutil/include/cbutil/result.h @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file result.h +/// +/// @brief Result type for error handling without exceptions +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CBUTIL_RESULT_H +#define CBUTIL_RESULT_H + +#include +#include + +namespace cbutil { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Result template for operations that return a value or an error +/// +template +class Result { +public: + static Result ok(T value) { + Result r; + r.m_ok = true; + r.m_value = std::move(value); + return r; + } + + static Result error(const std::string& msg) { + Result r; + r.m_ok = false; + r.m_error = msg; + return r; + } + + [[nodiscard]] bool isOk() const { return m_ok; } + [[nodiscard]] bool isError() const { return !m_ok; } + + const T& value() const { return m_value.value(); } + T& value() { return m_value.value(); } + [[nodiscard]] const std::string& error() const { return m_error; } + +private: + bool m_ok = false; + std::optional m_value; + std::string m_error; +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Specialization for operations that succeed or fail without returning a value +/// +template<> +class Result { +public: + static Result ok() { + Result r; + r.m_ok = true; + return r; + } + + static Result error(const std::string& msg) { + Result r; + r.m_ok = false; + r.m_error = msg; + return r; + } + + [[nodiscard]] bool isOk() const { return m_ok; } + [[nodiscard]] bool isError() const { return !m_ok; } + [[nodiscard]] const std::string& error() const { return m_error; } + +private: + bool m_ok = false; + std::string m_error; +}; + +} // namespace cbutil + +#endif // CBUTIL_RESULT_H diff --git a/src/ccfutils/CMakeLists.txt b/src/ccfutils/CMakeLists.txt new file mode 100644 index 00000000..e8cde72c --- /dev/null +++ b/src/ccfutils/CMakeLists.txt @@ -0,0 +1,203 @@ +# CCFUtils - Cerebus Configuration File Utilities +# Standalone library for reading/writing Cerebus configuration files + +cmake_minimum_required(VERSION 3.16) + +project(ccfutils + DESCRIPTION "Cerebus Configuration File (CCF) Utilities" + LANGUAGES CXX +) + +########################################################################################## +# Version from parent project +if(NOT DEFINED CCFUTILS_VERSION_MAJOR) + if(DEFINED PROJECT_VERSION AND NOT "${PROJECT_VERSION}" STREQUAL "") + string(REPLACE "." ";" VERSION_LIST ${PROJECT_VERSION}) + list(GET VERSION_LIST 0 CCFUTILS_VERSION_MAJOR) + list(GET VERSION_LIST 1 CCFUTILS_VERSION_MINOR) + list(LENGTH VERSION_LIST VERSION_LIST_LENGTH) + if(VERSION_LIST_LENGTH GREATER 2) + list(GET VERSION_LIST 2 CCFUTILS_VERSION_PATCH) + else() + set(CCFUTILS_VERSION_PATCH 0) + endif() + else() + # Default version if not provided by parent + set(CCFUTILS_VERSION_MAJOR 8) + set(CCFUTILS_VERSION_MINOR 1) + set(CCFUTILS_VERSION_PATCH 0) + endif() +endif() + +########################################################################################## +# Dependencies + +# PugiXML (for XML parsing) +if(NOT TARGET pugixml::pugixml) + include(FetchContent) + set(PUGIXML_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(PUGIXML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(PUGIXML_BUILD_SHARED_AND_STATIC OFF) + set(PUGIXML_INSTALL OFF CACHE BOOL "" FORCE) + FetchContent_Declare( + pugixml + GIT_REPOSITORY https://github.com/zeux/pugixml.git + GIT_TAG v1.15 + GIT_SHALLOW TRUE + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(pugixml) + if(TARGET pugixml AND NOT TARGET pugixml::pugixml) + add_library(pugixml::pugixml ALIAS pugixml) + endif() +endif() + +# cbproto (new modular architecture) +# Expect cbproto to be available either as subdirectory or installed +if(NOT TARGET cbproto) + # Try to find cbproto as a subdirectory + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/../cbproto/CMakeLists.txt) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cbproto + ${CMAKE_CURRENT_BINARY_DIR}/cbproto) + else() + # Try to find installed cbproto + find_package(cbproto REQUIRED) + endif() +endif() + +########################################################################################## +# Library Sources + +set(CCFUTILS_SOURCES + src/CCFUtils.cpp + src/CCFUtilsBinary.cpp + src/CCFUtilsConcurrent.cpp + src/CCFUtilsXml.cpp + src/CCFUtilsXmlItems.cpp + src/XmlFile.cpp + src/ccf_config.cpp +) + +set(CCFUTILS_HEADERS + include/CCFUtils.h + include/ccfutils/ccf_config.h +) + +set(CCFUTILS_INTERNAL_HEADERS + src/CCFUtilsBinary.h + src/CCFUtilsConcurrent.h + src/CCFUtilsXml.h + src/CCFUtilsXmlItems.h + src/CCFUtilsXmlItemsGenerate.h + src/CCFUtilsXmlItemsParse.h + src/XmlFile.h + src/XmlItem.h +) + +set(CCFUTILS_COMPAT_HEADERS + include/ccfutils/compat/platform.h + include/ccfutils/compat/debug.h +) + +########################################################################################## +# Build as STATIC library + +add_library(ccfutils STATIC + ${CCFUTILS_SOURCES} + ${CCFUTILS_HEADERS} + ${CCFUTILS_INTERNAL_HEADERS} + ${CCFUTILS_COMPAT_HEADERS} +) + +# Version definitions for compatibility headers +target_compile_definitions(ccfutils + PRIVATE + CCFUTILS_VERSION_MAJOR=${CCFUTILS_VERSION_MAJOR} + CCFUTILS_VERSION_MINOR=${CCFUTILS_VERSION_MINOR} + CCFUTILS_VERSION_PATCH=${CCFUTILS_VERSION_PATCH} +) + +# Include directories +target_include_directories(ccfutils + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +# Link dependencies +target_link_libraries(ccfutils + PUBLIC + cbproto + PRIVATE + pugixml::pugixml +) + +# Require C++17 +target_compile_features(ccfutils PUBLIC cxx_std_17) + +# Position-independent code (for linking into shared libraries) +set_target_properties(ccfutils PROPERTIES + POSITION_INDEPENDENT_CODE ON +) + +########################################################################################## +# Installation + +include(GNUInstallDirs) + +# Install library +# Note: We don't export ccfutils targets for now since it's meant to be +# used as part of the larger CBSDK build. If standalone installation is needed, +# the dependencies would need to be handled differently. +# install(TARGETS ccfutils +# EXPORT ccfutilsTargets +# LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +# ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} +# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +# ) + +# Install public headers +install(DIRECTORY include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING + PATTERN "*.h" +) + +# Install export targets +# Commented out for now - see note above about standalone installation +# install(EXPORT ccfutilsTargets +# FILE ccfutilsTargets.cmake +# NAMESPACE ccfutils:: +# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ccfutils +# ) + +# Create and install package config files +# Commented out for now - see note above about standalone installation +# include(CMakePackageConfigHelpers) +# +# configure_package_config_file( +# ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ccfutilsConfig.cmake.in +# ${CMAKE_CURRENT_BINARY_DIR}/ccfutilsConfig.cmake +# INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ccfutils +# ) +# +# write_basic_package_version_file( +# ${CMAKE_CURRENT_BINARY_DIR}/ccfutilsConfigVersion.cmake +# VERSION ${PROJECT_VERSION} +# COMPATIBILITY SameMajorVersion +# ) +# +# install(FILES +# ${CMAKE_CURRENT_BINARY_DIR}/ccfutilsConfig.cmake +# ${CMAKE_CURRENT_BINARY_DIR}/ccfutilsConfigVersion.cmake +# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ccfutils +# ) + +########################################################################################## +# Optional: Tests +# Uncomment if you want to build tests for ccfutils +# if(CBSDK_BUILD_TEST) +# add_subdirectory(tests) +# endif() diff --git a/src/ccfutils/cmake/ccfutilsConfig.cmake.in b/src/ccfutils/cmake/ccfutilsConfig.cmake.in new file mode 100644 index 00000000..326db337 --- /dev/null +++ b/src/ccfutils/cmake/ccfutilsConfig.cmake.in @@ -0,0 +1,13 @@ +@PACKAGE_INIT@ + +# ccfutils CMake configuration file + +include(CMakeFindDependencyMacro) + +# Find dependencies +find_dependency(cbproto REQUIRED) + +# Include targets +include("${CMAKE_CURRENT_LIST_DIR}/ccfutilsTargets.cmake") + +check_required_components(ccfutils) diff --git a/include/cerelink/CCFUtils.h b/src/ccfutils/include/CCFUtils.h similarity index 91% rename from include/cerelink/CCFUtils.h rename to src/ccfutils/include/CCFUtils.h index 8861bc24..55ff2bd7 100755 --- a/include/cerelink/CCFUtils.h +++ b/src/ccfutils/include/CCFUtils.h @@ -22,7 +22,7 @@ // implementation details such as Qt // -#include "cbproto.h" +#include // Latest CCF structure typedef struct { @@ -72,7 +72,7 @@ typedef enum _ccfResult }; // namespace ccf // CCF callback -typedef void (* cbCCFCallback)(uint32_t nInstance, const ccf::ccfResult res, LPCSTR szFileName, const cbStateCCF state, const uint32_t nProgress); +typedef void (* cbCCFCallback)(uint32_t nInstance, const ccf::ccfResult res, const char* szFileName, const cbStateCCF state, const uint32_t nProgress); class CCFUtils { @@ -85,9 +85,9 @@ class CCFUtils virtual ~CCFUtils(); public: - virtual ccf::ccfResult ReadCCF(LPCSTR szFileName = nullptr, bool bConvert = false); - virtual ccf::ccfResult WriteCCFNoPrompt(LPCSTR szFileName); - virtual ccf::ccfResult ReadVersion(LPCSTR szFileName); + virtual ccf::ccfResult ReadCCF(const char* szFileName = nullptr, bool bConvert = false); + virtual ccf::ccfResult WriteCCFNoPrompt(const char* szFileName); + virtual ccf::ccfResult ReadVersion(const char* szFileName); int GetInternalVersion(); int GetInternalOriginalVersion(); bool IsBinaryOriginal(); @@ -111,7 +111,7 @@ class CCFUtils int m_nInternalVersion; // internal version number int m_nInternalOriginalVersion; // internal version of original data int m_nInstance; // Library instance for CCF operations - LPCSTR m_szFileName; // filename + const char* m_szFileName; // filename bool m_bAutoSort; // Compatibility flag for auto sort bool m_bBinaryOriginal; // if original file is binary }; diff --git a/src/ccfutils/include/ccfutils/ccf_config.h b/src/ccfutils/include/ccfutils/ccf_config.h new file mode 100644 index 00000000..f7227a24 --- /dev/null +++ b/src/ccfutils/include/ccfutils/ccf_config.h @@ -0,0 +1,41 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file ccf_config.h +/// @brief Bridge between cbCCF (file data) and DeviceConfig (device state) +/// +/// Provides conversion functions between the CCF file format and device configuration. +/// These functions use only cbproto types and do not depend on cbdev. +/// +/// Usage: +/// Save: device->getDeviceConfig() -> extractDeviceConfig() -> WriteCCFNoPrompt() +/// Load: ReadCCF() -> buildConfigPackets() -> device->sendPackets() +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CCFUTILS_CCF_CONFIG_H +#define CCFUTILS_CCF_CONFIG_H + +#include +#include +#include + +namespace ccf { + +/// Extract device configuration into CCF data structure. +/// Copies fields from DeviceConfig into cbCCF, following Central's ReadCCFOfNSP() algorithm. +/// After calling this, pass the cbCCF to CCFUtils::WriteCCFNoPrompt() to save. +/// +/// @param config Device configuration obtained from IDeviceSession::getDeviceConfig() +/// @param ccf_data Output CCF structure (should be zero-initialized before calling) +void extractDeviceConfig(const cbproto::DeviceConfig& config, cbCCF& ccf_data); + +/// Build SET packets from CCF data for sending to device. +/// Constructs a vector of configuration packets following Central's SendCCF() algorithm. +/// After calling this, pass the vector to IDeviceSession::sendPackets(). +/// +/// @param ccf_data CCF data obtained from CCFUtils::ReadCCF() +/// @return Vector of generic packets ready to send to device +std::vector buildConfigPackets(const cbCCF& ccf_data); + +} // namespace ccf + +#endif // CCFUTILS_CCF_CONFIG_H diff --git a/src/ccfutils/include/ccfutils/compat/debug.h b/src/ccfutils/include/ccfutils/compat/debug.h new file mode 100644 index 00000000..55a546a1 --- /dev/null +++ b/src/ccfutils/include/ccfutils/compat/debug.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file debug.h +/// @author CereLink Development Team +/// @brief Debug macros for CCFUtils +/// +/// Minimal replacement for old debugmacs.h - provides ASSERT and TRACE macros +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CCFUTILS_COMPAT_DEBUG_H +#define CCFUTILS_COMPAT_DEBUG_H + +#include +#include + +// ASSERT macro - uses standard assert in debug builds, no-op in release +#ifndef ASSERT + #ifdef NDEBUG + #define ASSERT(x) ((void)0) + #else + #define ASSERT(x) assert(x) + #endif +#endif + +// TRACE macro - prints to stderr in debug builds, no-op in release +#ifndef TRACE + #ifdef NDEBUG + // No-op in release builds + #ifdef _MSC_VER + #define TRACE(...) ((void)0) + #else + #define TRACE(fmt, ...) ((void)0) + #endif + #else + // Print to stderr in debug builds + #ifdef _MSC_VER + #define TRACE(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__) + #else + #define TRACE(fmt, args...) fprintf(stderr, fmt, ##args) + #endif + #endif +#endif + +// Legacy _DEBUG define (some code checks for this) +#ifdef DEBUG + #ifndef _DEBUG + #define _DEBUG + #endif +#endif + +#endif // CCFUTILS_COMPAT_DEBUG_H diff --git a/src/ccfutils/include/ccfutils/compat/platform.h b/src/ccfutils/include/ccfutils/compat/platform.h new file mode 100644 index 00000000..5db93151 --- /dev/null +++ b/src/ccfutils/include/ccfutils/compat/platform.h @@ -0,0 +1,26 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file platform.h +/// @author CereLink Development Team +/// @brief Platform compatibility definitions for CCFUtils +/// +/// Minimal replacement for old StdAfx.h - provides cross-platform string function compatibility +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CCFUTILS_COMPAT_PLATFORM_H +#define CCFUTILS_COMPAT_PLATFORM_H + +#include +#include +#include + +// Windows-specific definitions +#ifdef _WIN32 + #ifndef WIN32 + #define WIN32 + #endif + + #include + +#endif // _WIN32 + +#endif // CCFUTILS_COMPAT_PLATFORM_H diff --git a/src/ccfutils/CCFUtils.cpp b/src/ccfutils/src/CCFUtils.cpp similarity index 94% rename from src/ccfutils/CCFUtils.cpp rename to src/ccfutils/src/CCFUtils.cpp index 6f2d825d..f8c22ee6 100755 --- a/src/ccfutils/CCFUtils.cpp +++ b/src/ccfutils/src/CCFUtils.cpp @@ -1,4 +1,4 @@ -// =STS=> CCFUtils.cpp[1691].aa28 open SMID:29 +// =STS=> CCFUtils.cpp[1691].aa28 open SMID:29 ////////////////////////////////////////////////////////////////////// // // (c) Copyright 2003-2008 Cyberkinetics, Inc. @@ -14,7 +14,11 @@ // ////////////////////////////////////////////////////////////////////// -#include "../include/cerelink/CCFUtils.h" +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include +#include "../include/CCFUtils.h" #include "CCFUtilsBinary.h" #include "CCFUtilsXml.h" #include "CCFUtilsConcurrent.h" @@ -86,7 +90,7 @@ CCFUtils * CCFUtils::Convert(CCFUtils * pOldConfig) // Do NOT prompt for the file to write out. // Inputs: // szFileName - the name of the file to write -ccfResult CCFUtils::WriteCCFNoPrompt(LPCSTR szFileName) +ccfResult CCFUtils::WriteCCFNoPrompt(const char* szFileName) { m_szFileName = szFileName; ccfResult res = CCFRESULT_SUCCESS; @@ -112,7 +116,7 @@ ccfResult CCFUtils::WriteCCFNoPrompt(LPCSTR szFileName) // Purpose: Read a CCF version information alone. // Inputs: // szFileName - the name of the file to read (if NULL uses live config) -ccfResult CCFUtils::ReadVersion(LPCSTR szFileName) +ccfResult CCFUtils::ReadVersion(const char* szFileName) { ccfResult res = CCFRESULT_SUCCESS; m_szFileName = szFileName; @@ -146,7 +150,7 @@ ccfResult CCFUtils::ReadVersion(LPCSTR szFileName) // Inputs: // szFileName - the name of the file to read (if NULL raises error; use SDK calls to fetch from device) // bConvert - if conversion can happen -ccfResult CCFUtils::ReadCCF(LPCSTR szFileName, bool bConvert) +ccfResult CCFUtils::ReadCCF(const char* szFileName, bool bConvert) { m_szFileName = szFileName; ccfResult res = CCFRESULT_SUCCESS; diff --git a/src/ccfutils/CCFUtilsBinary.cpp b/src/ccfutils/src/CCFUtilsBinary.cpp similarity index 99% rename from src/ccfutils/CCFUtilsBinary.cpp rename to src/ccfutils/src/CCFUtilsBinary.cpp index 60e8498d..086b563d 100755 --- a/src/ccfutils/CCFUtilsBinary.cpp +++ b/src/ccfutils/src/CCFUtilsBinary.cpp @@ -1,4 +1,4 @@ -// =STS=> CCFUtilsBinary.cpp[4874].aa02 open SMID:2 +// =STS=> CCFUtilsBinary.cpp[4874].aa02 open SMID:2 ////////////////////////////////////////////////////////////////////// // // (c) Copyright 2012-2013 Blackrock Microsystems @@ -13,7 +13,10 @@ // ////////////////////////////////////////////////////////////////////// -#include "StdAfx.h" +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include #include "CCFUtilsBinary.h" #include @@ -40,7 +43,7 @@ CCFUtilsBinary::CCFUtilsBinary() // Purpose: Read binary CCF file version // Inputs: // szFileName - the name of the file to read -ccfResult CCFUtilsBinary::ReadVersion(LPCSTR szFileName) +ccfResult CCFUtilsBinary::ReadVersion(const char* szFileName) { FILE * hSettingsFile = 0; ccfResult res = CCFRESULT_SUCCESS; @@ -103,7 +106,7 @@ ccfResult CCFUtilsBinary::ReadVersion(LPCSTR szFileName) // Inputs: // szFileName - the name of the file to read // bConvert - if convertion can happen -ccfResult CCFUtilsBinary::ReadCCF(LPCSTR szFileName, bool bConvert) +ccfResult CCFUtilsBinary::ReadCCF(const char* szFileName, bool bConvert) { ccfResult res = CCFRESULT_SUCCESS; // First read the version @@ -1263,7 +1266,7 @@ void CCFUtilsBinary::ReadSpikeSortingPackets(cbPKT_GENERIC_CB2003_10 *pPkt) // Do NOT prompt for the file to write out. // Inputs: // szFileName - the name of the file to write -ccfResult CCFUtilsBinary::WriteCCFNoPrompt(LPCSTR szFileName) +ccfResult CCFUtilsBinary::WriteCCFNoPrompt(const char* szFileName) { m_szFileName = szFileName; // Open a file @@ -1277,7 +1280,7 @@ ccfResult CCFUtilsBinary::WriteCCFNoPrompt(LPCSTR szFileName) fwrite(m_szConfigFileHeader, strlen(m_szConfigFileHeader), 1, hSettingsFile); char szVer[sizeof(m_szConfigFileVersion)] = {0}; // ANSI - all 0 - _snprintf(szVer, sizeof(szVer) - 1, "%d.%d", cbVERSION_MAJOR, cbVERSION_MINOR); + snprintf(szVer, sizeof(szVer), "%d.%d", cbVERSION_MAJOR, cbVERSION_MINOR); fwrite(szVer, sizeof(szVer) - 1, 1, hSettingsFile); diff --git a/src/ccfutils/CCFUtilsBinary.h b/src/ccfutils/src/CCFUtilsBinary.h similarity index 99% rename from src/ccfutils/CCFUtilsBinary.h rename to src/ccfutils/src/CCFUtilsBinary.h index bbd025d8..edaaa3a3 100755 --- a/src/ccfutils/CCFUtilsBinary.h +++ b/src/ccfutils/src/CCFUtilsBinary.h @@ -19,7 +19,8 @@ #ifndef CCFUTILSBINARY_H_INCLUDED #define CCFUTILSBINARY_H_INCLUDED -#include "../include/cerelink/CCFUtils.h" +#include +#include "../include/CCFUtils.h" #include #include @@ -870,8 +871,8 @@ class CCFUtilsBinary : public CCFUtils public: // Purpose: load the channel configuration from the file - ccf::ccfResult ReadCCF(LPCSTR szFileName, bool bConvert); - ccf::ccfResult ReadVersion(LPCSTR szFileName); // Read the version alone + ccf::ccfResult ReadCCF(const char* szFileName, bool bConvert); + ccf::ccfResult ReadVersion(const char* szFileName); // Read the version alone ccf::ccfResult SetProcInfo(const cbPROCINFO& isInfo); public: @@ -882,7 +883,7 @@ class CCFUtilsBinary : public CCFUtils // Convert from old config (generic) virtual CCFUtils * Convert(CCFUtils * pOldConfig); // Keep old binary writing for possible backward porting - virtual ccf::ccfResult WriteCCFNoPrompt(LPCSTR szFileName); + virtual ccf::ccfResult WriteCCFNoPrompt(const char* szFileName); private: // Used as identifiers in the channel configuration files diff --git a/src/ccfutils/CCFUtilsConcurrent.cpp b/src/ccfutils/src/CCFUtilsConcurrent.cpp similarity index 90% rename from src/ccfutils/CCFUtilsConcurrent.cpp rename to src/ccfutils/src/CCFUtilsConcurrent.cpp index f61c6fad..e28a8d29 100755 --- a/src/ccfutils/CCFUtilsConcurrent.cpp +++ b/src/ccfutils/src/CCFUtilsConcurrent.cpp @@ -12,6 +12,9 @@ // ////////////////////////////////////////////////////////////////////// +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + #include "CCFUtilsConcurrent.h" #include #include @@ -36,7 +39,7 @@ using namespace ccf; // pCCF - where to take an extra copy of the CCF upon successful reading void ReadCCFHelper(std::string strFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance) { - LPCSTR szFileName = strFileName.c_str(); + const char* szFileName = strFileName.c_str(); if (pCallbackFn) pCallbackFn(nInstance, CCFRESULT_SUCCESS, szFileName, CCFSTATE_THREADREAD, 0); CCFUtils config(false, pCCF, pCallbackFn, nInstance); @@ -49,7 +52,7 @@ void ReadCCFHelper(std::string strFileName, cbCCF * pCCF, cbCCFCallback pCallbac // Author & Date: Ehsan Azar 10 June 2012 // Purpose: Wrapper to run ReadCCFHelper in a thread -void ccf::ConReadCCF(LPCSTR szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance) +void ccf::ConReadCCF(const char* szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance) { std::string strFileName = szFileName == NULL ? "" : std::string(szFileName); // Launch detached thread (std::async future destruction was synchronizing and negating concurrency) @@ -67,7 +70,7 @@ void ccf::ConReadCCF(LPCSTR szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, // pCallbackFn - the progress reporting function void WriteCCFHelper(std::string strFileName, cbCCF ccf, cbCCFCallback pCallbackFn, uint32_t nInstance) { - LPCSTR szFileName = strFileName.c_str(); + const char* szFileName = strFileName.c_str(); if (pCallbackFn) pCallbackFn(nInstance, CCFRESULT_SUCCESS, szFileName, CCFSTATE_THREADWRITE, 0); // If valid ccf is passed, use it as initial data, otherwise use NULL to have it read from NSP @@ -79,7 +82,7 @@ void WriteCCFHelper(std::string strFileName, cbCCF ccf, cbCCFCallback pCallbackF // Author & Date: Ehsan Azar 10 June 2012 // Purpose: Wrapper to run WriteCCFHelper in a thread -void ccf::ConWriteCCF(LPCSTR szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance) +void ccf::ConWriteCCF(const char* szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance) { std::string strFileName = szFileName == NULL ? "" : std::string(szFileName); cbCCF ccf; diff --git a/src/ccfutils/CCFUtilsConcurrent.h b/src/ccfutils/src/CCFUtilsConcurrent.h similarity index 68% rename from src/ccfutils/CCFUtilsConcurrent.h rename to src/ccfutils/src/CCFUtilsConcurrent.h index 3ce37686..032a3243 100755 --- a/src/ccfutils/CCFUtilsConcurrent.h +++ b/src/ccfutils/src/CCFUtilsConcurrent.h @@ -18,13 +18,14 @@ #ifndef CCFUTILSCONCURRENT_H_INCLUDED #define CCFUTILSCONCURRENT_H_INCLUDED -#include "../include/cerelink/CCFUtils.h" +#include +#include "../include/CCFUtils.h" // Namespace for Cerebus Config Files namespace ccf { - void ConReadCCF(LPCSTR szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance); - void ConWriteCCF(LPCSTR szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance); + void ConReadCCF(const char* szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance); + void ConWriteCCF(const char* szFileName, cbCCF * pCCF, cbCCFCallback pCallbackFn, uint32_t nInstance); }; diff --git a/src/ccfutils/CCFUtilsXml.cpp b/src/ccfutils/src/CCFUtilsXml.cpp similarity index 96% rename from src/ccfutils/CCFUtilsXml.cpp rename to src/ccfutils/src/CCFUtilsXml.cpp index f5863e90..91831f84 100755 --- a/src/ccfutils/CCFUtilsXml.cpp +++ b/src/ccfutils/src/CCFUtilsXml.cpp @@ -1,4 +1,4 @@ -// =STS=> CCFUtilsXml.cpp[4876].aa03 open SMID:3 +// =STS=> CCFUtilsXml.cpp[4876].aa03 open SMID:3 ////////////////////////////////////////////////////////////////////// // // (c) Copyright 2012-2013 Blackrock Microsystems @@ -13,12 +13,15 @@ // ////////////////////////////////////////////////////////////////////// -#include "StdAfx.h" +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + +#include +#include +#include #include "CCFUtilsXml.h" #include "CCFUtilsXmlItemsGenerate.h" #include "CCFUtilsXmlItemsParse.h" -#include "debugmacs.h" -#include "../central/BmiVersion.h" #include #include #include @@ -29,7 +32,6 @@ #ifndef WIN32 #include #endif -#include "../include/cerelink/cbhwlib.h" #ifdef _DEBUG #undef THIS_FILE @@ -60,7 +62,7 @@ ccfResult CCFUtilsXml_v1::SetProcInfo(const cbPROCINFO& isInfo) { // Inputs: // szFileName - the name of the file to read // bConvert - if convertion can happen -ccfResult CCFUtilsXml_v1::ReadCCF(LPCSTR szFileName, bool bConvert) +ccfResult CCFUtilsXml_v1::ReadCCF(const char* szFileName, bool bConvert) { ccfResult res = CCFRESULT_SUCCESS; m_szFileName = szFileName; @@ -151,7 +153,7 @@ CCFUtils * CCFUtilsXml_v1::Convert(CCFUtils * pOldConfig) // Purpose: Write XML v1 CCF file // Inputs: // szFileName - the name of the file to write -ccfResult CCFUtilsXml_v1::WriteCCFNoPrompt(LPCSTR szFileName) +ccfResult CCFUtilsXml_v1::WriteCCFNoPrompt(const char* szFileName) { ccfResult res = CCFRESULT_SUCCESS; std::string strFilename = std::string(szFileName); @@ -297,7 +299,7 @@ void CCFUtilsXml_v1::Convert(ccf::ccfBinaryData & data) // Outputs: // Sets original XML version if valid CCF XML file, or zero fon non-XML // Returns error code -ccfResult CCFUtilsXml_v1::ReadVersion(LPCSTR szFileName) +ccfResult CCFUtilsXml_v1::ReadVersion(const char* szFileName) { ccfResult res = CCFRESULT_SUCCESS; std::string fname = szFileName; diff --git a/src/ccfutils/CCFUtilsXml.h b/src/ccfutils/src/CCFUtilsXml.h similarity index 87% rename from src/ccfutils/CCFUtilsXml.h rename to src/ccfutils/src/CCFUtilsXml.h index 1b1df200..aae41f22 100755 --- a/src/ccfutils/CCFUtilsXml.h +++ b/src/ccfutils/src/CCFUtilsXml.h @@ -40,15 +40,15 @@ class CCFUtilsXml_v1 : public CCFUtilsBinary public: // Purpose: load the channel configuration from the file - ccf::ccfResult ReadCCF(LPCSTR szFileName, bool bConvert); - ccf::ccfResult ReadVersion(LPCSTR szFileName); // Read the version alone + ccf::ccfResult ReadCCF(const char* szFileName, bool bConvert); + ccf::ccfResult ReadVersion(const char* szFileName); // Read the version alone ccf::ccfResult SetProcInfo(const cbPROCINFO& isInfo); protected: // Convert from old config (generic) virtual CCFUtils * Convert(CCFUtils * pOldConfig); // Write to file as XML v1 - virtual ccf::ccfResult WriteCCFNoPrompt(LPCSTR szFileName); + virtual ccf::ccfResult WriteCCFNoPrompt(const char* szFileName); private: // Convert from old data format (specific) diff --git a/src/ccfutils/CCFUtilsXmlItems.cpp b/src/ccfutils/src/CCFUtilsXmlItems.cpp similarity index 98% rename from src/ccfutils/CCFUtilsXmlItems.cpp rename to src/ccfutils/src/CCFUtilsXmlItems.cpp index 6b0d2fdd..6fdc7a6c 100755 --- a/src/ccfutils/CCFUtilsXmlItems.cpp +++ b/src/ccfutils/src/CCFUtilsXmlItems.cpp @@ -1,4 +1,4 @@ -// =STS=> CCFUtilsXmlItems.cpp[4878].aa03 open SMID:3 +// =STS=> CCFUtilsXmlItems.cpp[4878].aa03 open SMID:3 ////////////////////////////////////////////////////////////////////// // // (c) Copyright 2012-2013 Blackrock Microsystems @@ -41,6 +41,9 @@ // 8- Giving above rules higher precedence, use filed-name keeping its captial/lower case the same // +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + #include // Use C++ default min and max implementation. #include // For std::is_same_v #include "CCFUtilsXmlItems.h" @@ -627,8 +630,11 @@ CCFXmlItem::CCFXmlItem(std::vector lst, std::string strName) template std::any ccf::GetCCFXmlItem(T & pkt, std::string strName) { - std::any var = CCFXmlItem(pkt, strName); - return var; + // Slice to XmlItem so std::any holds typeid(XmlItem), + // matching the type checks in XmlFile::AddList/beginGroup. + // CCFXmlItem adds no data members, so slicing is safe. + CCFXmlItem ccfItem(pkt, strName); + return std::any(static_cast(ccfItem)); } // Author & Date: Ehsan Azar 16 April 2012 @@ -647,8 +653,8 @@ std::any ccf::GetCCFXmlItem(T pkt[], int count, std::string strName) if (item.IsValid()) lst.push_back(item.XmlValue()); } - std::any var = CCFXmlItem(lst, strName); - return var; + CCFXmlItem ccfItem(lst, strName); + return std::any(static_cast(ccfItem)); } // Author & Date: Ehsan Azar 16 April 2012 @@ -665,8 +671,8 @@ std::any ccf::GetCCFXmlItem(T ppkt[], int count1, int count2, std::string strNam std::vector sublst; for (int i = 0; i < count1; ++i) sublst.push_back(ccf::GetCCFXmlItem(ppkt[i], count2, strName2)); - std::any var = CCFXmlItem(sublst, strName1); - return var; + CCFXmlItem ccfItem(sublst, strName1); + return std::any(static_cast(ccfItem)); } // Author & Date: Ehsan Azar 16 April 2012 @@ -674,8 +680,8 @@ std::any ccf::GetCCFXmlItem(T ppkt[], int count1, int count2, std::string strNam template <> std::any ccf::GetCCFXmlItem(char pkt[], int count, std::string strName) { - std::any var = CCFXmlItem(pkt, count, strName); - return var; + CCFXmlItem ccfItem(pkt, count, strName); + return std::any(static_cast(ccfItem)); } //------------------------------------------------------------------------------------ @@ -1206,7 +1212,7 @@ void ccf::ReadItem(XmlFile * const xml, T item[], int count) subcount = mapItemCount[strSubKey]; mapItemCount[strSubKey] = subcount + 1; if (subcount) - strSubKey += std::to_string(subcount); + strSubKey += "<" + std::to_string(subcount) + ">"; T tmp; // Initialize with possible item int num = ItemNumber(xml, tmp, strSubKey); @@ -1275,7 +1281,7 @@ void ccf::ReadItem(XmlFile * const xml, T pItem[], int count1, int count2, std:: subcount = mapItemCount[strSubKey]; mapItemCount[strSubKey] = subcount + 1; if (subcount) - strSubKey += std::to_string(subcount); + strSubKey += "<" + std::to_string(subcount) + ">"; ReadItem(xml, pItem[i], count2, strSubKey); } } diff --git a/src/ccfutils/CCFUtilsXmlItems.h b/src/ccfutils/src/CCFUtilsXmlItems.h similarity index 98% rename from src/ccfutils/CCFUtilsXmlItems.h rename to src/ccfutils/src/CCFUtilsXmlItems.h index 5722c685..c7fd0efd 100755 --- a/src/ccfutils/CCFUtilsXmlItems.h +++ b/src/ccfutils/src/CCFUtilsXmlItems.h @@ -23,7 +23,7 @@ #ifndef CCFUTILSXMLITEMS_H_INCLUDED #define CCFUTILSXMLITEMS_H_INCLUDED -#include "../include/cerelink/cbproto.h" +#include #include "XmlItem.h" #include diff --git a/src/ccfutils/CCFUtilsXmlItemsGenerate.h b/src/ccfutils/src/CCFUtilsXmlItemsGenerate.h similarity index 100% rename from src/ccfutils/CCFUtilsXmlItemsGenerate.h rename to src/ccfutils/src/CCFUtilsXmlItemsGenerate.h diff --git a/src/ccfutils/CCFUtilsXmlItemsParse.h b/src/ccfutils/src/CCFUtilsXmlItemsParse.h similarity index 100% rename from src/ccfutils/CCFUtilsXmlItemsParse.h rename to src/ccfutils/src/CCFUtilsXmlItemsParse.h diff --git a/src/ccfutils/XmlFile.cpp b/src/ccfutils/src/XmlFile.cpp similarity index 92% rename from src/ccfutils/XmlFile.cpp rename to src/ccfutils/src/XmlFile.cpp index de6444a6..8b7c0837 100755 --- a/src/ccfutils/XmlFile.cpp +++ b/src/ccfutils/src/XmlFile.cpp @@ -1,4 +1,4 @@ -// =STS=> XmlFile.cpp[2738].aa05 open SMID:5 +// =STS=> XmlFile.cpp[2738].aa05 open SMID:5 ////////////////////////////////////////////////////////////////////////////// // // (c) Copyright 2010 - 2011 Blackrock Microsystems @@ -13,6 +13,9 @@ // ////////////////////////////////////////////////////////////////////////////// +// Platform headers MUST be included first (before cbproto) +#include "platform_first.h" + #include "XmlFile.h" #include "XmlItem.h" #include @@ -142,21 +145,21 @@ bool iequals(const std::string& a, const std::string& b) } pugi::xml_node find_or_create(pugi::xml_node& parent, const std::string& name) { - pugi::xml_node node = parent.child(name.c_str()); + pugi::xml_node node = parent.child(name); // If the node doesn't exist, create a new one if (!node) { - node = parent.append_child(name.c_str()); + node = parent.append_child(name); } return node; } pugi::xml_attribute find_or_create_attribute(pugi::xml_node& node, const std::string& name) { - pugi::xml_attribute attr = node.attribute(name.c_str()); + pugi::xml_attribute attr = node.attribute(name); // If the attribute doesn't exist, add a new one if (!attr) - attr = node.append_attribute(name.c_str()); + attr = node.append_attribute(name); return attr; } @@ -244,12 +247,11 @@ bool XmlFile::AddList(std::vector & list, std::string nodeName) count++; // count all valid items std::string strSubKey; std::map attribs; - if (subval.type() == typeid(const XmlItem *)) + if (subval.type() == typeid(XmlItem)) { - const auto * item = std::any_cast(subval); - // const auto * item = std::get(subval); - strSubKey = item->XmlName(); - attribs = item->XmlAttribs(); + const auto & item = std::any_cast(subval); + strSubKey = item.XmlName(); + attribs = item.XmlAttribs(); } if (strSubKey.empty()) strSubKey = nodeName + "_item"; @@ -319,7 +321,7 @@ bool XmlFile::beginGroup(std::string nodeName, const std::map(value); - std::any subval = item->XmlValue(); - - if (subval.type() == typeid(const XmlItem *)) + const auto & item = std::any_cast(value); + std::any subval = item.XmlValue(); + + if (subval.type() == typeid(XmlItem)) { - const auto * subitem = std::any_cast(subval); - std::string strSubKey = subitem->XmlName(); - std::map _attribs = subitem->XmlAttribs(); + const auto & subitem = std::any_cast(subval); + std::string strSubKey = subitem.XmlName(); + std::map _attribs = subitem.XmlAttribs(); // Recursively add this item beginGroup(strSubKey, _attribs, subval); endGroup(); @@ -400,15 +402,21 @@ bool XmlFile::beginGroup(std::string nodeName, const std::mapfirst; const std::any& attrValue = iterator->second; if (attrValue.type() == typeid(std::string)) - find_or_create_attribute(set, attrName) = std::any_cast(attrValue).c_str(); + find_or_create_attribute(set, attrName) = std::any_cast(attrValue); + else if (attrValue.type() == typeid(const char*)) + find_or_create_attribute(set, attrName) = std::any_cast(attrValue); else if (attrValue.type() == typeid(int)) find_or_create_attribute(set, attrName) = std::any_cast(attrValue); else if (attrValue.type() == typeid(unsigned int)) @@ -430,8 +440,10 @@ bool XmlFile::beginGroup(std::string nodeName, const std::map(attrValue); else if (attrValue.type() == typeid(unsigned long long)) find_or_create_attribute(set, attrName) = std::any_cast(attrValue); - else + else if (attrValue.type() == typeid(double)) find_or_create_attribute(set, attrName) = std::any_cast(attrValue); + else + find_or_create_attribute(set, attrName) = anyToString(attrValue); } return bRet; } @@ -532,7 +544,7 @@ std::string XmlFile::attribute(const std::string & attrName) { // Get the current node pugi::xml_node node = m_nodes.back(); - res = node.attribute(attrName.c_str()).value(); + res = node.attribute(attrName).value(); } return res; } @@ -604,12 +616,12 @@ bool XmlFile::contains(const std::string & nodeName) // If it is the first node if (m_nodes.empty()) { - set = m_doc.child(nodepath[0].c_str()); + set = m_doc.child(nodepath[0]); } else { // Get the parent node parent = m_nodes.back(); // Look if the node exists then get it - set = parent.child(nodepath[0].c_str()); + set = parent.child(nodepath[0]); } if (!set) return false; // not found @@ -618,7 +630,7 @@ bool XmlFile::contains(const std::string & nodeName) for (int i = 1; i < level; ++i) { parent = set; - set = parent.child(nodepath[i].c_str()); + set = parent.child(nodepath[i]); if (!set) return false; // not found } diff --git a/src/ccfutils/XmlFile.h b/src/ccfutils/src/XmlFile.h similarity index 100% rename from src/ccfutils/XmlFile.h rename to src/ccfutils/src/XmlFile.h diff --git a/src/ccfutils/XmlItem.h b/src/ccfutils/src/XmlItem.h similarity index 100% rename from src/ccfutils/XmlItem.h rename to src/ccfutils/src/XmlItem.h diff --git a/src/ccfutils/src/ccf_config.cpp b/src/ccfutils/src/ccf_config.cpp new file mode 100644 index 00000000..5f0ecf5a --- /dev/null +++ b/src/ccfutils/src/ccf_config.cpp @@ -0,0 +1,208 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file ccf_config.cpp +/// @brief Bridge between cbCCF (file data) and DeviceConfig (device state) +/// +/// Implementation follows Central-Suite's CCFUtils.cpp algorithms: +/// - extractDeviceConfig() mirrors ReadCCFOfNSP() (lines 444-486) +/// - buildConfigPackets() mirrors SendCCF() (lines 82-279) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +namespace ccf { + +void extractDeviceConfig(const cbproto::DeviceConfig& config, cbCCF& ccf_data) +{ + // Digital filters: copy the 4 custom filters from the 32-filter array + // Central indexes with cbFIRST_DIGITAL_FILTER + i - 1 (1-based), but CereLink's + // filtinfo array is 0-based, so we use cbFIRST_DIGITAL_FILTER + i directly for i in [0..3] + for (int i = 0; i < cbNUM_DIGITAL_FILTERS; ++i) + { + ccf_data.filtinfo[i] = config.filtinfo[cbFIRST_DIGITAL_FILTER + i]; + } + + // Channel info: direct copy + for (int i = 0; i < cbMAXCHANS; ++i) + { + ccf_data.isChan[i] = config.chaninfo[i]; + } + + // Adaptive filter info + ccf_data.isAdaptInfo = config.adaptinfo; + + // Spike sorting configuration + ccf_data.isSS_Detect = config.spike_sorting.detect; + ccf_data.isSS_ArtifactReject = config.spike_sorting.artifact_reject; + for (int i = 0; i < cbNUM_ANALOG_CHANS; ++i) + { + ccf_data.isSS_NoiseBoundary[i] = config.spike_sorting.noise_boundary[i]; + } + ccf_data.isSS_Statistics = config.spike_sorting.statistics; + + // Spike sorting status: set elapsed minutes to 99 (Central convention) + ccf_data.isSS_Status = config.spike_sorting.status; + ccf_data.isSS_Status.cntlNumUnits.fElapsedMinutes = 99; + ccf_data.isSS_Status.cntlUnitStats.fElapsedMinutes = 99; + + // System info: set type to SYSSETSPKLEN + ccf_data.isSysInfo = config.sysinfo; + ccf_data.isSysInfo.cbpkt_header.type = cbPKTTYPE_SYSSETSPKLEN; + + // N-trode info + for (int i = 0; i < cbMAXNTRODES; ++i) + { + ccf_data.isNTrodeInfo[i] = config.ntrodeinfo[i]; + } + + // LNC + ccf_data.isLnc = config.lnc; + + // Waveforms: copy and clear active flag to prevent auto-generation on load + for (int i = 0; i < AOUT_NUM_GAIN_CHANS; ++i) + { + for (int j = 0; j < cbMAX_AOUT_TRIGGER; ++j) + { + ccf_data.isWaveform[i][j] = config.waveform[i][j]; + ccf_data.isWaveform[i][j].active = 0; + } + } +} + +// Helper: reinterpret any packet struct as cbPKT_GENERIC and push to vector +template +static void pushPacket(std::vector& packets, const T& pkt) +{ + static_assert(sizeof(T) <= sizeof(cbPKT_GENERIC), "Packet exceeds cbPKT_GENERIC size"); + cbPKT_GENERIC generic; + std::memset(&generic, 0, sizeof(generic)); + std::memcpy(&generic, &pkt, sizeof(T)); + packets.push_back(generic); +} + +std::vector buildConfigPackets(const cbCCF& ccf_data) +{ + std::vector packets; + + // 1. Filters (cbPKTTYPE_FILTSET) + for (int i = 0; i < cbNUM_DIGITAL_FILTERS; ++i) + { + if (ccf_data.filtinfo[i].filt != 0) + { + cbPKT_FILTINFO pkt = ccf_data.filtinfo[i]; + pkt.cbpkt_header.type = cbPKTTYPE_FILTSET; + pushPacket(packets, pkt); + } + } + + // 2. Channels (cbPKTTYPE_CHANSET) + for (int i = 0; i < cbMAXCHANS; ++i) + { + if (ccf_data.isChan[i].chan != 0) + { + cbPKT_CHANINFO pkt = ccf_data.isChan[i]; + pkt.cbpkt_header.type = cbPKTTYPE_CHANSET; + pkt.cbpkt_header.instrument = static_cast(pkt.proc - 1); + pushPacket(packets, pkt); + } + } + + // 3. SS Statistics (cbPKTTYPE_SS_STATISTICSSET) + if (ccf_data.isSS_Statistics.cbpkt_header.type != 0) + { + cbPKT_SS_STATISTICS pkt = ccf_data.isSS_Statistics; + pkt.cbpkt_header.type = cbPKTTYPE_SS_STATISTICSSET; + pushPacket(packets, pkt); + } + + // 4. SS Noise Boundary (cbPKTTYPE_SS_NOISE_BOUNDARYSET) + for (int i = 0; i < cbNUM_ANALOG_CHANS; ++i) + { + if (ccf_data.isSS_NoiseBoundary[i].chan != 0) + { + cbPKT_SS_NOISE_BOUNDARY pkt = ccf_data.isSS_NoiseBoundary[i]; + pkt.cbpkt_header.type = cbPKTTYPE_SS_NOISE_BOUNDARYSET; + pushPacket(packets, pkt); + } + } + + // 5. SS Detect (cbPKTTYPE_SS_DETECTSET) + if (ccf_data.isSS_Detect.cbpkt_header.type != 0) + { + cbPKT_SS_DETECT pkt = ccf_data.isSS_Detect; + pkt.cbpkt_header.type = cbPKTTYPE_SS_DETECTSET; + pushPacket(packets, pkt); + } + + // 6. SS Artifact Reject (cbPKTTYPE_SS_ARTIF_REJECTSET) + if (ccf_data.isSS_ArtifactReject.cbpkt_header.type != 0) + { + cbPKT_SS_ARTIF_REJECT pkt = ccf_data.isSS_ArtifactReject; + pkt.cbpkt_header.type = cbPKTTYPE_SS_ARTIF_REJECTSET; + pushPacket(packets, pkt); + } + + // 7. SysInfo (cbPKTTYPE_SYSSETSPKLEN) + if (ccf_data.isSysInfo.cbpkt_header.type != 0) + { + cbPKT_SYSINFO pkt = ccf_data.isSysInfo; + pkt.cbpkt_header.type = cbPKTTYPE_SYSSETSPKLEN; + pushPacket(packets, pkt); + } + + // 8. LNC (cbPKTTYPE_LNCSET) + if (ccf_data.isLnc.cbpkt_header.type != 0) + { + cbPKT_LNC pkt = ccf_data.isLnc; + pkt.cbpkt_header.type = cbPKTTYPE_LNCSET; + pushPacket(packets, pkt); + } + + // 9. Waveforms (cbPKTTYPE_WAVEFORMSET) + for (int i = 0; i < AOUT_NUM_GAIN_CHANS; ++i) + { + for (int j = 0; j < cbMAX_AOUT_TRIGGER; ++j) + { + if (ccf_data.isWaveform[i][j].chan != 0) + { + cbPKT_AOUT_WAVEFORM pkt = ccf_data.isWaveform[i][j]; + pkt.cbpkt_header.type = cbPKTTYPE_WAVEFORMSET; + pkt.active = 0; + pushPacket(packets, pkt); + } + } + } + + // 10. NTrodes (cbPKTTYPE_SETNTRODEINFO) + for (int i = 0; i < cbMAXNTRODES; ++i) + { + if (ccf_data.isNTrodeInfo[i].ntrode != 0) + { + cbPKT_NTRODEINFO pkt = ccf_data.isNTrodeInfo[i]; + pkt.cbpkt_header.type = cbPKTTYPE_SETNTRODEINFO; + pushPacket(packets, pkt); + } + } + + // 11. Adaptive filter (cbPKTTYPE_ADAPTFILTSET) + if (ccf_data.isAdaptInfo.cbpkt_header.type != 0) + { + cbPKT_ADAPTFILTINFO pkt = ccf_data.isAdaptInfo; + pkt.cbpkt_header.type = cbPKTTYPE_ADAPTFILTSET; + pushPacket(packets, pkt); + } + + // 12. SS Status (cbPKTTYPE_SS_STATUSSET) — set ADAPT_NEVER to prevent rebuild + if (ccf_data.isSS_Status.cbpkt_header.type != 0) + { + cbPKT_SS_STATUS pkt = ccf_data.isSS_Status; + pkt.cbpkt_header.type = cbPKTTYPE_SS_STATUSSET; + pkt.cntlNumUnits.nMode = ADAPT_NEVER; + pushPacket(packets, pkt); + } + + return packets; +} + +} // namespace ccf diff --git a/src/ccfutils/src/platform_first.h b/src/ccfutils/src/platform_first.h new file mode 100644 index 00000000..dbe7d5ff --- /dev/null +++ b/src/ccfutils/src/platform_first.h @@ -0,0 +1,32 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file platform_first.h +/// @author CereLink Development Team +/// @date 2025-01-15 +/// +/// @brief Platform headers that MUST be included before cbproto headers +/// +/// IMPORTANT: This header must be included FIRST in any source file that uses both +/// Windows headers and cbproto headers. +/// +/// Reason: cbproto uses #pragma pack(1) for network structures. Windows SDK headers +/// (specifically winnt.h) have a static_assert that fails if packing is not at the +/// default setting when they are included. This header ensures Windows headers are +/// processed before any pack directives are active. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CCFUTILS_PLATFORM_FIRST_H +#define CCFUTILS_PLATFORM_FIRST_H + +#ifdef _WIN32 + // Windows headers must be included before cbproto headers due to packing requirements + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif + +#endif // CCFUTILS_PLATFORM_FIRST_H diff --git a/src/central/Instrument.cpp b/src/central/Instrument.cpp deleted file mode 100755 index d9a93354..00000000 --- a/src/central/Instrument.cpp +++ /dev/null @@ -1,411 +0,0 @@ -// =STS=> Instrument.cpp[1728].aa15 open SMID:15 -////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003-2008 Cyberkinetics, Inc. -// (c) Copyright 2008 - 2021 Blackrock Microsystems -// -// $Workfile: Instrument.cpp $ -// $Archive: /Cerebus/Human/WindowsApps/Central/Instrument.cpp $ -// $Revision: 5 $ -// $Date: 4/26/05 2:57p $ -// $Author: Kkorver $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////// - -#include "StdAfx.h" -#include // Use C++ default min and max implementation. -#include "debugmacs.h" -#include "Instrument.h" - -// table of starting ip addresses for each NSP (128 channels) -// indexed by NSP number: -#define RANGESIZE 16 - -const char g_NSP_IP[cbMAXOPEN][16] = -{ NSP_IN_ADDRESS, - "192.168.137.17", - "192.168.137.33", - "192.168.137.49" -}; - - -////////////////////////////////////////////////////////////////////// -// Construction/Destruction -////////////////////////////////////////////////////////////////////// - -Instrument::Instrument() : - m_nInPort(NSP_IN_PORT), m_nOutPort(NSP_OUT_PORT), - m_szInIP(NSP_IN_ADDRESS), m_szOutIP(NSP_OUT_ADDRESS), m_nRange(RANGESIZE) -{ -} - -Instrument::~Instrument() -{ - -} - -// Author & Date: Ehsan Azar 15 May 2012 -// Purpose: Set networking parameters to overwrite default addresses -// Note: must be called before Open for parameters to work -// Inputs: -// nProt - the networking protocol to use - 0 = UDP; 1 = TCP -// nInPort - the input port through which instrument connects to me -// nOutPort - the output port to connect to in order to connect to the instrument -// szInIP - Cerebus IP address -// szOutIP - Instrument IP address -void Instrument::SetNetwork(int nProtocol, int nInPort, int nOutPort, LPCSTR szInIP, LPCSTR szOutIP, int nRange) -{ - m_nProtocol = nProtocol; - m_nInPort = nInPort; - m_nOutPort = nOutPort; - m_szInIP = szInIP; - m_szOutIP = szOutIP; - m_nRange = nRange == 0 ? RANGESIZE : nRange; -} - -// Author & Date: Tom Richins 2 May 2011 -// Purpose: Open a network socket to the NSP instrument allowing for multiple NSPs -// If NSP is not detected, based on nStartupOptionsFlags, other addresses are tried -// Inputs: -// nStartupOptionsFlags - the network startup option -// nNSPnum - index of nsp -// Outputs: -// Returns the error code (0 means success) -cbRESULT Instrument::OpenNSP(STARTUP_OPTIONS nStartupOptionsFlags, uint16_t nNSPnum) -{ - m_szInIP = g_NSP_IP[nNSPnum < 4 ? nNSPnum : 0]; - return Open(nStartupOptionsFlags); -} - - -// Author & Date: Ehsan Azar 12 March 2010 -// Purpose: Open a network socket to the NSP instrument -// If NSP is not detected, based on nStartupOptionsFlags, other addresses are tried -// Inputs: -// nStartupOptionsFlags - the network startup option -// bBroadcast - establish a broadcast socket -// bDontRoute - establish a direct socket with no routing or gateway -// bNonBlocking - establish a non-blocking socket -// nRecBufSize - the system receive-buffer size allocated for this socket -// Outputs: -// Returns the error code (0 means success) -cbRESULT Instrument::Open(STARTUP_OPTIONS nStartupOptionsFlags, bool bBroadcast, bool bDontRoute, bool bNonBlocking, int nRecBufSize) -{ - int nRange= m_nRange; - int nInPort = m_nInPort; - int nOutPort = m_nOutPort; - LPCSTR szInIP = m_szInIP; - LPCSTR szOutIP = m_szOutIP; - - bool bVerbose = false; - if ((OPT_ANY_IP == nStartupOptionsFlags) || (OPT_LOOPBACK == nStartupOptionsFlags)) - { - bVerbose = true; // We want to see the transactions - nRange = 130; -#ifdef WIN32 - AllocConsole(); -#else - // Do nothing. _cprintf is defined as printf -#endif - } - if ((OPT_LOCAL == nStartupOptionsFlags) || (OPT_LOOPBACK == nStartupOptionsFlags)) - { - // If local instrument then connect to it - szInIP = LOOPBACK_ADDRESS; - szOutIP = LOOPBACK_BROADCAST; - nInPort = NSP_IN_PORT; - nOutPort = NSP_OUT_PORT; - } - - if (PROTOCOL_UDP == m_nProtocol) - { - return m_icUDP.OpenUDP(nStartupOptionsFlags, m_nRange, bVerbose, szInIP, szOutIP, - bBroadcast, bDontRoute, bNonBlocking, nRecBufSize, nInPort, nOutPort, cbCER_UDP_SIZE_MAX); - } - else - { - return m_icUDP.OpenTCP(nStartupOptionsFlags, m_nRange, bVerbose, szInIP, szOutIP, - bBroadcast, bDontRoute, bNonBlocking, nRecBufSize, nInPort, nOutPort, cbCER_UDP_SIZE_MAX); - } -} - -int Instrument::Send(void *ppkt) -{ - uint32_t quadlettotal = (((cbPKT_GENERIC*)ppkt)->cbpkt_header.dlen) + cbPKT_HEADER_32SIZE; - uint32_t cbSize = quadlettotal << 2; // number of bytes - - - CachedPacket * pEnd = ARRAY_END(m_aicCache); - CachedPacket * pCache; - // Find the first location that is open - for (pCache = m_aicCache; pCache != pEnd; pCache++) { - if (pCache->OkToSend()) - break; - } - - if (pCache == pEnd) - return -1; - pCache->AddPacket(ppkt, cbSize); - - if (PROTOCOL_UDP == m_nProtocol) - return m_icUDP.SendUDP(ppkt, cbSize); - else - return m_icUDP.SendTCP(ppkt, cbSize); -} - -int Instrument::Recv(void * packet) -{ - if (PROTOCOL_UDP == m_nProtocol) - return m_icUDP.RecvUDP(packet); - else - return m_icUDP.RecvTCP(packet); -} - - -// What is our current send mode? -Instrument::ModeType Instrument::GetSendMode() -{ - ModeType ret = static_cast(0); - for (CachedPacket * pCache = m_aicCache; pCache != ARRAY_END(m_aicCache); ++pCache) - { - ret = std::max(ret, pCache->GetMode()); - } - return ret; -} - - -// A packet has come in... -void Instrument::TestForReply(void * pPacket) -{ - // Give everyone a chance to see if this is an important packet - for (CachedPacket * pCache = m_aicCache; pCache != ARRAY_END(m_aicCache); ++pCache) - { - pCache->CheckForReply(pPacket); - } -} - - -void Instrument::Close() -{ - m_icUDP.Close(); -} - - -void Instrument::Standby() -{ - cbPKT_SYSINFO pktsysinfo; - pktsysinfo.cbpkt_header.time = 0; - pktsysinfo.cbpkt_header.chid = 0x8000; - pktsysinfo.cbpkt_header.type = cbPKTTYPE_SYSSETRUNLEV; - pktsysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; - pktsysinfo.runlevel = cbRUNLEVEL_HARDRESET; - Send(&pktsysinfo); - return; -} - - -void Instrument::Shutdown() -{ - cbPKT_SYSINFO pktsysinfo; - pktsysinfo.cbpkt_header.time = 0; - pktsysinfo.cbpkt_header.chid = 0x8000; - pktsysinfo.cbpkt_header.type = cbPKTTYPE_SYSSETRUNLEV; - pktsysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; - pktsysinfo.runlevel = cbRUNLEVEL_SHUTDOWN; - Send(&pktsysinfo); - return; -} - - -// Author & Date: Kirk Korver 17 Jun 2003 -// Purpose: receive a buffer from the "loopback" area. In other words, get it from our -// local buffer. It wasn't really sent -// Inputs: -// pBuffer - Where to stuff the packet -// pPacketData - the packet put put in this location -// Outputs: -// the number of bytes read, or 0 if no data was found -int Instrument::LoopbackRecvLow(void * pBuffer, void * pPacketData, uint32_t nInstance) -{ - uint32_t nIdx = cb_library_index[nInstance]; - - cbPKT_GENERIC * pPacket = static_cast(pPacketData); - - // find the length of the packet - const uint32_t quadlettotal = (pPacket->cbpkt_header.dlen) + cbPKT_HEADER_32SIZE; - const int cbSize = quadlettotal << 2; // How many bytes are there - - // copy the packet - memcpy(pBuffer, pPacket, cbSize); - - // complete the packet processing by clearing the packet from the xmt buffer - memset(pPacket, 0, cbSize); - cb_xmt_local_buffer_ptr[nIdx]->transmitted++; - cb_xmt_local_buffer_ptr[nIdx]->tailindex += quadlettotal; - - if ((cb_xmt_local_buffer_ptr[nIdx]->tailindex) > (cb_xmt_local_buffer_ptr[nIdx]->bufferlen - (cbCER_UDP_SIZE_MAX / 4))) - { - cb_xmt_local_buffer_ptr[nIdx]->tailindex = 0; - } - return cbSize; -} - - -// Called every 10 ms...use for "resending" -// Outputs: -// TRUE if any instrument error has happened; FALSE otherwise -bool Instrument::Tick() -{ - // What is the end? - CachedPacket * pEnd = ARRAY_END(m_aicCache); - CachedPacket * pFound; - // Find the first case where {item}.Tick(m_icUDP) == true - for (pFound = m_aicCache; pFound != pEnd; pFound++) - { - if (pFound->Tick(m_icUDP, m_nProtocol)) - break; - } - - // If I've reached the end, then none are true - return pFound != pEnd; -} - -void Instrument::Reset(int nMaxTickCount, int nMaxRetryCount) -{ - // Call everybody's reset member function - CachedPacket * pCache; - for (pCache = m_aicCache; pCache != ARRAY_END(m_aicCache); pCache++) - { - pCache->Reset(nMaxTickCount, nMaxRetryCount); - } -} - - -// Author & Date: Kirk Korver 05 Jan 2003 -// Purpose: Is it OK to send a new packet out? -// Outputs: -// TRUE if it is OK to send; FALSE if not -bool Instrument::OkToSend() -{ - CachedPacket * pEnd = ARRAY_END(m_aicCache); - CachedPacket * pFound; - // Find the first location that is open - for (pFound = m_aicCache; pFound != pEnd; pFound++) - { - if (pFound->OkToSend()) - break; - } - - return (pFound != pEnd); -} - - -Instrument::CachedPacket::CachedPacket() -{ - Reset(); -} - -// Stop trying to send packets and restart -void Instrument::CachedPacket::Reset(int nMaxTickCount, int nMaxRetryCount) -{ - m_enSendMode = MT_OK_TO_SEND; // What is our current send mode? - m_nTickCount = m_nMaxTickCount = nMaxTickCount; // How many ticks have passed since we sent? - m_nRetryCount = m_nMaxRetryCount = nMaxRetryCount; // How many times have we re-sent this packet? -} - -// Save this packet -bool Instrument::CachedPacket::AddPacket(void * pPacket, int cbBytes) -{ - ASSERT((unsigned int)cbBytes <= sizeof(m_abyPacket)); - memcpy(m_abyPacket, pPacket, cbBytes); - m_cbPacketBytes = cbBytes; - - - m_enSendMode = MT_WAITING_FOR_REPLY; - m_nTickCount = m_nMaxTickCount; // How many ticks have passed since we sent? - m_nRetryCount = m_nMaxRetryCount; // How many times have we re-sent this packet? - -// TRACE("Outgoing Pkt Type: 0x%2X\n", ((cbPKT_GENERIC*)pPacket)->cbpkt_header.type); - return true; -} - -// A packet came in, compare to see if necessary -void Instrument::CachedPacket::CheckForReply(void * pPacket) -{ - if (m_enSendMode == MT_WAITING_FOR_REPLY) - { - cbPKT_GENERIC * pIn = static_cast(pPacket); - cbPKT_GENERIC * pOut = reinterpret_cast(m_abyPacket); - - /* - DEBUG_CODE - ( - // If it is a "hearbeat" packet, then just don't look at it - if (pIn->cbpkt_header.chid == 0x8000 && pIn->cbpkt_header.type == 0) - return; - - TRACE("Incoming Pkt chid: 0x%04X type: 0x%02X, SENT chid: 0x%04X, type: 0x%02X (type: 0x%02X), chan: %d\n", - pIn->cbpkt_header.chid, pIn->cbpkt_header.type, - pOut->cbpkt_header.chid, pOut->cbpkt_header.type, pOut->cbpkt_header.type & ~0x80, - ((cbPKT_CHANINFO *)(pOut))->chan ); - - ); - */ - - if (pIn->cbpkt_header.type != (pOut->cbpkt_header.type & ~0x80)) // mask off the highest bit - return; - - if (pIn->cbpkt_header.chid != pOut->cbpkt_header.chid) - return; - - // If this is a "configuration type packet" - // The logic works because Chanset is 0xC0 and all config packets are 0xC? - // It will also work out because the 0xD0 family of packets will come in here - // and their 1st value is channel. - if ((pOut->cbpkt_header.type & cbPKTTYPE_CHANSET) == cbPKTTYPE_CHANSET) - { - if (pOut->cbpkt_header.dlen) - { - cbPKT_CHANINFO * pChanIn = static_cast(pPacket); - cbPKT_CHANINFO * pChanOut = reinterpret_cast(m_abyPacket); - - if (pChanIn->chan != pChanOut->chan) - return; - } - } - - // If we get this far, then we must have a match - m_enSendMode = MT_OK_TO_SEND; - } -} - - -// Called every 10 ms...use for "resending" -// Outputs: -// TRUE if any instrument error has happened; FALSE otherwise -bool Instrument::CachedPacket::Tick(const UDPSocket& rcUDP, int nProtocol) -{ - if (m_enSendMode == MT_WAITING_FOR_REPLY) - { - if (--m_nTickCount <= 0) // If time to resend - { - if (--m_nRetryCount > 0) // If not too many retries - { - TRACE("********************Resending packet******************* type: 0x%02X\n", ((cbPKT_GENERIC*)m_abyPacket)->cbpkt_header.type); - if (PROTOCOL_UDP == nProtocol) - rcUDP.SendUDP(m_abyPacket, m_cbPacketBytes); - else - rcUDP.SendTCP(m_abyPacket, m_cbPacketBytes); - m_nTickCount = m_nMaxTickCount; - } - else - { - // true means that we have an error here - return true; - } - } - } - return false; -} diff --git a/src/central/Instrument.h b/src/central/Instrument.h deleted file mode 100755 index 4239d434..00000000 --- a/src/central/Instrument.h +++ /dev/null @@ -1,168 +0,0 @@ -/* =STS=> Instrument.h[1729].aa09 open SMID:10 */ -////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003 - 2008 Cyberkinetics, Inc. -// (c) Copyright 2008 - 2021 Blackrock Microsystems, LLC -// -// $Workfile: Instrument.h $ -// $Archive: /Cerebus/WindowsApps/Central/Instrument.h $ -// $Revision: 4 $ -// $Date: 1/05/04 4:36p $ -// $Author: Kkorver $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////// - -#ifndef INSTRUMENT_H_INCLUDED -#define INSTRUMENT_H_INCLUDED - -#if _MSC_VER > 1000 -#pragma once -#endif // _MSC_VER > 1000 - -#include "../include/cerelink/cbhwlib.h" -#include "UDPsocket.h" -#include "cki_common.h" - -#define NSP_IN_ADDRESS cbNET_UDP_ADDR_INST // NSP default address -#define NSP_OUT_ADDRESS cbNET_UDP_ADDR_CNT // NSP subnet default address -#define NSP_IN_PORT cbNET_UDP_PORT_BCAST // Neuroflow Data Port -#define NSP_OUT_PORT cbNET_UDP_PORT_CNT // Neuroflow Control Port -#define NSP_REC_BUF_SIZE (4096 * 2048) // Receiving system buffer size (multiple of 4096) - -class Instrument -{ -public: - Instrument(); - virtual ~Instrument(); - - // Open the NSP instrument - cbRESULT OpenNSP(STARTUP_OPTIONS nStartupOptionsFlags, uint16_t nNSPnum); - cbRESULT Open(STARTUP_OPTIONS nStartupOptionsFlags, bool bBroadcast = false, bool bDontRoute = true, - bool bNonBlocking = true, int nRecBufSize = NSP_REC_BUF_SIZE); - void SetNetwork(int nProtocol, int nInPort, int nOutPort, LPCSTR szInIP, LPCSTR szOutIP, int nRange); - - void Close(); - void Standby(); - void Shutdown(); - void TestForReply(void * pPacket); - - enum { TICK_COUNT = 15 }; // Default number of ticks to wait for a response ( 10ms per tick) - enum { RESEND_COUNT = 10 }; // Default number of times to "resend" a packet before erroring out. - void Reset(int nMaxTickCount = TICK_COUNT, int nMaxRetryCount = RESEND_COUNT); - - - // Called every 10 ms...use for "resending" - // Outputs: - // TRUE if any instrument error has happened; FALSE otherwise - bool Tick(void); - - - enum ModeType - { - MT_OK_TO_SEND, // It is OK to send out a packet - MT_WAITING_FOR_REPLY, // We are waiting for the "response" - }; - ModeType GetSendMode(); // What is our current send mode? - - - - int Recv(void * packet); - - int Send(void *ppkt); // Send this packet out - - // Is it OK to send a new packet out? - bool OkToSend(); - - - // Purpose: receive a buffer from the "loopback" area. In other words, get it from our - // local buffer. It wasn't really sent - // Inputs: - // pBuffer - Where to stuff the packet - // Outputs: - // the number of bytes read, or 0 if no data was found - int LoopbackRecv(void * pBuffer, uint32_t nInstance = 0) - { - uint32_t nIdx = cb_library_index[nInstance]; - - // The logic here is quite complicated. Data is filled in from other processes - // in a 2 pass mode. First they fill all except they skip the first 4 bytes. - // The final step in the process is to convert the 1st dword from "0" to some other number. - // This step is done in a thread-safe manner - // Consequently, all packets can not have "0" as the first DWORD. At the time of writing, - // We were looking at the "time" value of a packet. - if (cb_xmt_local_buffer_ptr[nIdx]->buffer[cb_xmt_local_buffer_ptr[nIdx]->tailindex] != 0) - { - cbPKT_GENERIC * pPacket = (cbPKT_GENERIC*)&(cb_xmt_local_buffer_ptr[nIdx]->buffer[cb_xmt_local_buffer_ptr[nIdx]->tailindex]); - return LoopbackRecvLow(pBuffer, pPacket, nInstance); - } - return 0; - } - -protected: - UDPSocket m_icUDP; // Socket to deal with the sending of the UDP packets - - // Purpose: receive a buffer from the "loopback" area. In other words, get it from our - // local buffer. It wasn't really sent. We assume that there is enough memory for - // all of the memory copies that take place - // Inputs: - // pBuffer - Where to stuff the packet - // pPacketData - the packet put put in this location - // Outputs: - // the number of bytes read, or 0 if no data was found - int LoopbackRecvLow(void * pBuffer, void * pPacketData, uint32_t nInstance = 0); - - - class CachedPacket - { - public: - CachedPacket(); - ModeType GetMode() { return m_enSendMode; } - - void Reset(int nMaxTickCount = TICK_COUNT, int nMaxRetryCount = RESEND_COUNT); // Stop trying to send packets and restart - bool AddPacket(void * pPacket, int cbBytes); // Save this packet, of this size - void CheckForReply(void * pPacket); // A packet came in, compare to see if necessary - bool OkToSend() { return m_enSendMode == MT_OK_TO_SEND; } - - - // Called every 10 ms...use for "resending" - // Outputs: - // TRUE if any instrument error has happened; FALSE otherwise - bool Tick(const UDPSocket& rcUDP, int nProtocol); - - protected: - - ModeType m_enSendMode; // What is our current send mode? - - int m_nTickCount; // How many ticks have passed since we sent? - int m_nRetryCount; // How many times have we re-sent this packet? - int m_nMaxTickCount; // How many ticks to wait for a response ( 10ms per tick) - int m_nMaxRetryCount; // How many times to "resend" a packet before erroring out. - - // This array MUST be larger than the largest data packet - char m_abyPacket[cbPKT_MAX_SIZE * 2]; // This is the packet that was sent out. - int m_cbPacketBytes; // The size of the most recent packet addition - - }; - - - enum { NUM_OF_PACKETS_CACHED = 6 }; - CachedPacket m_aicCache[NUM_OF_PACKETS_CACHED]; - -private: - int m_nProtocol; - int m_nInPort; - int m_nOutPort; - LPCSTR m_szInIP; - LPCSTR m_szOutIP; - int m_nRange; -}; - - - -extern Instrument g_icInstrument; // The one and only instrument - - - -#endif // include guard diff --git a/src/central/UDPsocket.cpp b/src/central/UDPsocket.cpp deleted file mode 100755 index 8ec5b7d3..00000000 --- a/src/central/UDPsocket.cpp +++ /dev/null @@ -1,610 +0,0 @@ -// =STS=> UDPsocket.cpp[1732].aa11 open SMID:11 -////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003 - 2008 Cyberkinetics, Inc. -// (c) Copyright 2008 - 2017 Blackrock Microsystems, LLC -// -// $Workfile: UDPsocket.cpp $ -// $Archive: /Cerebus/WindowsApps/Central/UDPsocket.cpp $ -// $Revision: 1 $ -// $Date: 1/05/04 4:29p $ -// $Author: Kkorver $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////// -#include "StdAfx.h" -#include "debugmacs.h" -#include "UDPsocket.h" -#ifdef WIN32 -#include -typedef int socklen_t; -#else -#include -#include -#include -#include -#include -typedef struct sockaddr SOCKADDR; -#define INVALID_SOCKET -1 -#define SOCKET_ERROR -1 -#define FAR -#define SD_BOTH SHUT_RDWR -#endif - - -////////////////////////////////////////////////////////////////////// -// Construction/Destruction -////////////////////////////////////////////////////////////////////// - -UDPSocket::UDPSocket() : - m_nStartupOptionsFlags(OPT_NONE), m_bVerbose(false), m_TCPconnected(false) -{ - inst_sock = INVALID_SOCKET; -} - -UDPSocket::~UDPSocket() -{ - if (inst_sock != INVALID_SOCKET) - Close(); -} - -// Author & Date: Ehsan Azar 12 March 2010 -// Purpose: Open a network UDP socket -// Inputs: -// nStartupOptionsFlags - the network startup option -// nRange - the maximum allowed increments to the given IP address of the instrument to automatically connect to -// bVerbose - verbose mode -// szInIP - the IP of the instrument through which connects to me -// szOutIP - the IP of the instrument to connect to (it could be a subnet) -// bBroadcast - establish a broadcast socket -// bDontRoute - establish a direct socket with no routing or gateway -// bNonBlocking - establish a non-blocking socket -// nRecBufSize - the system receive-buffer size allocated for this socket -// nInPort - the input port through which instrument connects to me -// nOutPort - the output port to connect to in order to connect to the instrument -// nPacketSize - the maximum packet size that we receive -// Outputs: -// Returns the error code (0 means success) -cbRESULT UDPSocket::OpenUDP(STARTUP_OPTIONS nStartupOptionsFlags, int nRange, bool bVerbose, LPCSTR szInIP, - LPCSTR szOutIP, bool bBroadcast, bool bDontRoute, bool bNonBlocking, - int nRecBufSize, int nInPort, int nOutPort, int nPacketSize) -{ - m_bVerbose = bVerbose; - m_nPacketSize = nPacketSize; - m_nStartupOptionsFlags = nStartupOptionsFlags; - -#ifdef WIN32 - // Initialize Winsock 2.2 - WSADATA data; - if (WSAStartup (MAKEWORD(2,0), &data) != 0) - return cbRESULT_SOCKERR; -#endif - - // Create Socket for Receiving the Data Stream -#ifdef WIN32 - inst_sock = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, NULL, 0, 0); -#else - inst_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); -#endif - if (inst_sock == INVALID_SOCKET) - { - Close(); - return cbRESULT_SOCKERR; - } - - int opt_one = 1; - - if (bBroadcast) - { - if (setsockopt(inst_sock, SOL_SOCKET, SO_BROADCAST, (char*)&opt_one, sizeof(opt_one)) != 0) - { - Close(); - return cbRESULT_SOCKOPTERR; - } - } - - if (bDontRoute) - { - if (setsockopt(inst_sock, SOL_SOCKET, SO_DONTROUTE, (char*)&opt_one, sizeof(opt_one)) != 0) - { - Close(); - return cbRESULT_SOCKOPTERR; - } - } - - if (OPT_REUSE == nStartupOptionsFlags) - { - if (setsockopt(inst_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt_one, sizeof(opt_one)) != 0) - { - Close(); - return cbRESULT_SOCKOPTERR; - } - } - - if (nRecBufSize > 0) - { - // Set the data stream input buffer size - int data_buff_size = nRecBufSize; - if (setsockopt(inst_sock, SOL_SOCKET, SO_RCVBUF, (char*)&data_buff_size, sizeof(data_buff_size)) != 0) - { - Close(); -#ifdef __APPLE__ - return cbRESULT_SOCKMEMERR; -#else - return cbRESULT_SOCKOPTERR; -#endif - } - socklen_t opt_len = sizeof(int); - if (getsockopt(inst_sock, SOL_SOCKET, SO_RCVBUF, (char *)&data_buff_size, &opt_len) != 0) - { - Close(); - return cbRESULT_SOCKOPTERR; - } -#ifdef __linux__ - // Linux returns double the requested size up to twice the rmem_max - data_buff_size /= 2; -#endif - if (data_buff_size < nRecBufSize) - { - // to increase buffer - // sysctl -w net.core.rmem_max=8388608 - // or - // nvram boot-args="ncl=65536" - // sysctl -w kern.ipc.maxsockbuf=8388608 - Close(); - return cbRESULT_SOCKMEMERR; - } - } - - if (bNonBlocking) - { - // Set the data socket to non-blocking operation -#ifdef WIN32 - u_long arg_val = 1; - if (ioctlsocket(inst_sock, FIONBIO, &arg_val) == SOCKET_ERROR) -#else - if (fcntl(inst_sock, F_SETFL, O_NONBLOCK)) -#endif - { - Close(); - return cbRESULT_SOCKOPTERR; - } - } - - // Attempt to bind Data Stream Socket to lowest address in range 192.168.137.1 to XXX.16 - bool socketbound = false; - SOCKADDR_IN inst_sockaddr; - memset(&inst_sockaddr, 0, sizeof(inst_sockaddr)); - - inst_sockaddr.sin_family = AF_INET; - inst_sockaddr.sin_port = htons(nInPort); // Neuroflow Data Port -#ifdef __APPLE__ - inst_sockaddr.sin_len = sizeof(inst_sockaddr); -#endif - if (szInIP == 0) - inst_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); - else - inst_sockaddr.sin_addr.s_addr = inet_addr(szInIP); - - int nCount = 1; - do - { - if (bind(inst_sock, (struct sockaddr FAR *)&inst_sockaddr, sizeof(inst_sockaddr)) == 0) - socketbound = true; - else - { - // Extract the Host ID of the instrument network IP address - std::string ipStr(szInIP); - size_t lastDot = ipStr.rfind('.'); - int iHostID = 0; - if (lastDot != std::string::npos) { - std::string szHostID = ipStr.substr(lastDot + 1); - iHostID = std::stoi(szHostID); - } - else - { - return cbRESULT_INSTINVALID; - } - // increment the Host ID number of IP address if legacy and decrement the ID number if Gemini - if (validHostIP_Gemini.count(iHostID)) - { - inst_sockaddr.sin_addr.s_addr = htonl(ntohl(inst_sockaddr.sin_addr.s_addr) - 1); // decrement IP by 1 - } - else if (validHostIP_Legacy.count(iHostID)) - { - inst_sockaddr.sin_addr.s_addr = htonl(ntohl(inst_sockaddr.sin_addr.s_addr) + 1); // increment IP by 1 - } - else - { - return cbRESULT_INSTINVALID; - } - } - nCount++; - } while( (!socketbound) && (nCount <= nRange)); - - if (socketbound) - { - // Set up transmission target address - dest_sockaddr.sin_family = AF_INET; - dest_sockaddr.sin_port = htons(nOutPort); // Neuroflow Control Port - dest_sockaddr.sin_addr.s_addr = inet_addr(szOutIP); // Subnet Broadcast - } - else - { - if (OPT_NONE == nStartupOptionsFlags) - { - // if no valid address was found to bind the socket, shut her down and return error - Close(); - return cbRESULT_SOCKBIND; - } - else - { - // if no valid address was found to bind the socket, just connect on any available - // interface - if (m_bVerbose) - _cprintf("Warning: could not bind to socket on the subnet...\n"); - - if (setsockopt(inst_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt_one, sizeof(opt_one)) != 0) - { - if(m_bVerbose) - _cprintf("Error enabling address re-used\n"); - Close(); - return cbRESULT_SOCKOPTERR; - } - - // Bind to the broadcast address - if (OPT_LOOPBACK == nStartupOptionsFlags) - inst_sockaddr.sin_addr.s_addr = inet_addr(LOOPBACK_ADDRESS); - else - inst_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); - - int result = bind(inst_sock, (struct sockaddr FAR *)&inst_sockaddr, sizeof(inst_sockaddr)); - - if (result == 0) - { - // Set up transmission target address - // - // Assume there's no cerebus subnet, so control packets should go to - // broadcast - dest_sockaddr.sin_family = AF_INET; - dest_sockaddr.sin_port = htons(nOutPort); // Neuroflow Control Port - - if (OPT_LOOPBACK == nStartupOptionsFlags) - dest_sockaddr.sin_addr.s_addr = inet_addr(LOOPBACK_BROADCAST); - else - dest_sockaddr.sin_addr.s_addr = htonl(INADDR_BROADCAST); - } else { // if we still can't bind, it's an error - Close(); - return cbRESULT_SOCKBIND; - } - } - } - if(m_bVerbose) { - _cprintf("Successfully initialized network socket, bound to %s:%d\n", - inet_ntoa(inst_sockaddr.sin_addr),(int)(ntohs(inst_sockaddr.sin_port))); - - _cprintf("Sending control packets to %s:%d\n", - inet_ntoa(dest_sockaddr.sin_addr),(int)(ntohs(dest_sockaddr.sin_port))); - } - return cbRESULT_OK; -} - -// Author & Date: Ehsan Azar 13 Aug 2010 -// Purpose: Change destination port number. -// Inputs: -// nOutPort - new port number -void UDPSocket::OutPort(int nOutPort) -{ - dest_sockaddr.sin_port = htons(nOutPort); -} - - -cbRESULT UDPSocket::OpenTCP(STARTUP_OPTIONS nStartupOptionsFlags, int nRange, bool bVerbose, LPCSTR szInIP, - LPCSTR szOutIP, bool bBroadcast, bool bDontRoute, bool bNonBlocking, - int nRecBufSize, int nInPort, int nOutPort, int nPacketSize) -{ - m_bVerbose = bVerbose; - m_nPacketSize = nPacketSize; - m_nStartupOptionsFlags = nStartupOptionsFlags; - -#ifdef WIN32 - // Initialize Winsock 2.2 - WSADATA data; - if (WSAStartup(MAKEWORD(2, 0), &data) != 0) - return cbRESULT_SOCKERR; -#endif - - // Create Socket -#ifdef WIN32 - inst_sock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, 0); -#else - inst_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); -#endif - if (inst_sock == INVALID_SOCKET) - { - Close(); - return cbRESULT_SOCKERR; - } - - if (nRecBufSize > 0) - { - // Set the data stream input buffer size - - int data_buff_size = nRecBufSize; - if (setsockopt(inst_sock, SOL_SOCKET, SO_RCVBUF, (char*)&data_buff_size, sizeof(data_buff_size)) != 0) - { - Close(); -#ifdef __APPLE__ - return cbRESULT_SOCKMEMERR; -#else - return cbRESULT_SOCKOPTERR; -#endif - } - - socklen_t opt_len = sizeof(int); - if (getsockopt(inst_sock, SOL_SOCKET, SO_RCVBUF, (char*)&data_buff_size, &opt_len) != 0) - { - Close(); - return cbRESULT_SOCKOPTERR; - } - else - { - TRACE("GOT RCVBUF %i\n", data_buff_size); - } -#ifdef __linux__ - // Linux returns double the requested size up to twice the rmem_max - data_buff_size /= 2; -#endif - if (data_buff_size < nRecBufSize) - { - // to increase buffer - // sysctl -w net.core.rmem_max=8388608 - // or - // nvram boot-args="ncl=65536" - // sysctl -w kern.ipc.maxsockbuf=8388608 - Close(); - return cbRESULT_SOCKMEMERR; - } - } - - int opt_sndbuf = nRecBufSize; - if (setsockopt(inst_sock, SOL_SOCKET, SO_SNDBUF, (char*)&opt_sndbuf, sizeof(opt_sndbuf)) != 0) - { - Close(); - return cbRESULT_SOCKOPTERR; - } - - // if (bDontRoute) - { - int opt_one = 1; - if (setsockopt(inst_sock, SOL_SOCKET, SO_DONTROUTE, (char*)&opt_one, sizeof(opt_one)) != 0) - { - Close(); - return cbRESULT_SOCKOPTERR; - } - } - - // if (bNonBlocking) - // { - // Set the data socket to non-blocking operation -#ifdef WIN32 - u_long arg_val = 1; - if (ioctlsocket(inst_sock, FIONBIO, &arg_val) == SOCKET_ERROR) -#else - if (fcntl(inst_sock, F_SETFL, O_NONBLOCK)) -#endif - { - Close(); - return cbRESULT_SOCKOPTERR; - } - // } - - // Attempt to connect to an existing Gemini server - bool socketbound = false; - - SOCKADDR_IN inst_sockaddr; - memset(&inst_sockaddr, 0, sizeof(inst_sockaddr)); - - inst_sockaddr.sin_family = AF_INET; - inst_sockaddr.sin_port = htons(nInPort); // Neuroflow Data Port - inst_sockaddr.sin_addr.s_addr = inet_addr(szInIP); -#ifdef __APPLE__ - inst_sockaddr.sin_len = sizeof(inst_sockaddr); -#endif - - int err = connect(inst_sock, (struct sockaddr FAR*) & inst_sockaddr, sizeof(inst_sockaddr)); - - if (err == 0) - { - Close(); - return cbRESULT_SOCKOPTERR; - } - - if (m_bVerbose) { - _cprintf("Successfully initialized TCP network socket, bound to %s:%d\n", - inet_ntoa(inst_sockaddr.sin_addr), (int)(ntohs(inst_sockaddr.sin_port))); - - _cprintf("Sending control packets to %s:%d\n", - inet_ntoa(dest_sockaddr.sin_addr), (int)(ntohs(dest_sockaddr.sin_port))); - } - - m_TCPconnected = true; - - return cbRESULT_OK; -} - - -void UDPSocket::Close() -{ - m_TCPconnected = false; - - // Shutdown the socket gracefully - shutdown(inst_sock, SD_BOTH); - - // Close the socket -#ifdef WIN32 - closesocket(inst_sock); - WSACleanup(); -#else - close(inst_sock); -#endif - inst_sock = INVALID_SOCKET; -} - -int UDPSocket::RecvUDP(void * packet) const -{ - int ret = recv(inst_sock, (char*)packet, m_nPacketSize, 0); - - if (ret != SOCKET_ERROR) - return ret; // This is actual size returned - else - { - int err = 0; -#ifdef WIN32 - err = ::WSAGetLastError(); - if (err == WSAEWOULDBLOCK) - return 0; - TRACE("Socket Recv error was %i\n", err); -#else - err = errno; - if (err == EAGAIN) - return 0; - TRACE("Socket Recv error was %i\n", err); -#endif - return 0; - } -} - - -int UDPSocket::RecvTCP(void* packet) const -{ - while (1) - { - int err = 0; - int ret = recv(inst_sock, (char*)packet, m_nPacketSize, 0); -#ifdef WIN32 - err = ::WSAGetLastError(); -#else - err = errno; -#endif - - if (ret != SOCKET_ERROR) - { - TRACE("TCP rcv - packet read %i, (error) %i\n", ret, err); - // return ret; // This is actual size returned - } - else - { - //return 0; - } - } - - return 0; - - // static unsigned __int16 pkt_length = 0xFFFF; - // int ret, err; - // - // if(pkt_length == 0xFFFF) - // { - // ret = recv(inst_sock, (char*)&pkt_length, sizeof(pkt_length), 0); - // err = ::WSAGetLastError(); - // - //// TRACE("TCP rcv pkt size rcv - ret %i, pkt length %i\n",ret, pkt_length); - // - // return 0; - //// if (ret == SOCKET_ERROR) // Identify if we received data or if the socket is in error - //// { - //// - ////// TRACE("TCP rcv - error getting length %i\n", err); // The socket was in error - return the error code - //// if (err == WSAEWOULDBLOCK) // The socket was empty - return - //// return 0; - //// - //// return -1; - //// } - //// - //// if (ret != sizeof(pkt_length) || pkt_length > m_nPacketSize) // Do a second level check to understand if the packet size is legit - //// { - ////// TRACE("TCP Socket Recv error %i\n", ret); - //// return -1; - //// } - // } - // - // ret = recv(inst_sock, (char*)packet, pkt_length, 0); - // err = ::WSAGetLastError(); - // - //// TRACE("TCP rcv - packet received %i\n", ret); - // - // if(ret == SOCKET_ERROR) - // { - //// TRACE("TCP rcv - packet read %i, (error) %i\n", ret, err); - // - // if (err == WSAEWOULDBLOCK) // The socket was empty - return - // return 0; - // - // return -1; - // } - // - // if (ret == pkt_length) - // { - // // TRACE("TCP rcv - packet properly received %i\n", ret); - // pkt_length = 0xFFFF; - // return ret; // This is actual size returned - // } - // else - // { - // TRACE("TCP rcv - packet received does not have the right length\n"); - // return -1; - // } -} - - -int UDPSocket::SendUDP(void *ppkt, int cbBytes) const -{ - int sendRet = sendto(inst_sock, (const char *)ppkt, cbBytes, 0, - (SOCKADDR*)&dest_sockaddr, sizeof(dest_sockaddr)); -#ifdef WIN32 - DEBUG_CODE - ( - if (sendRet == SOCKET_ERROR) { - TRACE("Socket Send error was %i\n", ::WSAGetLastError()); - } - ) -#else - DEBUG_CODE - ( - if (sendRet == SOCKET_ERROR) { - TRACE("Socket Send error was %i\n", errno); - } - ) -#endif - - return sendRet; - -} - - -int UDPSocket::SendTCP(void* ppkt, int cbBytes) const -{ - - TRACE("Send TCP pkt type: 0x%02X\n", ((cbPKT_GENERIC*)ppkt)->cbpkt_header.type); - - int sendRet = send(inst_sock, (const char*)ppkt, cbBytes, 0); - //#ifdef WIN32 - // DEBUG_CODE - // ( - if (sendRet == SOCKET_ERROR) { - // TRACE("Socket Send error was %i\n", ::WSAGetLastError()); - } - // ) - //#else - // DEBUG_CODE - // ( - // if (sendRet == SOCKET_ERROR) { - // TRACE("Socket Send error was %i\n", errno); - // } - // ) - //#endif - - return sendRet; -} diff --git a/src/central/UDPsocket.h b/src/central/UDPsocket.h deleted file mode 100755 index 4404f9a8..00000000 --- a/src/central/UDPsocket.h +++ /dev/null @@ -1,87 +0,0 @@ -/* =STS=> UDPsocket.h[1733].aa07 open SMID:7 */ -////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2003 Cyberkinetics, Inc. -// -// $Workfile: UDPsocket.h $ -// $Archive: /Cerebus/WindowsApps/Central/UDPsocket.h $ -// $Revision: 1 $ -// $Date: 1/05/04 4:29p $ -// $Author: Kkorver $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////// - -#ifndef UDPSOCKET_H_INCLUDED -#define UDPSOCKET_H_INCLUDED - -#if _MSC_VER > 1000 -#pragma once -#endif // _MSC_VER > 1000 - - -#ifdef WIN32 -#include -#include -#else -#ifndef __APPLE__ -#include -#endif -#include -#include -#include -typedef struct sockaddr_in SOCKADDR_IN; -typedef int SOCKET; -#endif -#include -#include -#include "../include/cerelink/cbhwlib.h" -#include "cki_common.h" - -#define LOOPBACK_ADDRESS "127.0.0.1" -#define LOOPBACK_BROADCAST "127.0.0.1" - -class UDPSocket -{ -public: - UDPSocket(); - virtual ~UDPSocket(); - - // Open UDP socket - // Open Network socket (UDP version) - cbRESULT OpenUDP(STARTUP_OPTIONS nStartupOptionsFlags, int nRange, bool bVerbose, LPCSTR szInIP, - LPCSTR szOutIP, bool bBroadcast, bool bDontRoute, bool bNonBlocking, - int nRecBufSize, int nInPort, int nOutPort, int nPacketSize); - - // Open Network socket (TCP version) - cbRESULT OpenTCP(STARTUP_OPTIONS nStartupOptionsFlags, int nRange, bool bVerbose, LPCSTR szInIP, - LPCSTR szOutIP, bool bBroadcast, bool bDontRoute, bool bNonBlocking, - int nRecBufSize, int nInPort, int nOutPort, int nPacketSize); - - void OutPort(int nOutPort); - - void Close(); - - SOCKET GetSocket() {return inst_sock;} - - // Receive one packet if queued - int RecvUDP(void* packet) const; - int RecvTCP(void* packet) const; - - // Send this packet, it has cbBytes of length - int SendUDP(void* ppkt, int cbBytes) const; - int SendTCP(void* ppkt, int cbBytes) const; - -protected: - SOCKET inst_sock; // instrument socket for input - SOCKADDR_IN dest_sockaddr; - STARTUP_OPTIONS m_nStartupOptionsFlags; - int m_nPacketSize; // packet size - bool m_bVerbose; // verbose output - bool m_TCPconnected; - std::set validHostIP_Legacy = {1, 17, 33, 49}; // Legacy Host PC default Host IDs - std::set validHostIP_Gemini = {199, 183, 167, 151}; // Gemini Host PC default Host IDs -}; - -#endif // include guard diff --git a/src/compat/afxres.h b/src/compat/afxres.h deleted file mode 100644 index f43680a9..00000000 --- a/src/compat/afxres.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef _AFXRES_H -#define _AFXRES_H -#if __GNUC__ >= 3 -#pragma GCC system_header -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef _WINDOWS_H -#include -#endif - -#include "WinResrc.h" - -/* IDC_STATIC is documented in winuser.h, but not defined. */ -#ifndef IDC_STATIC -#define IDC_STATIC (-1) -#endif - -#ifdef __cplusplus -} -#endif -#endif - diff --git a/src/compat/pstdint.h b/src/compat/pstdint.h deleted file mode 100644 index 1a9d95be..00000000 --- a/src/compat/pstdint.h +++ /dev/null @@ -1,919 +0,0 @@ -/* A portable stdint.h - **************************************************************************** - * BSD License: - **************************************************************************** - * - * Copyright (c) 2005-2016 Paul Hsieh - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - **************************************************************************** - * - * Version 0.1.16.0 - * - * The ANSI C standard committee, for the C99 standard, specified the - * inclusion of a new standard include file called stdint.h. This is - * a very useful and long desired include file which contains several - * very precise definitions for integer scalar types that is critically - * important for making several classes of applications portable - * including cryptography, hashing, variable length integer libraries - * and so on. But for most developers its likely useful just for - * programming sanity. - * - * The problem is that some compiler vendors chose to ignore the C99 - * standard and some older compilers have no opportunity to be updated. - * Because of this situation, simply including stdint.h in your code - * makes it unportable. - * - * So that's what this file is all about. It's an attempt to build a - * single universal include file that works on as many platforms as - * possible to deliver what stdint.h is supposed to. Even compilers - * that already come with stdint.h can use this file instead without - * any loss of functionality. A few things that should be noted about - * this file: - * - * 1) It is not guaranteed to be portable and/or present an identical - * interface on all platforms. The extreme variability of the - * ANSI C standard makes this an impossibility right from the - * very get go. Its really only meant to be useful for the vast - * majority of platforms that possess the capability of - * implementing usefully and precisely defined, standard sized - * integer scalars. Systems which are not intrinsically 2s - * complement may produce invalid constants. - * - * 2) There is an unavoidable use of non-reserved symbols. - * - * 3) Other standard include files are invoked. - * - * 4) This file may come in conflict with future platforms that do - * include stdint.h. The hope is that one or the other can be - * used with no real difference. - * - * 5) In the current version, if your platform can't represent - * int32_t, int16_t and int8_t, it just dumps out with a compiler - * error. - * - * 6) 64 bit integers may or may not be defined. Test for their - * presence with the test: #ifdef INT64_MAX or #ifdef UINT64_MAX. - * Note that this is different from the C99 specification which - * requires the existence of 64 bit support in the compiler. If - * this is not defined for your platform, yet it is capable of - * dealing with 64 bits then it is because this file has not yet - * been extended to cover all of your system's capabilities. - * - * 7) (u)intptr_t may or may not be defined. Test for its presence - * with the test: #ifdef PTRDIFF_MAX. If this is not defined - * for your platform, then it is because this file has not yet - * been extended to cover all of your system's capabilities, not - * because its optional. - * - * 8) The following might not been defined even if your platform is - * capable of defining it: - * - * WCHAR_MIN - * WCHAR_MAX - * (u)int64_t - * PTRDIFF_MIN - * PTRDIFF_MAX - * (u)intptr_t - * - * 9) The following have not been defined: - * - * WINT_MIN - * WINT_MAX - * - * 10) The criteria for defining (u)int_least(*)_t isn't clear, - * except for systems which don't have a type that precisely - * defined 8, 16, or 32 bit types (which this include file does - * not support anyways). Default definitions have been given. - * - * 11) The criteria for defining (u)int_fast(*)_t isn't something I - * would trust to any particular compiler vendor or the ANSI C - * committee. It is well known that "compatible systems" are - * commonly created that have very different performance - * characteristics from the systems they are compatible with, - * especially those whose vendors make both the compiler and the - * system. Default definitions have been given, but its strongly - * recommended that users never use these definitions for any - * reason (they do *NOT* deliver any serious guarantee of - * improved performance -- not in this file, nor any vendor's - * stdint.h). - * - * 12) The following macros: - * - * PRINTF_INTMAX_MODIFIER - * PRINTF_INT64_MODIFIER - * PRINTF_INT32_MODIFIER - * PRINTF_INT16_MODIFIER - * PRINTF_LEAST64_MODIFIER - * PRINTF_LEAST32_MODIFIER - * PRINTF_LEAST16_MODIFIER - * PRINTF_INTPTR_MODIFIER - * - * are strings which have been defined as the modifiers required - * for the "d", "u" and "x" printf formats to correctly output - * (u)intmax_t, (u)int64_t, (u)int32_t, (u)int16_t, (u)least64_t, - * (u)least32_t, (u)least16_t and (u)intptr_t types respectively. - * PRINTF_INTPTR_MODIFIER is not defined for some systems which - * provide their own stdint.h. PRINTF_INT64_MODIFIER is not - * defined if INT64_MAX is not defined. These are an extension - * beyond what C99 specifies must be in stdint.h. - * - * In addition, the following macros are defined: - * - * PRINTF_INTMAX_HEX_WIDTH - * PRINTF_INT64_HEX_WIDTH - * PRINTF_INT32_HEX_WIDTH - * PRINTF_INT16_HEX_WIDTH - * PRINTF_INT8_HEX_WIDTH - * PRINTF_INTMAX_DEC_WIDTH - * PRINTF_INT64_DEC_WIDTH - * PRINTF_INT32_DEC_WIDTH - * PRINTF_INT16_DEC_WIDTH - * PRINTF_UINT8_DEC_WIDTH - * PRINTF_UINTMAX_DEC_WIDTH - * PRINTF_UINT64_DEC_WIDTH - * PRINTF_UINT32_DEC_WIDTH - * PRINTF_UINT16_DEC_WIDTH - * PRINTF_UINT8_DEC_WIDTH - * - * Which specifies the maximum number of characters required to - * print the number of that type in either hexadecimal or decimal. - * These are an extension beyond what C99 specifies must be in - * stdint.h. - * - * Compilers tested (all with 0 warnings at their highest respective - * settings): Borland Turbo C 2.0, WATCOM C/C++ 11.0 (16 bits and 32 - * bits), Microsoft Visual C++ 6.0 (32 bit), Microsoft Visual Studio - * .net (VC7), Intel C++ 4.0, GNU gcc v3.3.3 - * - * This file should be considered a work in progress. Suggestions for - * improvements, especially those which increase coverage are strongly - * encouraged. - * - * Acknowledgements - * - * The following people have made significant contributions to the - * development and testing of this file: - * - * Chris Howie - * John Steele Scott - * Dave Thorup - * John Dill - * Florian Wobbe - * Christopher Sean Morrison - * Mikkel Fahnoe Jorgensen - * - */ - -#include -#include -#include - -/* - * For gcc with _STDINT_H, fill in the PRINTF_INT*_MODIFIER macros, and - * do nothing else. On the Mac OS X version of gcc this is _STDINT_H_. - */ - -#if ((defined(__SUNPRO_C) && __SUNPRO_C >= 0x570) || (defined(_MSC_VER) && _MSC_VER >= 1600) || (defined(__STDC__) && __STDC__ && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined (__WATCOMC__) && (defined (_STDINT_H_INCLUDED) || __WATCOMC__ >= 1250)) || (defined(__GNUC__) && (__GNUC__ > 3 || defined(_STDINT_H) || defined(_STDINT_H_) || defined (__UINT_FAST64_TYPE__)) )) && !defined (_PSTDINT_H_INCLUDED) -#include -#define _PSTDINT_H_INCLUDED -# if defined(__GNUC__) && (defined(__x86_64__) || defined(__ppc64__)) && !(defined(__APPLE__) && defined(__MACH__)) -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "l" -# endif -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -# else -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "ll" -# endif -# ifndef PRINTF_INT32_MODIFIER -# if (UINT_MAX == UINT32_MAX) -# define PRINTF_INT32_MODIFIER "" -# else -# define PRINTF_INT32_MODIFIER "l" -# endif -# endif -# endif -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "h" -# endif -# ifndef PRINTF_INTMAX_MODIFIER -# define PRINTF_INTMAX_MODIFIER PRINTF_INT64_MODIFIER -# endif -# ifndef PRINTF_INT64_HEX_WIDTH -# define PRINTF_INT64_HEX_WIDTH "16" -# endif -# ifndef PRINTF_UINT64_HEX_WIDTH -# define PRINTF_UINT64_HEX_WIDTH "16" -# endif -# ifndef PRINTF_INT32_HEX_WIDTH -# define PRINTF_INT32_HEX_WIDTH "8" -# endif -# ifndef PRINTF_UINT32_HEX_WIDTH -# define PRINTF_UINT32_HEX_WIDTH "8" -# endif -# ifndef PRINTF_INT16_HEX_WIDTH -# define PRINTF_INT16_HEX_WIDTH "4" -# endif -# ifndef PRINTF_UINT16_HEX_WIDTH -# define PRINTF_UINT16_HEX_WIDTH "4" -# endif -# ifndef PRINTF_INT8_HEX_WIDTH -# define PRINTF_INT8_HEX_WIDTH "2" -# endif -# ifndef PRINTF_UINT8_HEX_WIDTH -# define PRINTF_UINT8_HEX_WIDTH "2" -# endif -# ifndef PRINTF_INT64_DEC_WIDTH -# define PRINTF_INT64_DEC_WIDTH "19" -# endif -# ifndef PRINTF_UINT64_DEC_WIDTH -# define PRINTF_UINT64_DEC_WIDTH "20" -# endif -# ifndef PRINTF_INT32_DEC_WIDTH -# define PRINTF_INT32_DEC_WIDTH "10" -# endif -# ifndef PRINTF_UINT32_DEC_WIDTH -# define PRINTF_UINT32_DEC_WIDTH "10" -# endif -# ifndef PRINTF_INT16_DEC_WIDTH -# define PRINTF_INT16_DEC_WIDTH "5" -# endif -# ifndef PRINTF_UINT16_DEC_WIDTH -# define PRINTF_UINT16_DEC_WIDTH "5" -# endif -# ifndef PRINTF_INT8_DEC_WIDTH -# define PRINTF_INT8_DEC_WIDTH "3" -# endif -# ifndef PRINTF_UINT8_DEC_WIDTH -# define PRINTF_UINT8_DEC_WIDTH "3" -# endif -# ifndef PRINTF_INTMAX_HEX_WIDTH -# define PRINTF_INTMAX_HEX_WIDTH PRINTF_UINT64_HEX_WIDTH -# endif -# ifndef PRINTF_UINTMAX_HEX_WIDTH -# define PRINTF_UINTMAX_HEX_WIDTH PRINTF_UINT64_HEX_WIDTH -# endif -# ifndef PRINTF_INTMAX_DEC_WIDTH -# define PRINTF_INTMAX_DEC_WIDTH PRINTF_UINT64_DEC_WIDTH -# endif -# ifndef PRINTF_UINTMAX_DEC_WIDTH -# define PRINTF_UINTMAX_DEC_WIDTH PRINTF_UINT64_DEC_WIDTH -# endif - -/* - * Something really weird is going on with Open Watcom. Just pull some of - * these duplicated definitions from Open Watcom's stdint.h file for now. - */ - -# if defined (__WATCOMC__) && __WATCOMC__ >= 1250 -# if !defined (INT64_C) -# define INT64_C(x) (x + (INT64_MAX - INT64_MAX)) -# endif -# if !defined (UINT64_C) -# define UINT64_C(x) (x + (UINT64_MAX - UINT64_MAX)) -# endif -# if !defined (INT32_C) -# define INT32_C(x) (x + (INT32_MAX - INT32_MAX)) -# endif -# if !defined (UINT32_C) -# define UINT32_C(x) (x + (UINT32_MAX - UINT32_MAX)) -# endif -# if !defined (INT16_C) -# define INT16_C(x) (x) -# endif -# if !defined (UINT16_C) -# define UINT16_C(x) (x) -# endif -# if !defined (INT8_C) -# define INT8_C(x) (x) -# endif -# if !defined (UINT8_C) -# define UINT8_C(x) (x) -# endif -# if !defined (UINT64_MAX) -# define UINT64_MAX 18446744073709551615ULL -# endif -# if !defined (INT64_MAX) -# define INT64_MAX 9223372036854775807LL -# endif -# if !defined (UINT32_MAX) -# define UINT32_MAX 4294967295UL -# endif -# if !defined (INT32_MAX) -# define INT32_MAX 2147483647L -# endif -# if !defined (INTMAX_MAX) -# define INTMAX_MAX INT64_MAX -# endif -# if !defined (INTMAX_MIN) -# define INTMAX_MIN INT64_MIN -# endif -# endif -#endif - -/* - * I have no idea what is the truly correct thing to do on older Solaris. - * From some online discussions, this seems to be what is being - * recommended. For people who actually are developing on older Solaris, - * what I would like to know is, does this define all of the relevant - * macros of a complete stdint.h? Remember, in pstdint.h 64 bit is - * considered optional. - */ - -#if (defined(__SUNPRO_C) && __SUNPRO_C >= 0x420) && !defined(_PSTDINT_H_INCLUDED) -#include -#define _PSTDINT_H_INCLUDED -#endif - -#ifndef _PSTDINT_H_INCLUDED -#define _PSTDINT_H_INCLUDED - -#ifndef SIZE_MAX -# define SIZE_MAX ((size_t)-1) -#endif - -/* - * Deduce the type assignments from limits.h under the assumption that - * integer sizes in bits are powers of 2, and follow the ANSI - * definitions. - */ - -#ifndef UINT8_MAX -# define UINT8_MAX 0xff -#endif -#if !defined(uint8_t) && !defined(_UINT8_T) && !defined(vxWorks) -# if (UCHAR_MAX == UINT8_MAX) || defined (S_SPLINT_S) - typedef unsigned char uint8_t; -# define UINT8_C(v) ((uint8_t) v) -# else -# error "Platform not supported" -# endif -#endif - -#ifndef INT8_MAX -# define INT8_MAX 0x7f -#endif -#ifndef INT8_MIN -# define INT8_MIN INT8_C(0x80) -#endif -#if !defined(int8_t) && !defined(_INT8_T) && !defined(vxWorks) -# if (SCHAR_MAX == INT8_MAX) || defined (S_SPLINT_S) - typedef signed char int8_t; -# define INT8_C(v) ((int8_t) v) -# else -# error "Platform not supported" -# endif -#endif - -#ifndef UINT16_MAX -# define UINT16_MAX 0xffff -#endif -#if !defined(uint16_t) && !defined(_UINT16_T) && !defined(vxWorks) -#if (UINT_MAX == UINT16_MAX) || defined (S_SPLINT_S) - typedef unsigned int uint16_t; -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "" -# endif -# define UINT16_C(v) ((uint16_t) (v)) -#elif (USHRT_MAX == UINT16_MAX) - typedef unsigned short uint16_t; -# define UINT16_C(v) ((uint16_t) (v)) -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "h" -# endif -#else -#error "Platform not supported" -#endif -#endif - -#ifndef INT16_MAX -# define INT16_MAX 0x7fff -#endif -#ifndef INT16_MIN -# define INT16_MIN INT16_C(0x8000) -#endif -#if !defined(int16_t) && !defined(_INT16_T) && !defined(vxWorks) -#if (INT_MAX == INT16_MAX) || defined (S_SPLINT_S) - typedef signed int int16_t; -# define INT16_C(v) ((int16_t) (v)) -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "" -# endif -#elif (SHRT_MAX == INT16_MAX) - typedef signed short int16_t; -# define INT16_C(v) ((int16_t) (v)) -# ifndef PRINTF_INT16_MODIFIER -# define PRINTF_INT16_MODIFIER "h" -# endif -#else -#error "Platform not supported" -#endif -#endif - -#ifndef UINT32_MAX -# define UINT32_MAX (0xffffffffUL) -#endif -#if !defined(uint32_t) && !defined(_UINT32_T) && !defined(vxWorks) -#if (ULONG_MAX == UINT32_MAX) || defined (S_SPLINT_S) - typedef unsigned long uint32_t; -# define UINT32_C(v) v ## UL -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "l" -# endif -#elif (UINT_MAX == UINT32_MAX) - typedef unsigned int uint32_t; -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -# define UINT32_C(v) v ## U -#elif (USHRT_MAX == UINT32_MAX) - typedef unsigned short uint32_t; -# define UINT32_C(v) ((unsigned short) (v)) -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -#else -#error "Platform not supported" -#endif -#endif - -#ifndef INT32_MAX -# define INT32_MAX (0x7fffffffL) -#endif -#ifndef INT32_MIN -# define INT32_MIN INT32_C(0x80000000) -#endif -#if !defined(int32_t) && !defined(_INT32_T) && !defined(vxWorks) -#if (LONG_MAX == INT32_MAX) || defined (S_SPLINT_S) - typedef signed long int32_t; -# define INT32_C(v) v ## L -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "l" -# endif -#elif (INT_MAX == INT32_MAX) - typedef signed int int32_t; -# define INT32_C(v) v -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -#elif (SHRT_MAX == INT32_MAX) - typedef signed short int32_t; -# define INT32_C(v) ((short) (v)) -# ifndef PRINTF_INT32_MODIFIER -# define PRINTF_INT32_MODIFIER "" -# endif -#else -#error "Platform not supported" -#endif -#endif - -/* - * The macro stdint_int64_defined is temporarily used to record - * whether or not 64 integer support is available. It must be - * defined for any 64 integer extensions for new platforms that are - * added. - */ - -#undef stdint_int64_defined -#if (defined(__STDC__) && defined(__STDC_VERSION__)) || defined (S_SPLINT_S) -# if (__STDC__ && __STDC_VERSION__ >= 199901L) || defined (S_SPLINT_S) -# define stdint_int64_defined - typedef long long int64_t; - typedef unsigned long long uint64_t; -# define UINT64_C(v) v ## ULL -# define INT64_C(v) v ## LL -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "ll" -# endif -# endif -#endif - -#if !defined (stdint_int64_defined) -# if defined(__GNUC__) && !defined(vxWorks) -# define stdint_int64_defined - __extension__ typedef long long int64_t; - __extension__ typedef unsigned long long uint64_t; -# define UINT64_C(v) v ## ULL -# define INT64_C(v) v ## LL -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "ll" -# endif -# elif defined(__MWERKS__) || defined (__SUNPRO_C) || defined (__SUNPRO_CC) || defined (__APPLE_CC__) || defined (_LONG_LONG) || defined (_CRAYC) || defined (S_SPLINT_S) -# define stdint_int64_defined - typedef long long int64_t; - typedef unsigned long long uint64_t; -# define UINT64_C(v) v ## ULL -# define INT64_C(v) v ## LL -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "ll" -# endif -# elif (defined(__WATCOMC__) && defined(__WATCOM_INT64__)) || (defined(_MSC_VER) && _INTEGRAL_MAX_BITS >= 64) || (defined (__BORLANDC__) && __BORLANDC__ > 0x460) || defined (__alpha) || defined (__DECC) -# define stdint_int64_defined - typedef __int64 int64_t; - typedef unsigned __int64 uint64_t; -# define UINT64_C(v) v ## UI64 -# define INT64_C(v) v ## I64 -# ifndef PRINTF_INT64_MODIFIER -# define PRINTF_INT64_MODIFIER "I64" -# endif -# endif -#endif - -#if !defined (LONG_LONG_MAX) && defined (INT64_C) -# define LONG_LONG_MAX INT64_C (9223372036854775807) -#endif -#ifndef ULONG_LONG_MAX -# define ULONG_LONG_MAX UINT64_C (18446744073709551615) -#endif - -#if !defined (INT64_MAX) && defined (INT64_C) -# define INT64_MAX INT64_C (9223372036854775807) -#endif -#if !defined (INT64_MIN) && defined (INT64_C) -# define INT64_MIN INT64_C (-9223372036854775808) -#endif -#if !defined (UINT64_MAX) && defined (INT64_C) -# define UINT64_MAX UINT64_C (18446744073709551615) -#endif - -/* - * Width of hexadecimal for number field. - */ - -#ifndef PRINTF_INT64_HEX_WIDTH -# define PRINTF_INT64_HEX_WIDTH "16" -#endif -#ifndef PRINTF_INT32_HEX_WIDTH -# define PRINTF_INT32_HEX_WIDTH "8" -#endif -#ifndef PRINTF_INT16_HEX_WIDTH -# define PRINTF_INT16_HEX_WIDTH "4" -#endif -#ifndef PRINTF_INT8_HEX_WIDTH -# define PRINTF_INT8_HEX_WIDTH "2" -#endif -#ifndef PRINTF_INT64_DEC_WIDTH -# define PRINTF_INT64_DEC_WIDTH "19" -#endif -#ifndef PRINTF_INT32_DEC_WIDTH -# define PRINTF_INT32_DEC_WIDTH "10" -#endif -#ifndef PRINTF_INT16_DEC_WIDTH -# define PRINTF_INT16_DEC_WIDTH "5" -#endif -#ifndef PRINTF_INT8_DEC_WIDTH -# define PRINTF_INT8_DEC_WIDTH "3" -#endif -#ifndef PRINTF_UINT64_DEC_WIDTH -# define PRINTF_UINT64_DEC_WIDTH "20" -#endif -#ifndef PRINTF_UINT32_DEC_WIDTH -# define PRINTF_UINT32_DEC_WIDTH "10" -#endif -#ifndef PRINTF_UINT16_DEC_WIDTH -# define PRINTF_UINT16_DEC_WIDTH "5" -#endif -#ifndef PRINTF_UINT8_DEC_WIDTH -# define PRINTF_UINT8_DEC_WIDTH "3" -#endif - -/* - * Ok, lets not worry about 128 bit integers for now. Moore's law says - * we don't need to worry about that until about 2040 at which point - * we'll have bigger things to worry about. - */ - -#ifdef stdint_int64_defined - typedef int64_t intmax_t; - typedef uint64_t uintmax_t; -# define INTMAX_MAX INT64_MAX -# define INTMAX_MIN INT64_MIN -# define UINTMAX_MAX UINT64_MAX -# define UINTMAX_C(v) UINT64_C(v) -# define INTMAX_C(v) INT64_C(v) -# ifndef PRINTF_INTMAX_MODIFIER -# define PRINTF_INTMAX_MODIFIER PRINTF_INT64_MODIFIER -# endif -# ifndef PRINTF_INTMAX_HEX_WIDTH -# define PRINTF_INTMAX_HEX_WIDTH PRINTF_INT64_HEX_WIDTH -# endif -# ifndef PRINTF_INTMAX_DEC_WIDTH -# define PRINTF_INTMAX_DEC_WIDTH PRINTF_INT64_DEC_WIDTH -# endif -#else - typedef int32_t intmax_t; - typedef uint32_t uintmax_t; -# define INTMAX_MAX INT32_MAX -# define UINTMAX_MAX UINT32_MAX -# define UINTMAX_C(v) UINT32_C(v) -# define INTMAX_C(v) INT32_C(v) -# ifndef PRINTF_INTMAX_MODIFIER -# define PRINTF_INTMAX_MODIFIER PRINTF_INT32_MODIFIER -# endif -# ifndef PRINTF_INTMAX_HEX_WIDTH -# define PRINTF_INTMAX_HEX_WIDTH PRINTF_INT32_HEX_WIDTH -# endif -# ifndef PRINTF_INTMAX_DEC_WIDTH -# define PRINTF_INTMAX_DEC_WIDTH PRINTF_INT32_DEC_WIDTH -# endif -#endif - -/* - * Because this file currently only supports platforms which have - * precise powers of 2 as bit sizes for the default integers, the - * least definitions are all trivial. Its possible that a future - * version of this file could have different definitions. - */ - -#ifndef stdint_least_defined - typedef int8_t int_least8_t; - typedef uint8_t uint_least8_t; - typedef int16_t int_least16_t; - typedef uint16_t uint_least16_t; - typedef int32_t int_least32_t; - typedef uint32_t uint_least32_t; -# define PRINTF_LEAST32_MODIFIER PRINTF_INT32_MODIFIER -# define PRINTF_LEAST16_MODIFIER PRINTF_INT16_MODIFIER -# define UINT_LEAST8_MAX UINT8_MAX -# define INT_LEAST8_MAX INT8_MAX -# define UINT_LEAST16_MAX UINT16_MAX -# define INT_LEAST16_MAX INT16_MAX -# define UINT_LEAST32_MAX UINT32_MAX -# define INT_LEAST32_MAX INT32_MAX -# define INT_LEAST8_MIN INT8_MIN -# define INT_LEAST16_MIN INT16_MIN -# define INT_LEAST32_MIN INT32_MIN -# ifdef stdint_int64_defined - typedef int64_t int_least64_t; - typedef uint64_t uint_least64_t; -# define PRINTF_LEAST64_MODIFIER PRINTF_INT64_MODIFIER -# define UINT_LEAST64_MAX UINT64_MAX -# define INT_LEAST64_MAX INT64_MAX -# define INT_LEAST64_MIN INT64_MIN -# endif -#endif -#undef stdint_least_defined - -/* - * The ANSI C committee has defined *int*_fast*_t types as well. This, - * of course, defies rationality -- you can't know what will be fast - * just from the type itself. Even for a given architecture, compatible - * implementations might have different performance characteristics. - * Developers are warned to stay away from these types when using this - * or any other stdint.h. - */ - -typedef int_least8_t int_fast8_t; -typedef uint_least8_t uint_fast8_t; -typedef int_least16_t int_fast16_t; -typedef uint_least16_t uint_fast16_t; -typedef int_least32_t int_fast32_t; -typedef uint_least32_t uint_fast32_t; -#define UINT_FAST8_MAX UINT_LEAST8_MAX -#define INT_FAST8_MAX INT_LEAST8_MAX -#define UINT_FAST16_MAX UINT_LEAST16_MAX -#define INT_FAST16_MAX INT_LEAST16_MAX -#define UINT_FAST32_MAX UINT_LEAST32_MAX -#define INT_FAST32_MAX INT_LEAST32_MAX -#define INT_FAST8_MIN INT_LEAST8_MIN -#define INT_FAST16_MIN INT_LEAST16_MIN -#define INT_FAST32_MIN INT_LEAST32_MIN -#ifdef stdint_int64_defined - typedef int_least64_t int_fast64_t; - typedef uint_least64_t uint_fast64_t; -# define UINT_FAST64_MAX UINT_LEAST64_MAX -# define INT_FAST64_MAX INT_LEAST64_MAX -# define INT_FAST64_MIN INT_LEAST64_MIN -#endif - -#undef stdint_int64_defined - -/* - * Whatever piecemeal, per compiler thing we can do about the wchar_t - * type limits. - */ - -#if defined(__WATCOMC__) || defined(_MSC_VER) || defined (__GNUC__) && !defined(vxWorks) -# include -# ifndef WCHAR_MIN -# define WCHAR_MIN 0 -# endif -# ifndef WCHAR_MAX -# define WCHAR_MAX ((wchar_t)-1) -# endif -#endif - -/* - * Whatever piecemeal, per compiler/platform thing we can do about the - * (u)intptr_t types and limits. - */ - -#if (defined (_MSC_VER) && defined (_UINTPTR_T_DEFINED)) || defined (_UINTPTR_T) -# define STDINT_H_UINTPTR_T_DEFINED -#endif - -#ifndef STDINT_H_UINTPTR_T_DEFINED -# if defined (__alpha__) || defined (__ia64__) || defined (__x86_64__) || defined (_WIN64) || defined (__ppc64__) -# define stdint_intptr_bits 64 -# elif defined (__WATCOMC__) || defined (__TURBOC__) -# if defined(__TINY__) || defined(__SMALL__) || defined(__MEDIUM__) -# define stdint_intptr_bits 16 -# else -# define stdint_intptr_bits 32 -# endif -# elif defined (__i386__) || defined (_WIN32) || defined (WIN32) || defined (__ppc64__) -# define stdint_intptr_bits 32 -# elif defined (__INTEL_COMPILER) -/* TODO -- what did Intel do about x86-64? */ -# else -/* #error "This platform might not be supported yet" */ -# endif - -# ifdef stdint_intptr_bits -# define stdint_intptr_glue3_i(a,b,c) a##b##c -# define stdint_intptr_glue3(a,b,c) stdint_intptr_glue3_i(a,b,c) -# ifndef PRINTF_INTPTR_MODIFIER -# define PRINTF_INTPTR_MODIFIER stdint_intptr_glue3(PRINTF_INT,stdint_intptr_bits,_MODIFIER) -# endif -# ifndef PTRDIFF_MAX -# define PTRDIFF_MAX stdint_intptr_glue3(INT,stdint_intptr_bits,_MAX) -# endif -# ifndef PTRDIFF_MIN -# define PTRDIFF_MIN stdint_intptr_glue3(INT,stdint_intptr_bits,_MIN) -# endif -# ifndef UINTPTR_MAX -# define UINTPTR_MAX stdint_intptr_glue3(UINT,stdint_intptr_bits,_MAX) -# endif -# ifndef INTPTR_MAX -# define INTPTR_MAX stdint_intptr_glue3(INT,stdint_intptr_bits,_MAX) -# endif -# ifndef INTPTR_MIN -# define INTPTR_MIN stdint_intptr_glue3(INT,stdint_intptr_bits,_MIN) -# endif -# ifndef INTPTR_C -# define INTPTR_C(x) stdint_intptr_glue3(INT,stdint_intptr_bits,_C)(x) -# endif -# ifndef UINTPTR_C -# define UINTPTR_C(x) stdint_intptr_glue3(UINT,stdint_intptr_bits,_C)(x) -# endif - typedef stdint_intptr_glue3(uint,stdint_intptr_bits,_t) uintptr_t; - typedef stdint_intptr_glue3( int,stdint_intptr_bits,_t) intptr_t; -# else -/* TODO -- This following is likely wrong for some platforms, and does - nothing for the definition of uintptr_t. */ - typedef ptrdiff_t intptr_t; -# endif -# define STDINT_H_UINTPTR_T_DEFINED -#endif - -/* - * Assumes sig_atomic_t is signed and we have a 2s complement machine. - */ - -#ifndef SIG_ATOMIC_MAX -# define SIG_ATOMIC_MAX ((((sig_atomic_t) 1) << (sizeof (sig_atomic_t)*CHAR_BIT-1)) - 1) -#endif - -#endif - -#if defined (__TEST_PSTDINT_FOR_CORRECTNESS) - -/* - * Please compile with the maximum warning settings to make sure macros are - * not defined more than once. - */ - -#include -#include -#include - -#define glue3_aux(x,y,z) x ## y ## z -#define glue3(x,y,z) glue3_aux(x,y,z) - -#define DECLU(bits) glue3(uint,bits,_t) glue3(u,bits,) = glue3(UINT,bits,_C) (0); -#define DECLI(bits) glue3(int,bits,_t) glue3(i,bits,) = glue3(INT,bits,_C) (0); - -#define DECL(us,bits) glue3(DECL,us,) (bits) - -#define TESTUMAX(bits) glue3(u,bits,) = ~glue3(u,bits,); if (glue3(UINT,bits,_MAX) != glue3(u,bits,)) printf ("Something wrong with UINT%d_MAX\n", bits) - -#define REPORTERROR(msg) { err_n++; if (err_first <= 0) err_first = __LINE__; printf msg; } - -#define X_SIZE_MAX ((size_t)-1) - -int main () { - int err_n = 0; - int err_first = 0; - DECL(I,8) - DECL(U,8) - DECL(I,16) - DECL(U,16) - DECL(I,32) - DECL(U,32) -#ifdef INT64_MAX - DECL(I,64) - DECL(U,64) -#endif - intmax_t imax = INTMAX_C(0); - uintmax_t umax = UINTMAX_C(0); - char str0[256], str1[256]; - - sprintf (str0, "%" PRINTF_INT32_MODIFIER "d", INT32_C(2147483647)); - if (0 != strcmp (str0, "2147483647")) REPORTERROR (("Something wrong with PRINTF_INT32_MODIFIER : %s\n", str0)); - if (atoi(PRINTF_INT32_DEC_WIDTH) != (int) strlen(str0)) REPORTERROR (("Something wrong with PRINTF_INT32_DEC_WIDTH : %s\n", PRINTF_INT32_DEC_WIDTH)); - sprintf (str0, "%" PRINTF_INT32_MODIFIER "u", UINT32_C(4294967295)); - if (0 != strcmp (str0, "4294967295")) REPORTERROR (("Something wrong with PRINTF_INT32_MODIFIER : %s\n", str0)); - if (atoi(PRINTF_UINT32_DEC_WIDTH) != (int) strlen(str0)) REPORTERROR (("Something wrong with PRINTF_UINT32_DEC_WIDTH : %s\n", PRINTF_UINT32_DEC_WIDTH)); -#ifdef INT64_MAX - sprintf (str1, "%" PRINTF_INT64_MODIFIER "d", INT64_C(9223372036854775807)); - if (0 != strcmp (str1, "9223372036854775807")) REPORTERROR (("Something wrong with PRINTF_INT32_MODIFIER : %s\n", str1)); - if (atoi(PRINTF_INT64_DEC_WIDTH) != (int) strlen(str1)) REPORTERROR (("Something wrong with PRINTF_INT64_DEC_WIDTH : %s, %d\n", PRINTF_INT64_DEC_WIDTH, (int) strlen(str1))); - sprintf (str1, "%" PRINTF_INT64_MODIFIER "u", UINT64_C(18446744073709550591)); - if (0 != strcmp (str1, "18446744073709550591")) REPORTERROR (("Something wrong with PRINTF_INT32_MODIFIER : %s\n", str1)); - if (atoi(PRINTF_UINT64_DEC_WIDTH) != (int) strlen(str1)) REPORTERROR (("Something wrong with PRINTF_UINT64_DEC_WIDTH : %s, %d\n", PRINTF_UINT64_DEC_WIDTH, (int) strlen(str1))); -#endif - - sprintf (str0, "%d %x\n", 0, ~0); - - sprintf (str1, "%d %x\n", i8, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with i8 : %s\n", str1)); - sprintf (str1, "%u %x\n", u8, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with u8 : %s\n", str1)); - sprintf (str1, "%d %x\n", i16, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with i16 : %s\n", str1)); - sprintf (str1, "%u %x\n", u16, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with u16 : %s\n", str1)); - sprintf (str1, "%" PRINTF_INT32_MODIFIER "d %x\n", i32, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with i32 : %s\n", str1)); - sprintf (str1, "%" PRINTF_INT32_MODIFIER "u %x\n", u32, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with u32 : %s\n", str1)); -#ifdef INT64_MAX - sprintf (str1, "%" PRINTF_INT64_MODIFIER "d %x\n", i64, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with i64 : %s\n", str1)); -#endif - sprintf (str1, "%" PRINTF_INTMAX_MODIFIER "d %x\n", imax, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with imax : %s\n", str1)); - sprintf (str1, "%" PRINTF_INTMAX_MODIFIER "u %x\n", umax, ~0); - if (0 != strcmp (str0, str1)) REPORTERROR (("Something wrong with umax : %s\n", str1)); - - TESTUMAX(8); - TESTUMAX(16); - TESTUMAX(32); -#ifdef INT64_MAX - TESTUMAX(64); -#endif - -#define STR(v) #v -#define Q(v) printf ("sizeof " STR(v) " = %u\n", (unsigned) sizeof (v)); - if (err_n) { - printf ("pstdint.h is not correct. Please use sizes below to correct it:\n"); - } - - Q(int) - Q(unsigned) - Q(long int) - Q(short int) - Q(int8_t) - Q(int16_t) - Q(int32_t) -#ifdef INT64_MAX - Q(int64_t) -#endif - -#if UINT_MAX < X_SIZE_MAX - printf ("UINT_MAX < X_SIZE_MAX\n"); -#else - printf ("UINT_MAX >= X_SIZE_MAX\n"); -#endif - printf ("%" PRINTF_INT64_MODIFIER "u vs %" PRINTF_INT64_MODIFIER "u\n", UINT_MAX, X_SIZE_MAX); - - return EXIT_SUCCESS; -} - -#endif \ No newline at end of file diff --git a/tests/128ChannelDefaultMapping.cmp b/tests/128ChannelDefaultMapping.cmp new file mode 100644 index 00000000..f6f57b46 --- /dev/null +++ b/tests/128ChannelDefaultMapping.cmp @@ -0,0 +1,141 @@ +// 128-Channel NSP mapping +// +// Data is as follows 'c r b e l' +// c - 0 based column from left to right +// r - 0 based row from bottom to top +// b - bank name - values can be A, B, C, or D +// e - 1 based electrode number within the bank - values can be 1-32 +// l - label used to rename channels in Central +// +// Comments begin with // +// First non-comment line is the Mapfile description +// +128-Channel NSP mapping +0 7 A 1 chan1 +1 7 A 2 chan2 +2 7 A 3 chan3 +3 7 A 4 chan4 +4 7 A 5 chan5 +5 7 A 6 chan6 +6 7 A 7 chan7 +7 7 A 8 chan8 +8 7 A 9 chan9 +9 7 A 10 chan10 +10 7 A 11 chan11 +11 7 A 12 chan12 +12 7 A 13 chan13 +13 7 A 14 chan14 +14 7 A 15 chan15 +15 7 A 16 chan16 +0 6 A 17 chan17 +1 6 A 18 chan18 +2 6 A 19 chan19 +3 6 A 20 chan20 +4 6 A 21 chan21 +5 6 A 22 chan22 +6 6 A 23 chan23 +7 6 A 24 chan24 +8 6 A 25 chan25 +9 6 A 26 chan26 +10 6 A 27 chan27 +11 6 A 28 chan28 +12 6 A 29 chan29 +13 6 A 30 chan30 +14 6 A 31 chan31 +15 6 A 32 chan32 +0 5 B 1 chan33 +1 5 B 2 chan34 +2 5 B 3 chan35 +3 5 B 4 chan36 +4 5 B 5 chan37 +5 5 B 6 chan38 +6 5 B 7 chan39 +7 5 B 8 chan40 +8 5 B 9 chan41 +9 5 B 10 chan42 +10 5 B 11 chan43 +11 5 B 12 chan44 +12 5 B 13 chan45 +13 5 B 14 chan46 +14 5 B 15 chan47 +15 5 B 16 chan48 +0 4 B 17 chan49 +1 4 B 18 chan50 +2 4 B 19 chan51 +3 4 B 20 chan52 +4 4 B 21 chan53 +5 4 B 22 chan54 +6 4 B 23 chan55 +7 4 B 24 chan56 +8 4 B 25 chan57 +9 4 B 26 chan58 +10 4 B 27 chan59 +11 4 B 28 chan60 +12 4 B 29 chan61 +13 4 B 30 chan62 +14 4 B 31 chan63 +15 4 B 32 chan64 +0 3 C 1 chan65 +1 3 C 2 chan66 +2 3 C 3 chan67 +3 3 C 4 chan68 +4 3 C 5 chan69 +5 3 C 6 chan70 +6 3 C 7 chan71 +7 3 C 8 chan72 +8 3 C 9 chan73 +9 3 C 10 chan74 +10 3 C 11 chan75 +11 3 C 12 chan76 +12 3 C 13 chan77 +13 3 C 14 chan78 +14 3 C 15 chan79 +15 3 C 16 chan80 +0 2 C 17 chan81 +1 2 C 18 chan82 +2 2 C 19 chan83 +3 2 C 20 chan84 +4 2 C 21 chan85 +5 2 C 22 chan86 +6 2 C 23 chan87 +7 2 C 24 chan88 +8 2 C 25 chan89 +9 2 C 26 chan90 +10 2 C 27 chan91 +11 2 C 28 chan92 +12 2 C 29 chan93 +13 2 C 30 chan94 +14 2 C 31 chan95 +15 2 C 32 chan96 +0 1 D 1 chan97 +1 1 D 2 chan98 +2 1 D 3 chan99 +3 1 D 4 chan100 +4 1 D 5 chan101 +5 1 D 6 chan102 +6 1 D 7 chan103 +7 1 D 8 chan104 +8 1 D 9 chan105 +9 1 D 10 chan106 +10 1 D 11 chan107 +11 1 D 12 chan108 +12 1 D 13 chan109 +13 1 D 14 chan110 +14 1 D 15 chan111 +15 1 D 16 chan112 +0 0 D 17 chan113 +1 0 D 18 chan114 +2 0 D 19 chan115 +3 0 D 20 chan116 +4 0 D 21 chan117 +5 0 D 22 chan118 +6 0 D 23 chan119 +7 0 D 24 chan120 +8 0 D 25 chan121 +9 0 D 26 chan122 +10 0 D 27 chan123 +11 0 D 28 chan124 +12 0 D 29 chan125 +13 0 D 30 chan126 +14 0 D 31 chan127 +15 0 D 32 chan128 diff --git a/tests/16ChannelDefaultMapping.cmp b/tests/16ChannelDefaultMapping.cmp new file mode 100644 index 00000000..85e6246b --- /dev/null +++ b/tests/16ChannelDefaultMapping.cmp @@ -0,0 +1,29 @@ +// 16-Channel NSP mapping +// +// Data is as follows 'c r b e l' +// c - 0 based column from left to right +// r - 0 based row from bottom to top +// b - bank name - values can be A, B, C, or D +// e - 1 based electrode number within the bank - values can be 1-32 +// l - label used to rename channels in Central +// +// Comments begin with // +// First non-comment line is the Mapfile description +// +16-Channel NSP mapping +0 1 A 1 chan1 +1 1 A 2 chan2 +2 1 A 3 chan3 +3 1 A 4 chan4 +4 1 A 5 chan5 +5 1 A 6 chan6 +6 1 A 7 chan7 +7 1 A 8 chan8 +0 0 A 9 chan9 +1 0 A 10 chan10 +2 0 A 11 chan11 +3 0 A 12 chan12 +4 0 A 13 chan13 +5 0 A 14 chan14 +6 0 A 15 chan15 +7 0 A 16 chan16 diff --git a/tests/32ChannelDefaultMapping.cmp b/tests/32ChannelDefaultMapping.cmp new file mode 100644 index 00000000..6add6126 --- /dev/null +++ b/tests/32ChannelDefaultMapping.cmp @@ -0,0 +1,45 @@ +// 32-Channel NSP mapping +// +// Data is as follows 'c r b e l' +// c - 0 based column from left to right +// r - 0 based row from bottom to top +// b - bank name - values can be A, B, C, or D +// e - 1 based electrode number within the bank - values can be 1-32 +// l - label used to rename channels in Central +// +// Comments begin with // +// First non-comment line is the Mapfile description +// +32-Channel NSP mapping +0 3 A 1 chan1 +1 3 A 2 chan2 +2 3 A 3 chan3 +3 3 A 4 chan4 +4 3 A 5 chan5 +5 3 A 6 chan6 +6 3 A 7 chan7 +7 3 A 8 chan8 +0 2 A 9 chan9 +1 2 A 10 chan10 +2 2 A 11 chan11 +3 2 A 12 chan12 +4 2 A 13 chan13 +5 2 A 14 chan14 +6 2 A 15 chan15 +7 2 A 16 chan16 +0 1 A 17 chan17 +1 1 A 18 chan18 +2 1 A 19 chan19 +3 1 A 20 chan20 +4 1 A 21 chan21 +5 1 A 22 chan22 +6 1 A 23 chan23 +7 1 A 24 chan24 +0 0 A 25 chan25 +1 0 A 26 chan26 +2 0 A 27 chan27 +3 0 A 28 chan28 +4 0 A 29 chan29 +5 0 A 30 chan30 +6 0 A 31 chan31 +7 0 A 32 chan32 diff --git a/tests/64ChannelDefaultMapping.cmp b/tests/64ChannelDefaultMapping.cmp new file mode 100644 index 00000000..496bd3de --- /dev/null +++ b/tests/64ChannelDefaultMapping.cmp @@ -0,0 +1,77 @@ +// 64-Channel NSP mapping +// +// Data is as follows 'c r b e l' +// c - 0 based column from left to right +// r - 0 based row from bottom to top +// b - bank name - values can be A, B, C, or D +// e - 1 based electrode number within the bank - values can be 1-32 +// l - label used to rename channels in Central +// +// Comments begin with // +// First non-comment line is the Mapfile description +// +64-Channel NSP mapping +0 3 A 1 chan1 +1 3 A 2 chan2 +2 3 A 3 chan3 +3 3 A 4 chan4 +4 3 A 5 chan5 +5 3 A 6 chan6 +6 3 A 7 chan7 +7 3 A 8 chan8 +8 3 A 9 chan9 +9 3 A 10 chan10 +10 3 A 11 chan11 +11 3 A 12 chan12 +12 3 A 13 chan13 +13 3 A 14 chan14 +14 3 A 15 chan15 +15 3 A 16 chan16 +0 2 A 17 chan17 +1 2 A 18 chan18 +2 2 A 19 chan19 +3 2 A 20 chan20 +4 2 A 21 chan21 +5 2 A 22 chan22 +6 2 A 23 chan23 +7 2 A 24 chan24 +8 2 A 25 chan25 +9 2 A 26 chan26 +10 2 A 27 chan27 +11 2 A 28 chan28 +12 2 A 29 chan29 +13 2 A 30 chan30 +14 2 A 31 chan31 +15 2 A 32 chan32 +0 1 B 1 chan33 +1 1 B 2 chan34 +2 1 B 3 chan35 +3 1 B 4 chan36 +4 1 B 5 chan37 +5 1 B 6 chan38 +6 1 B 7 chan39 +7 1 B 8 chan40 +8 1 B 9 chan41 +9 1 B 10 chan42 +10 1 B 11 chan43 +11 1 B 12 chan44 +12 1 B 13 chan45 +13 1 B 14 chan46 +14 1 B 15 chan47 +15 1 B 16 chan48 +0 0 B 17 chan49 +1 0 B 18 chan50 +2 0 B 19 chan51 +3 0 B 20 chan52 +4 0 B 21 chan53 +5 0 B 22 chan54 +6 0 B 23 chan55 +7 0 B 24 chan56 +8 0 B 25 chan57 +9 0 B 26 chan58 +10 0 B 27 chan59 +11 0 B 28 chan60 +12 0 B 29 chan61 +13 0 B 30 chan62 +14 0 B 31 chan63 +15 0 B 32 chan64 diff --git a/tests/8ChannelDefaultMapping.cmp b/tests/8ChannelDefaultMapping.cmp new file mode 100644 index 00000000..061ccdbe --- /dev/null +++ b/tests/8ChannelDefaultMapping.cmp @@ -0,0 +1,21 @@ +// 8-Channel NSP mapping +// +// Data is as follows 'c r b e l' +// c - 0 based column from left to right +// r - 0 based row from bottom to top +// b - bank name - values can be A, B, C, or D +// e - 1 based electrode number within the bank - values can be 1-32 +// l - label used to rename channels in Central +// +// Comments begin with // +// First non-comment line is the Mapfile description +// +8-Channel NSP mapping +0 1 A 1 chan1 +1 1 A 2 chan2 +2 1 A 3 chan3 +3 1 A 4 chan4 +0 0 A 5 chan5 +1 0 A 6 chan6 +2 0 A 7 chan7 +3 0 A 8 chan8 diff --git a/tests/96ChannelDefaultMapping.cmp b/tests/96ChannelDefaultMapping.cmp new file mode 100644 index 00000000..25d603c1 --- /dev/null +++ b/tests/96ChannelDefaultMapping.cmp @@ -0,0 +1,109 @@ +// 96-Channel NSP mapping +// +// Data is as follows 'c r b e l' +// c - 0 based column from left to right +// r - 0 based row from bottom to top +// b - bank name - values can be A, B, C, or D +// e - 1 based electrode number within the bank - values can be 1-32 +// l - label used to rename channels in Central +// +// Comments begin with // +// First non-comment line is the Mapfile description +// +96-Channel NSP mapping +0 5 A 1 chan1 +1 5 A 2 chan2 +2 5 A 3 chan3 +3 5 A 4 chan4 +4 5 A 5 chan5 +5 5 A 6 chan6 +6 5 A 7 chan7 +7 5 A 8 chan8 +8 5 A 9 chan9 +9 5 A 10 chan10 +10 5 A 11 chan11 +11 5 A 12 chan12 +12 5 A 13 chan13 +13 5 A 14 chan14 +14 5 A 15 chan15 +15 5 A 16 chan16 +0 4 A 17 chan17 +1 4 A 18 chan18 +2 4 A 19 chan19 +3 4 A 20 chan20 +4 4 A 21 chan21 +5 4 A 22 chan22 +6 4 A 23 chan23 +7 4 A 24 chan24 +8 4 A 25 chan25 +9 4 A 26 chan26 +10 4 A 27 chan27 +11 4 A 28 chan28 +12 4 A 29 chan29 +13 4 A 30 chan30 +14 4 A 31 chan31 +15 4 A 32 chan32 +0 3 B 1 chan33 +1 3 B 2 chan34 +2 3 B 3 chan35 +3 3 B 4 chan36 +4 3 B 5 chan37 +5 3 B 6 chan38 +6 3 B 7 chan39 +7 3 B 8 chan40 +8 3 B 9 chan41 +9 3 B 10 chan42 +10 3 B 11 chan43 +11 3 B 12 chan44 +12 3 B 13 chan45 +13 3 B 14 chan46 +14 3 B 15 chan47 +15 3 B 16 chan48 +0 2 B 17 chan49 +1 2 B 18 chan50 +2 2 B 19 chan51 +3 2 B 20 chan52 +4 2 B 21 chan53 +5 2 B 22 chan54 +6 2 B 23 chan55 +7 2 B 24 chan56 +8 2 B 25 chan57 +9 2 B 26 chan58 +10 2 B 27 chan59 +11 2 B 28 chan60 +12 2 B 29 chan61 +13 2 B 30 chan62 +14 2 B 31 chan63 +15 2 B 32 chan64 +0 1 C 1 chan65 +1 1 C 2 chan66 +2 1 C 3 chan67 +3 1 C 4 chan68 +4 1 C 5 chan69 +5 1 C 6 chan70 +6 1 C 7 chan71 +7 1 C 8 chan72 +8 1 C 9 chan73 +9 1 C 10 chan74 +10 1 C 11 chan75 +11 1 C 12 chan76 +12 1 C 13 chan77 +13 1 C 14 chan78 +14 1 C 15 chan79 +15 1 C 16 chan80 +0 0 C 17 chan81 +1 0 C 18 chan82 +2 0 C 19 chan83 +3 0 C 20 chan84 +4 0 C 21 chan85 +5 0 C 22 chan86 +6 0 C 23 chan87 +7 0 C 24 chan88 +8 0 C 25 chan89 +9 0 C 26 chan90 +10 0 C 27 chan91 +11 0 C 28 chan92 +12 0 C 29 chan93 +13 0 C 30 chan94 +14 0 C 31 chan95 +15 0 C 32 chan96 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 62f505e8..30848783 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -73,7 +73,7 @@ target_include_directories(continuous_data_tests PRIVATE ${PROJECT_SOURCE_DIR}/CCFUtils ${PROJECT_SOURCE_DIR}/cbproto ${PROJECT_SOURCE_DIR}/cbsdk - ${PROJECT_SOURCE_DIR}/src/cbsdk + ${PROJECT_SOURCE_DIR}/src/cbsdk_old ${PROJECT_SOURCE_DIR}/src/cbhwlib ${PROJECT_SOURCE_DIR}/cbhwlib ${PROJECT_SOURCE_DIR}/Central @@ -94,7 +94,7 @@ target_include_directories(event_data_tests PRIVATE ${PROJECT_SOURCE_DIR}/CCFUtils ${PROJECT_SOURCE_DIR}/cbproto ${PROJECT_SOURCE_DIR}/cbsdk - ${PROJECT_SOURCE_DIR}/src/cbsdk + ${PROJECT_SOURCE_DIR}/src/cbsdk_old ${PROJECT_SOURCE_DIR}/src/cbhwlib ${PROJECT_SOURCE_DIR}/cbhwlib ${PROJECT_SOURCE_DIR}/Central @@ -109,3 +109,4 @@ target_link_libraries(event_data_tests ) add_test(NAME event_data_tests COMMAND event_data_tests) + diff --git a/tests/ContinuousDataTests.cpp b/tests/ContinuousDataTests.cpp index a6a60273..197e0c0c 100644 --- a/tests/ContinuousDataTests.cpp +++ b/tests/ContinuousDataTests.cpp @@ -1,5 +1,5 @@ #include -#include "../src/cbsdk/ContinuousData.h" +#include "../src/cbsdk_old/ContinuousData.h" #include #include #include diff --git a/tests/EventDataTests.cpp b/tests/EventDataTests.cpp index 952c6b56..bab96ea8 100644 --- a/tests/EventDataTests.cpp +++ b/tests/EventDataTests.cpp @@ -1,5 +1,5 @@ #include -#include "../src/cbsdk/EventData.h" +#include "../src/cbsdk_old/EventData.h" #include #include #include diff --git a/tests/ccf_raw.ccf b/tests/ccf_raw.ccf new file mode 100644 index 00000000..6417941a --- /dev/null +++ b/tests/ccf_raw.ccf @@ -0,0 +1,45 @@ + + + + + 32768 + 64 + 166 + 1 + 1 + 1 + 1 + + 64 + 65792 + + + 10000 + + + 0 + 0 + + + + 32768 + 64 + 166 + 2 + 1 + 1 + 2 + + 64 + 65792 + + + 10000 + + + 0 + 0 + + + + diff --git a/tests/ccf_spk_1k.ccf b/tests/ccf_spk_1k.ccf new file mode 100644 index 00000000..065db42c --- /dev/null +++ b/tests/ccf_spk_1k.ccf @@ -0,0 +1,45 @@ + + + + + 32768 + 64 + 166 + 1 + 1 + 1 + 1 + + 258 + 65793 + + + 10000 + + + 6 + 2 + + + + 32768 + 64 + 166 + 2 + 1 + 1 + 2 + + 258 + 65793 + + + 10000 + + + 6 + 2 + + + + diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt new file mode 100644 index 00000000..0e653496 --- /dev/null +++ b/tests/integration/CMakeLists.txt @@ -0,0 +1,27 @@ +# Integration Tests +# Tests cross-module interactions (cbdev + cbshm, cbsdk end-to-end, etc.) + +# Note: gated by CBSDK_BUILD_TEST in top-level CMakeLists.txt + +# GoogleTest framework (shared with unit tests) +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 + GIT_SHALLOW TRUE +) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() +include(GoogleTest) + +# TODO: Add integration test executables when ready +# Examples: +# - cbdev_cbshm_integration: Test device → shmem packet flow +# - cbsdk_standalone_test: End-to-end standalone mode test +# - cbsdk_client_test: End-to-end client mode test (with mock Central) +# - mode_switching_test: Test switching between modes + +message(STATUS "Integration test framework configured (waiting for test sources)") diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 00000000..7450b753 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,238 @@ +# Unit Tests +# Each module (cbproto, cbshm, cbdev, cbsdk) will have its own test suite + +# Note: gated by CBSDK_BUILD_TEST in top-level CMakeLists.txt + +# GoogleTest will be fetched as needed +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 + GIT_SHALLOW TRUE +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() +include(GoogleTest) + +# cbproto tests +add_executable(cbproto_tests + test_instrument_id.cpp + test_protocol_structures.cpp +) + +target_link_libraries(cbproto_tests + PRIVATE + cbproto + GTest::gtest_main +) + +target_include_directories(cbproto_tests + PRIVATE + ${PROJECT_SOURCE_DIR}/src/cbproto/include +) + +gtest_discover_tests(cbproto_tests) + +message(STATUS "Unit tests configured for cbproto") + +# cbshm tests +add_executable(cbshm_tests + test_shmem_session.cpp + test_native_types.cpp +) + +target_link_libraries(cbshm_tests + PRIVATE + cbshm + cbproto + GTest::gtest_main +) + +# Include directories inherited from cbshm and cbproto targets + +gtest_discover_tests(cbshm_tests) + +message(STATUS "Unit tests configured for cbshm") + +# cbdev tests (non-device-dependent: packet translation, clock sync) +add_executable(cbdev_tests + test_packet_translation.cpp + test_clock_sync.cpp + packet_test_helpers.cpp +) + +target_link_libraries(cbdev_tests + PRIVATE + cbdev + cbproto + GTest::gtest_main +) + +target_include_directories(cbdev_tests + BEFORE PRIVATE + ${PROJECT_SOURCE_DIR}/src/cbdev/include + ${PROJECT_SOURCE_DIR}/src/cbdev/src + ${PROJECT_SOURCE_DIR}/src/cbproto/include +) + +gtest_discover_tests(cbdev_tests) + +message(STATUS "Unit tests configured for cbdev") + +# cbdev device session tests (require network/device, excluded from CI) +add_executable(cbdev_device_tests + test_device_session.cpp +) + +target_link_libraries(cbdev_device_tests + PRIVATE + cbdev + cbproto + GTest::gtest_main +) + +target_include_directories(cbdev_device_tests + BEFORE PRIVATE + ${PROJECT_SOURCE_DIR}/src/cbdev/include + ${PROJECT_SOURCE_DIR}/src/cbdev/src + ${PROJECT_SOURCE_DIR}/src/cbproto/include +) + +gtest_discover_tests(cbdev_device_tests + TEST_PREFIX "device." +) + +message(STATUS "Unit tests configured for cbdev device sessions (prefixed with 'device.' for CI filtering)") + +# Standalone clock sync test (does not depend on socket/device test helpers) +add_executable(clock_sync_tests + test_clock_sync.cpp +) + +target_link_libraries(clock_sync_tests + PRIVATE + cbdev + GTest::gtest_main +) + +target_include_directories(clock_sync_tests + BEFORE PRIVATE + ${PROJECT_SOURCE_DIR}/src/cbdev/src +) + +gtest_discover_tests(clock_sync_tests) + +message(STATUS "Unit tests configured for clock_sync") + +# cbsdk tests +add_executable(cbsdk_tests + test_sdk_session.cpp + test_sdk_handshake.cpp + test_cbsdk_c_api.cpp +) + +target_link_libraries(cbsdk_tests + PRIVATE + cbsdk + cbdev + cbproto + cbshm + GTest::gtest_main +) + +target_include_directories(cbsdk_tests + BEFORE PRIVATE + ${PROJECT_SOURCE_DIR}/src/cbsdk/include + ${PROJECT_SOURCE_DIR}/src/cbdev/include + ${PROJECT_SOURCE_DIR}/src/cbdev/src + ${PROJECT_SOURCE_DIR}/src/cbshm/include + ${PROJECT_SOURCE_DIR}/src/cbproto/include +) + +# Prefix cbsdk test names so CI can easily exclude device-dependent tests. +# Once a mock device is implemented, the "device." prefix can be removed. +gtest_discover_tests(cbsdk_tests + TEST_PREFIX "device." +) + +message(STATUS "Unit tests configured for cbsdk (prefixed with 'device.' for CI filtering)") + +# CMP parser tests (no device needed) +add_executable(cmp_parser_tests + test_cmp_parser.cpp +) + +target_link_libraries(cmp_parser_tests + PRIVATE + cbsdk + GTest::gtest_main +) + +target_include_directories(cmp_parser_tests + BEFORE PRIVATE + ${PROJECT_SOURCE_DIR}/src/cbsdk/src + ${PROJECT_SOURCE_DIR}/src/cbsdk/include + ${PROJECT_SOURCE_DIR}/src/cbproto/include +) + +target_compile_definitions(cmp_parser_tests + PRIVATE + CMP_TEST_DATA_DIR="${PROJECT_SOURCE_DIR}/tests" +) + +gtest_discover_tests(cmp_parser_tests) + +message(STATUS "Unit tests configured for CMP parser") + +# ccfutils tests (CCF <-> DeviceConfig conversion) +add_executable(ccfutils_tests + test_ccf_config.cpp +) + +target_link_libraries(ccfutils_tests + PRIVATE + ccfutils + cbproto + GTest::gtest_main +) + +target_include_directories(ccfutils_tests + BEFORE PRIVATE + ${PROJECT_SOURCE_DIR}/src/ccfutils/include + ${PROJECT_SOURCE_DIR}/src/cbproto/include +) + +gtest_discover_tests(ccfutils_tests) + +message(STATUS "Unit tests configured for ccfutils") + +# ccfutils XML read/write tests (uses test fixture files, no device needed) +add_executable(ccfutils_xml_tests + test_ccf_xml.cpp +) + +target_link_libraries(ccfutils_xml_tests + PRIVATE + ccfutils + cbproto + GTest::gtest_main +) + +target_include_directories(ccfutils_xml_tests + BEFORE PRIVATE + ${PROJECT_SOURCE_DIR}/src/ccfutils/include + ${PROJECT_SOURCE_DIR}/src/cbproto/include +) + +target_compile_definitions(ccfutils_xml_tests + PRIVATE + CCF_TEST_DATA_DIR="${PROJECT_SOURCE_DIR}/tests" +) + +gtest_discover_tests(ccfutils_xml_tests) + +message(STATUS "Unit tests configured for ccfutils XML read/write") diff --git a/tests/unit/packet_test_helpers.cpp b/tests/unit/packet_test_helpers.cpp new file mode 100644 index 00000000..58be70b8 --- /dev/null +++ b/tests/unit/packet_test_helpers.cpp @@ -0,0 +1,303 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file packet_test_helpers.cpp +/// @author CereLink Development Team +/// @date 2025-01-19 +/// +/// @brief Implementation of packet factory helper functions +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "packet_test_helpers.h" +#include +#include + +namespace test_helpers { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Header Factories +/////////////////////////////////////////////////////////////////////////////////////////////////// + +std::vector make_311_header(const uint32_t time, const uint16_t chid, const uint8_t type, const uint8_t dlen) { + std::vector header(HEADER_SIZE_311); + + // Protocol 3.11 layout: time(32b) chid(16b) type(8b) dlen(8b) + *reinterpret_cast(&header[0]) = time; + *reinterpret_cast(&header[4]) = chid; + header[6] = type; + header[7] = dlen; + + return header; +} + +std::vector make_400_header(const uint64_t time, const uint16_t chid, const uint8_t type, + const uint16_t dlen, const uint8_t instrument) { + std::vector header(HEADER_SIZE_400); + + // Protocol 4.0 layout: time(64b) chid(16b) type(8b) dlen(16b) instrument(8b) reserved(16b) + *reinterpret_cast(&header[0]) = time; + *reinterpret_cast(&header[8]) = chid; + header[10] = type; + *reinterpret_cast(&header[11]) = dlen; + header[13] = instrument; + *reinterpret_cast(&header[14]) = 0; // reserved + + return header; +} + +cbPKT_HEADER make_current_header(const uint64_t time, const uint16_t chid, const uint16_t type, + const uint16_t dlen, const uint8_t instrument) { + cbPKT_HEADER header = {}; + header.time = time; + header.chid = chid; + header.type = type; + header.dlen = dlen; + header.instrument = instrument; + header.reserved = 0; + return header; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Protocol 3.11 Packet Factories +/////////////////////////////////////////////////////////////////////////////////////////////////// + +std::vector make_311_SYSPROTOCOLMONITOR(const uint32_t sentpkts, const uint32_t time) { + // 3.11 SYSPROTOCOLMONITOR: header + sentpkts (no counter field) + constexpr size_t dlen = cbPKTDLEN_SYSPROTOCOLMONITOR - 1; + auto packet = make_311_header(time, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_SYSPROTOCOLMONITOR, dlen); + + // Append payload + const size_t offset = packet.size(); + packet.resize(offset + dlen * 4); + *reinterpret_cast(&packet[offset]) = sentpkts; + + return packet; +} + +std::vector make_311_NPLAY(const uint32_t ftime, const uint32_t stime, const uint32_t etime, + const uint32_t val, const uint32_t mode) { + // 3.11 NPLAY: header + 4 time fields (uint32_t) + unchanged: mode + flags + speed + fname + + constexpr size_t dlen = cbPKTDLEN_NPLAY - 4; + auto packet = make_311_header(1000, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_NPLAYREP, dlen); + + const size_t offset = packet.size(); + packet.resize(offset + dlen * 4); + + *reinterpret_cast(&packet[offset + 0]) = ftime; + *reinterpret_cast(&packet[offset + 4]) = stime; + *reinterpret_cast(&packet[offset + 8]) = etime; + *reinterpret_cast(&packet[offset + 12]) = val; + *reinterpret_cast(&packet[offset + 16]) = mode; + *reinterpret_cast(&packet[offset + 18]) = cbNPLAY_FLAG_NONE; + *reinterpret_cast(&packet[offset + 20]) = 1.0; // speed + // TODO: fname + + return packet; +} + +std::vector make_311_COMMENT(const uint8_t flags, const uint32_t data, + const char* comment, const uint32_t time) { + // 3.11 COMMENT: header + info(4 bytes) + data(4 bytes) + comment string + // info: type(1) flags(1) reserved(2) + + const size_t comment_len = comment ? strlen(comment) + 1 : 1; // Include null terminator + const size_t total_payload = 8 + comment_len; // info(4) + data(4) + comment + // Pad to quadlet boundary + const size_t padded_payload = ((total_payload + 3) / 4) * 4; + const auto dlen = static_cast(padded_payload / 4); + + auto packet = make_311_header(time, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_COMMENTREP, dlen); + + const size_t offset = packet.size(); + packet.resize(offset + padded_payload, 0); // Zero-fill padding + + // info structure (4 bytes) + packet[offset + 0] = 0; // type + packet[offset + 1] = flags; + packet[offset + 2] = 0; // reserved + packet[offset + 3] = 0; // reserved + + // data field (4 bytes) + *reinterpret_cast(&packet[offset + 4]) = data; + + // comment string + if (comment) { + std::memcpy(&packet[offset + 8], comment, comment_len); + } + + return packet; +} + + +std::vector make_311_CHANINFO(const uint32_t chan, const uint32_t monsource) { + // Create a 3.11 CHANINFO packet + constexpr uint8_t dlen = cbPKTDLEN_CHANINFO - 1; + + auto packet = make_311_header(1000, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_CHANREP, dlen); + + const size_t offset = packet.size(); + packet.resize(offset + dlen * 4, 0); + + // chan field (first field after header) + *reinterpret_cast(&packet[offset]) = chan; + + // Modify monsource + constexpr size_t monsource_offset = offsetof(cbPKT_CHANINFO, moninst) - cbPKT_HEADER_SIZE; + *reinterpret_cast(&packet[offset + monsource_offset]) = monsource; + + // Modify trigchan + constexpr size_t trigchan_offset = offsetof(cbPKT_CHANINFO, trigchan) - cbPKT_HEADER_SIZE - 3; + *reinterpret_cast(&packet[offset + trigchan_offset]) = 42; // example trigchan + + return packet; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Protocol 4.0 Packet Factories +/////////////////////////////////////////////////////////////////////////////////////////////////// + +std::vector make_400_SYSPROTOCOLMONITOR(const uint32_t sentpkts, const uint64_t time) { + // 4.0 SYSPROTOCOLMONITOR: same payload as 3.11 (no counter field) + auto packet = make_400_header(time, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_SYSPROTOCOLMONITOR, 1, 0); + + const size_t offset = packet.size(); + packet.resize(offset + 4); + *reinterpret_cast(&packet[offset]) = sentpkts; + + return packet; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Protocol 4.10 Packet Factories +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbPKT_GENERIC make_410_CHANRESET(const uint32_t chan, const uint8_t monsource) { + // 4.10 CHANRESET has monsource as single uint8_t (before moninst/monchan split in 4.2) + // Note: 4.10 header is same as current, only payload structure differs + + cbPKT_GENERIC pkt = {}; + pkt.cbpkt_header = make_current_header(1000, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_CHANRESET, 7, 0); + + // Create CHANRESET payload with pre-4.2 structure + // We'll use the current cbPKT_CHANRESET but modify it to match 4.10 layout + auto* chanreset = reinterpret_cast(&pkt); + chanreset->chan = chan; + chanreset->label = 0; + chanreset->userflags = 0; + // In 4.10, there's only monsource (uint8_t), not moninst + monchan + chanreset->moninst = monsource; // This would be monsource in 4.10 + // monchan doesn't exist in 4.10 + + return pkt; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Current Protocol Packet Factories +/////////////////////////////////////////////////////////////////////////////////////////////////// + +cbPKT_SYSPROTOCOLMONITOR make_current_SYSPROTOCOLMONITOR(const uint32_t sentpkts, const uint32_t counter) { + cbPKT_SYSPROTOCOLMONITOR pkt = {}; + pkt.cbpkt_header = make_current_header(1000, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_SYSPROTOCOLMONITOR, + cbPKTDLEN_SYSPROTOCOLMONITOR, 0); + pkt.sentpkts = sentpkts; + pkt.counter = counter; + return pkt; +} + +cbPKT_NPLAY make_current_NPLAY(const PROCTIME ftime, const PROCTIME stime, const PROCTIME etime, + const PROCTIME val, const uint32_t mode) { + cbPKT_NPLAY pkt = {}; + pkt.cbpkt_header = make_current_header(1000, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_NPLAYREP, + cbPKTDLEN_NPLAY, 0); + pkt.ftime = ftime; + pkt.stime = stime; + pkt.etime = etime; + pkt.val = val; + pkt.mode = mode; + std::memset(pkt.fname, 0, sizeof(pkt.fname)); + return pkt; +} + +cbPKT_COMMENT make_current_COMMENT(const uint8_t charset, const PROCTIME timeStarted, + const uint32_t rgba, const char* comment) { + cbPKT_COMMENT pkt = {}; + pkt.cbpkt_header = make_current_header(1000, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_COMMENTREP, + cbPKTDLEN_COMMENT, 0); + pkt.info.charset = charset; + pkt.timeStarted = timeStarted; + pkt.rgba = rgba; + + if (comment) { + std::strncpy(reinterpret_cast(pkt.comment), comment, cbMAX_COMMENT - 1); + pkt.comment[cbMAX_COMMENT - 1] = '\0'; + } else { + std::memset(pkt.comment, 0, cbMAX_COMMENT); + } + + return pkt; +} + +cbPKT_CHANINFO make_current_CHANINFO(const uint32_t chan, const uint16_t moninst, const uint16_t monchan) { + cbPKT_CHANINFO pkt = {}; + pkt.cbpkt_header = make_current_header(1000, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_CHANREP, + cbPKTDLEN_CHANINFO, 0); + pkt.chan = chan; + pkt.moninst = moninst; + pkt.monchan = monchan; + // Fill other fields with defaults + std::memset(pkt.label, 0, sizeof(pkt.label)); + return pkt; +} + +cbPKT_CHANRESET make_current_CHANRESET(const uint32_t chan, const uint8_t moninst, const uint8_t monchan) { + cbPKT_CHANRESET pkt = {}; + pkt.cbpkt_header = make_current_header(1000, cbPKTCHAN_CONFIGURATION, + cbPKTTYPE_CHANRESET, + cbPKTDLEN_CHANRESET, 0); + pkt.chan = chan; + pkt.moninst = moninst; + pkt.monchan = monchan; + // Fill other fields with defaults + pkt.label = 0; + pkt.userflags = 0; + return pkt; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Comparison Helpers +/////////////////////////////////////////////////////////////////////////////////////////////////// + +bool packets_equal(const cbPKT_GENERIC& expected, const cbPKT_GENERIC& actual, + uint64_t tolerance) { + // Compare headers + if (std::abs(static_cast(expected.cbpkt_header.time) - + static_cast(actual.cbpkt_header.time)) > static_cast(tolerance)) { + return false; + } + + if (expected.cbpkt_header.chid != actual.cbpkt_header.chid) return false; + if (expected.cbpkt_header.type != actual.cbpkt_header.type) return false; + if (expected.cbpkt_header.dlen != actual.cbpkt_header.dlen) return false; + if (expected.cbpkt_header.instrument != actual.cbpkt_header.instrument) return false; + + // Compare payload (first dlen quadlets after header) + const auto* expected_bytes = reinterpret_cast(&expected); + const auto* actual_bytes = reinterpret_cast(&actual); + + const size_t payload_size = expected.cbpkt_header.dlen * 4; + return std::memcmp(expected_bytes + cbPKT_HEADER_SIZE, + actual_bytes + cbPKT_HEADER_SIZE, + payload_size) == 0; +} + +} // namespace test_helpers diff --git a/tests/unit/packet_test_helpers.h b/tests/unit/packet_test_helpers.h new file mode 100644 index 00000000..0d6a3bde --- /dev/null +++ b/tests/unit/packet_test_helpers.h @@ -0,0 +1,200 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file packet_test_helpers.h +/// @author CereLink Development Team +/// @date 2025-01-19 +/// +/// @brief Helper functions for creating test packets in different protocol versions +/// +/// Provides factory functions to generate well-formed packets for testing packet translation +/// between protocol versions 3.11, 4.0, 4.10, and current (4.2+). +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CERELINK_PACKET_TEST_HELPERS_H +#define CERELINK_PACKET_TEST_HELPERS_H + +#include +#include +#include +#include + +namespace test_helpers { + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Protocol Version Constants +/// @{ + +/// Protocol 3.11: 32-bit timestamps, 8-byte header +/// Layout: time(32b) chid(16b) type(8b) dlen(8b) +constexpr size_t HEADER_SIZE_311 = 8; + +/// Protocol 4.0: 64-bit timestamps, 16-byte header, 8-bit type +/// Layout: time(64b) chid(16b) type(8b) dlen(16b) instrument(8b) reserved(16b) +constexpr size_t HEADER_SIZE_400 = 16; + +/// Protocol 4.10: Same header as current (only payload differences for some packets) +/// Layout: time(64b) chid(16b) type(16b) dlen(16b) instrument(8b) reserved(8b) +constexpr size_t HEADER_SIZE_410 = 16; + +/// Current protocol (4.2+): 64-bit timestamps, 16-byte header, 16-bit type +/// Layout: time(64b) chid(16b) type(16b) dlen(16b) instrument(8b) reserved(8b) +constexpr size_t HEADER_SIZE_CURRENT = 16; + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Header Factory Functions +/// @{ + +/// Create a protocol 3.11 header (8 bytes) +/// @param time 32-bit timestamp in 30kHz ticks +/// @param chid Channel ID +/// @param type Packet type (8-bit) +/// @param dlen Payload length in quadlets (8-bit, max 255) +/// @return 8-byte header in 3.11 format +std::vector make_311_header(uint32_t time, uint16_t chid, uint8_t type, uint8_t dlen); + +/// Create a protocol 4.0 header (16 bytes) +/// @param time 64-bit timestamp in nanoseconds +/// @param chid Channel ID +/// @param type Packet type (8-bit) +/// @param dlen Payload length in quadlets (16-bit) +/// @param instrument Instrument ID (0-3) +/// @return 16-byte header in 4.0 format +std::vector make_400_header(uint64_t time, uint16_t chid, uint8_t type, + uint16_t dlen, uint8_t instrument); + +/// Create a current protocol header (16 bytes) +/// @param time 64-bit timestamp in nanoseconds +/// @param chid Channel ID +/// @param type Packet type (16-bit) +/// @param dlen Payload length in quadlets (16-bit) +/// @param instrument Instrument ID (0-3) +/// @return Filled cbPKT_HEADER structure +cbPKT_HEADER make_current_header(uint64_t time, uint16_t chid, uint16_t type, + uint16_t dlen, uint8_t instrument); + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Legacy Protocol (3.11) Packet Factories +/// @{ + +/// Create a protocol 3.11 SYSPROTOCOLMONITOR packet +/// @param sentpkts Number of packets sent +/// @param time Header timestamp (30kHz ticks) +/// @return Complete packet as byte vector +std::vector make_311_SYSPROTOCOLMONITOR(uint32_t sentpkts, uint32_t time = 1000); + +/// Create a protocol 3.11 NPLAY packet +/// @param ftime File time in 30kHz ticks +/// @param stime Start time in 30kHz ticks +/// @param etime End time in 30kHz ticks +/// @param val Current value in 30kHz ticks +/// @param mode Play mode +/// @return Complete packet as byte vector +std::vector make_311_NPLAY(uint32_t ftime, uint32_t stime, uint32_t etime, + uint32_t val, uint32_t mode); + +/// Create a protocol 3.11 COMMENT packet +/// @param flags Comment flags (0x00=RGBA, 0x01=timeStarted) +/// @param data RGBA color (if flags=0) or timeStarted in ticks (if flags=1) +/// @param comment Comment text (null-terminated) +/// @param time Header timestamp (30kHz ticks) +/// @return Complete packet as byte vector +std::vector make_311_COMMENT(uint8_t flags, uint32_t data, + const char* comment, uint32_t time = 1000); + +/// Create a protocol 3.11 DINP (Digital Input) packet +/// Note: 3.11 DINP has a different structure than 4.0+ +/// @return Complete packet as byte vector +std::vector make_311_DINP(uint32_t time = 1000); + +/// Create a protocol 3.11 CHANINFO packet +/// @param chan Channel number +/// @param monsource Monitor source (32-bit in 3.11, becomes moninst in 4.1+) +/// @return Complete packet as byte vector +std::vector make_311_CHANINFO(uint32_t chan, uint32_t monsource); + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Protocol 4.0 Packet Factories +/// @{ + +/// Create a protocol 4.0 SYSPROTOCOLMONITOR packet +/// Note: 4.0 has same payload structure as 3.11 (no counter field) +std::vector make_400_SYSPROTOCOLMONITOR(uint32_t sentpkts, uint64_t time = 1000000); + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Protocol 4.10 Packet Factories +/// @{ +/// Note: 4.10 header is identical to current, only some payload structures differ + +/// Create a protocol 4.10 CHANRESET packet (pre-4.2 structure) +/// @param chan Channel number +/// @param monsource Monitor source (single uint8_t, becomes moninst in 4.2+) +/// @return Complete packet as cbPKT_GENERIC +cbPKT_GENERIC make_410_CHANRESET(uint32_t chan, uint8_t monsource); + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Current Protocol Packet Factories +/// @{ + +/// Create a current protocol SYSPROTOCOLMONITOR packet +cbPKT_SYSPROTOCOLMONITOR make_current_SYSPROTOCOLMONITOR(uint32_t sentpkts, uint32_t counter); + +/// Create a current protocol NPLAY packet +cbPKT_NPLAY make_current_NPLAY(PROCTIME ftime, PROCTIME stime, PROCTIME etime, + PROCTIME val, uint32_t mode); + +/// Create a current protocol COMMENT packet +cbPKT_COMMENT make_current_COMMENT(uint8_t charset, PROCTIME timeStarted, + uint32_t rgba, const char* comment); + +/// Create a current protocol DINP packet +cbPKT_DINP make_current_DINP(uint32_t valueRead, uint32_t bitsChanged, uint32_t eventType); + +/// Create a current protocol CHANINFO packet +cbPKT_CHANINFO make_current_CHANINFO(uint32_t chan, uint16_t moninst, uint16_t monchan); + +/// Create a current protocol CHANRESET packet (4.2+ structure) +cbPKT_CHANRESET make_current_CHANRESET(uint32_t chan, uint8_t moninst, uint8_t monchan); + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Comparison Helpers +/// @{ + +/// Compare two packets for equality, ignoring minor precision differences in timestamps +/// @param expected Expected packet +/// @param actual Actual packet +/// @param tolerance Allowable difference in nanosecond timestamps +/// @return true if packets match within tolerance +bool packets_equal(const cbPKT_GENERIC& expected, const cbPKT_GENERIC& actual, + uint64_t tolerance = 0); + +/// Convert 30kHz ticks to nanoseconds (for 3.11 timestamps) +/// Uses same formula as actual translation code: multiply first, then divide +/// This preserves precision: 30000 * 1000000000 / 30000 = 1000000000 (exact) +/// vs. 30000 * (1000000000 / 30000) = 30000 * 33333 = 999990000 (loses precision) +inline uint64_t ticks_to_ns(uint32_t ticks) { + return static_cast(ticks) * 1000000000ULL / 30000ULL; +} + +/// Convert nanoseconds to 30kHz ticks (for reverse translation) +/// Uses same formula as actual translation code +inline uint32_t ns_to_ticks(uint64_t ns) { + return static_cast(ns * 30000ULL / 1000000000ULL); +} + +/// @} + +} // namespace test_helpers + +#endif // CERELINK_PACKET_TEST_HELPERS_H diff --git a/tests/unit/test_cbsdk_c_api.cpp b/tests/unit/test_cbsdk_c_api.cpp new file mode 100644 index 00000000..51a44271 --- /dev/null +++ b/tests/unit/test_cbsdk_c_api.cpp @@ -0,0 +1,478 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_cbsdk_c_api.cpp +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Unit tests for C API (cbsdk.h) +/// +/// Tests the C interface for language bindings +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "cbsdk/cbsdk.h" +#include + +/// Test fixture for C API tests +class CbsdkCApiTest : public ::testing::Test { +protected: + void SetUp() override { + test_name = "test_capi_" + std::to_string(test_counter++); + } + + void TearDown() override { + // Cleanup happens via cbsdk_session_destroy() + } + + std::string test_name; + static int test_counter; +}; + +int CbsdkCApiTest::test_counter = 0; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, Config_Default) { + cbsdk_config_t config = cbsdk_config_default(); + + EXPECT_EQ(config.device_type, CBPROTO_DEVICE_TYPE_LEGACY_NSP); + EXPECT_EQ(config.callback_queue_depth, 16384); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Creation Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, Create_NullSessionPointer) { + cbsdk_config_t config = cbsdk_config_default(); + cbsdk_result_t result = cbsdk_session_create(nullptr, &config); + EXPECT_EQ(result, CBSDK_RESULT_INVALID_PARAMETER); +} + +TEST_F(CbsdkCApiTest, Create_NullConfig) { + cbsdk_session_t session = nullptr; + cbsdk_result_t result = cbsdk_session_create(&session, nullptr); + EXPECT_EQ(result, CBSDK_RESULT_INVALID_PARAMETER); +} + +TEST_F(CbsdkCApiTest, Create_Success) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + + cbsdk_session_t session = nullptr; + cbsdk_result_t result = cbsdk_session_create(&session, &config); + + EXPECT_EQ(result, CBSDK_RESULT_SUCCESS); + EXPECT_NE(session, nullptr); + + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, Destroy_NullSession) { + // Should not crash + cbsdk_session_destroy(nullptr); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Lifecycle Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, StartStop) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + // Start session + EXPECT_EQ(cbsdk_session_start(session), CBSDK_RESULT_SUCCESS); + EXPECT_TRUE(cbsdk_session_is_running(session)); + + // Stop session + cbsdk_session_stop(session); + EXPECT_FALSE(cbsdk_session_is_running(session)); + + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, StartTwice_Error) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + // config.recv_port =53005; + // config.send_port =53006; + + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + // start() is idempotent - calling it when already running returns SUCCESS + EXPECT_EQ(cbsdk_session_start(session), CBSDK_RESULT_SUCCESS); + EXPECT_EQ(cbsdk_session_start(session), CBSDK_RESULT_SUCCESS); + + cbsdk_session_stop(session); + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, IsRunning_NullSession) { + EXPECT_FALSE(cbsdk_session_is_running(nullptr)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Callback Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +static int g_packet_callback_count = 0; +static int g_error_callback_count = 0; + +static void packet_callback(const cbPKT_GENERIC* pkts, size_t count, void* user_data) { + g_packet_callback_count++; + int* counter = static_cast(user_data); + if (counter) { + (*counter)++; + } +} + +static void error_callback(const char* error_message, void* user_data) { + g_error_callback_count++; + int* counter = static_cast(user_data); + if (counter) { + (*counter)++; + } +} + +TEST_F(CbsdkCApiTest, SetCallbacks) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + // config.recv_port =53007; + // config.send_port =53008; + + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + int packet_user_data = 0; + int error_user_data = 0; + + cbsdk_session_set_packet_callback(session, packet_callback, &packet_user_data); + cbsdk_session_set_error_callback(session, error_callback, &error_user_data); + + // Callbacks set successfully (no crash) + EXPECT_EQ(packet_user_data, 0); + EXPECT_EQ(error_user_data, 0); + + cbsdk_session_destroy(session); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Statistics Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, Statistics_Valid) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + cbsdk_stats_t stats; + cbsdk_session_get_stats(session, &stats); + + // With a real device, packets may already be flowing; just verify no drops + EXPECT_GE(stats.packets_received_from_device, stats.packets_stored_to_shmem); + EXPECT_EQ(stats.packets_dropped, 0); + + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, Statistics_GetStats_NullSession) { + cbsdk_stats_t stats; + cbsdk_session_get_stats(nullptr, &stats); + // Should not crash +} + +TEST_F(CbsdkCApiTest, Statistics_GetStats_NullStats) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + // config.recv_port =53011; + // config.send_port =53012; + + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + cbsdk_session_get_stats(session, nullptr); + // Should not crash + + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, Statistics_ResetStats) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + // config.recv_port =53013; + // config.send_port =53014; + + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + cbsdk_session_reset_stats(session); + + cbsdk_stats_t stats; + cbsdk_session_get_stats(session, &stats); + EXPECT_EQ(stats.packets_received_from_device, 0); + + cbsdk_session_destroy(session); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Typed Callback Registration Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +static void event_callback(const cbPKT_GENERIC* pkt, void* user_data) { + int* counter = static_cast(user_data); + if (counter) (*counter)++; +} + +static void group_callback(const cbPKT_GROUP* pkt, void* user_data) { + int* counter = static_cast(user_data); + if (counter) (*counter)++; +} + +static void config_callback(const cbPKT_GENERIC* pkt, void* user_data) { + int* counter = static_cast(user_data); + if (counter) (*counter)++; +} + +TEST_F(CbsdkCApiTest, RegisterPacketCallback_NullSession) { + int counter = 0; + EXPECT_EQ(cbsdk_session_register_packet_callback(nullptr, packet_callback, &counter), 0); +} + +TEST_F(CbsdkCApiTest, RegisterPacketCallback_NullCallback) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + EXPECT_EQ(cbsdk_session_register_packet_callback(session, nullptr, nullptr), 0); + + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, RegisterTypedCallbacks) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + int counter = 0; + + // Register each typed callback and verify we get a valid handle + cbsdk_callback_handle_t h1 = cbsdk_session_register_packet_callback( + session, packet_callback, &counter); + EXPECT_NE(h1, 0); + + cbsdk_callback_handle_t h2 = cbsdk_session_register_event_callback( + session, CBPROTO_CHANNEL_TYPE_FRONTEND, event_callback, &counter); + EXPECT_NE(h2, 0); + + cbsdk_callback_handle_t h3 = cbsdk_session_register_group_callback( + session, CBPROTO_GROUP_RATE_30000Hz, group_callback, &counter); + EXPECT_NE(h3, 0); + + cbsdk_callback_handle_t h4 = cbsdk_session_register_config_callback( + session, 0x01, config_callback, &counter); + EXPECT_NE(h4, 0); + + // All handles should be unique + EXPECT_NE(h1, h2); + EXPECT_NE(h2, h3); + EXPECT_NE(h3, h4); + + // Unregister all (should not crash) + cbsdk_session_unregister_callback(session, h1); + cbsdk_session_unregister_callback(session, h2); + cbsdk_session_unregister_callback(session, h3); + cbsdk_session_unregister_callback(session, h4); + + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, RegisterEventCallback_AnyChannel) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + int counter = 0; + // -1 cast to cbproto_channel_type_t = ANY + cbsdk_callback_handle_t h = cbsdk_session_register_event_callback( + session, (cbproto_channel_type_t)(-1), event_callback, &counter); + EXPECT_NE(h, 0); + + cbsdk_session_unregister_callback(session, h); + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, UnregisterCallback_NullSession) { + // Should not crash + cbsdk_session_unregister_callback(nullptr, 1); +} + +TEST_F(CbsdkCApiTest, UnregisterCallback_ZeroHandle) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + // Should not crash + cbsdk_session_unregister_callback(session, 0); + + cbsdk_session_destroy(session); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Access Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, GetRunlevel_NullSession) { + EXPECT_EQ(cbsdk_session_get_runlevel(nullptr), 0); +} + +TEST_F(CbsdkCApiTest, GetChannelLabel_NullSession) { + EXPECT_EQ(cbsdk_session_get_channel_label(nullptr, 1), nullptr); +} + +TEST_F(CbsdkCApiTest, GetChannelSmpgroup_NullSession) { + EXPECT_EQ(cbsdk_session_get_channel_smpgroup(nullptr, 1), 0); +} + +TEST_F(CbsdkCApiTest, GetChannelChancaps_NullSession) { + EXPECT_EQ(cbsdk_session_get_channel_chancaps(nullptr, 1), 0); +} + +TEST_F(CbsdkCApiTest, GetGroupLabel_NullSession) { + EXPECT_EQ(cbsdk_session_get_group_label(nullptr, 1), nullptr); +} + +TEST_F(CbsdkCApiTest, GetConstants) { + EXPECT_GT(cbsdk_get_max_chans(), 0); + EXPECT_GT(cbsdk_get_num_fe_chans(), 0); + EXPECT_GT(cbsdk_get_num_analog_chans(), 0); + EXPECT_GE(cbsdk_get_max_chans(), cbsdk_get_num_analog_chans()); +} + +TEST_F(CbsdkCApiTest, ConfigAccess_WithSession) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + // These may return NULL/0 without a device, but must not crash + cbsdk_session_get_channel_label(session, 1); + cbsdk_session_get_channel_label(session, 0); // Invalid channel + cbsdk_session_get_channel_label(session, 99999); // Out of range + cbsdk_session_get_channel_smpgroup(session, 1); + cbsdk_session_get_channel_chancaps(session, 1); + cbsdk_session_get_group_label(session, 1); + cbsdk_session_get_group_label(session, 0); // Invalid group + cbsdk_session_get_runlevel(session); + + cbsdk_session_destroy(session); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel Configuration Tests (NULL safety) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, SetChannelSampleGroup_NullSession) { + EXPECT_EQ(cbsdk_session_set_channel_sample_group(nullptr, 256, + CBPROTO_CHANNEL_TYPE_FRONTEND, CBPROTO_GROUP_RATE_30000Hz, false), CBSDK_RESULT_INVALID_PARAMETER); +} + +TEST_F(CbsdkCApiTest, SetChannelConfig_NullSession) { + cbPKT_CHANINFO info = {}; + EXPECT_EQ(cbsdk_session_set_channel_config(nullptr, &info), CBSDK_RESULT_INVALID_PARAMETER); +} + +TEST_F(CbsdkCApiTest, SetChannelConfig_NullChaninfo) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + EXPECT_EQ(cbsdk_session_set_channel_config(session, nullptr), CBSDK_RESULT_INVALID_PARAMETER); + + cbsdk_session_destroy(session); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Command Tests (NULL safety) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, SendComment_NullSession) { + EXPECT_EQ(cbsdk_session_send_comment(nullptr, "test", 0, 0), CBSDK_RESULT_INVALID_PARAMETER); +} + +TEST_F(CbsdkCApiTest, SendComment_NullComment) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + EXPECT_EQ(cbsdk_session_send_comment(session, nullptr, 0, 0), CBSDK_RESULT_INVALID_PARAMETER); + + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, SendPacket_NullSession) { + cbPKT_GENERIC pkt = {}; + EXPECT_EQ(cbsdk_session_send_packet(nullptr, &pkt), CBSDK_RESULT_INVALID_PARAMETER); +} + +TEST_F(CbsdkCApiTest, SendPacket_NullPacket) { + cbsdk_config_t config = cbsdk_config_default(); + config.device_type = CBPROTO_DEVICE_TYPE_HUB1; + cbsdk_session_t session = nullptr; + ASSERT_EQ(cbsdk_session_create(&session, &config), CBSDK_RESULT_SUCCESS); + + EXPECT_EQ(cbsdk_session_send_packet(session, nullptr), CBSDK_RESULT_INVALID_PARAMETER); + + cbsdk_session_destroy(session); +} + +TEST_F(CbsdkCApiTest, SetDigitalOutput_NullSession) { + EXPECT_EQ(cbsdk_session_set_digital_output(nullptr, 1, 0), CBSDK_RESULT_INVALID_PARAMETER); +} + +TEST_F(CbsdkCApiTest, SetRunlevel_NullSession) { + EXPECT_EQ(cbsdk_session_set_runlevel(nullptr, 0), CBSDK_RESULT_INVALID_PARAMETER); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Error Handling Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, ErrorMessage_AllCodes) { + EXPECT_STRNE(cbsdk_get_error_message(CBSDK_RESULT_SUCCESS), ""); + EXPECT_STRNE(cbsdk_get_error_message(CBSDK_RESULT_INVALID_PARAMETER), ""); + EXPECT_STRNE(cbsdk_get_error_message(CBSDK_RESULT_ALREADY_RUNNING), ""); + EXPECT_STRNE(cbsdk_get_error_message(CBSDK_RESULT_NOT_RUNNING), ""); + EXPECT_STRNE(cbsdk_get_error_message(CBSDK_RESULT_SHMEM_ERROR), ""); + EXPECT_STRNE(cbsdk_get_error_message(CBSDK_RESULT_DEVICE_ERROR), ""); + EXPECT_STRNE(cbsdk_get_error_message(CBSDK_RESULT_INTERNAL_ERROR), ""); +} + +TEST_F(CbsdkCApiTest, ErrorMessage_InvalidCode) { + const char* msg = cbsdk_get_error_message((cbsdk_result_t)9999); + EXPECT_STRNE(msg, ""); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Version Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(CbsdkCApiTest, Version) { + const char* version = cbsdk_get_version(); + EXPECT_NE(version, nullptr); + EXPECT_GT(strlen(version), 0); +} diff --git a/tests/unit/test_ccf_config.cpp b/tests/unit/test_ccf_config.cpp new file mode 100644 index 00000000..64690e52 --- /dev/null +++ b/tests/unit/test_ccf_config.cpp @@ -0,0 +1,476 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_ccf_config.cpp +/// @brief Tests for CCF <-> DeviceConfig conversion functions +/// +/// Validates extractDeviceConfig() and buildConfigPackets() produce correct results, +/// following the same algorithms as Central's ReadCCFOfNSP() and SendCCF(). +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Test fixture for CCF config conversion tests +/////////////////////////////////////////////////////////////////////////////////////////////////// +class CcfConfigTest : public ::testing::Test { +protected: + cbproto::DeviceConfig device_config{}; + cbCCF ccf_data{}; + + void SetUp() override + { + std::memset(&device_config, 0, sizeof(device_config)); + std::memset(&ccf_data, 0, sizeof(ccf_data)); + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name extractDeviceConfig tests +/// @{ + +TEST_F(CcfConfigTest, ExtractChannels) +{ + // Populate a few channels with test data + device_config.chaninfo[0].chan = 1; + device_config.chaninfo[0].proc = 1; + device_config.chaninfo[0].smpfilter = 42; + device_config.chaninfo[0].spkfilter = 7; + + device_config.chaninfo[5].chan = 6; + device_config.chaninfo[5].proc = 1; + device_config.chaninfo[5].ainpopts = 0x1234; + + ccf::extractDeviceConfig(device_config, ccf_data); + + EXPECT_EQ(ccf_data.isChan[0].chan, 1u); + EXPECT_EQ(ccf_data.isChan[0].proc, 1u); + EXPECT_EQ(ccf_data.isChan[0].smpfilter, 42u); + EXPECT_EQ(ccf_data.isChan[0].spkfilter, 7u); + EXPECT_EQ(ccf_data.isChan[5].chan, 6u); + EXPECT_EQ(ccf_data.isChan[5].ainpopts, 0x1234u); + + // Unpopulated channels should be zero + EXPECT_EQ(ccf_data.isChan[10].chan, 0u); +} + +TEST_F(CcfConfigTest, ExtractFilters) +{ + // Set custom digital filters at the expected offset in the 32-filter array + device_config.filtinfo[cbFIRST_DIGITAL_FILTER].filt = 1; + device_config.filtinfo[cbFIRST_DIGITAL_FILTER].hpfreq = 300000; // 300 Hz in milliHertz + device_config.filtinfo[cbFIRST_DIGITAL_FILTER + 1].filt = 2; + device_config.filtinfo[cbFIRST_DIGITAL_FILTER + 1].lpfreq = 6000000; + device_config.filtinfo[cbFIRST_DIGITAL_FILTER + 2].filt = 3; + device_config.filtinfo[cbFIRST_DIGITAL_FILTER + 3].filt = 4; + + ccf::extractDeviceConfig(device_config, ccf_data); + + EXPECT_EQ(ccf_data.filtinfo[0].filt, 1u); + EXPECT_EQ(ccf_data.filtinfo[0].hpfreq, 300000u); + EXPECT_EQ(ccf_data.filtinfo[1].filt, 2u); + EXPECT_EQ(ccf_data.filtinfo[1].lpfreq, 6000000u); + EXPECT_EQ(ccf_data.filtinfo[2].filt, 3u); + EXPECT_EQ(ccf_data.filtinfo[3].filt, 4u); +} + +TEST_F(CcfConfigTest, ExtractSpikeSorting) +{ + device_config.spike_sorting.detect.fThreshold = 5.5f; + device_config.spike_sorting.detect.cbpkt_header.type = 0xD0; + device_config.spike_sorting.artifact_reject.nMaxSimulChans = 3; + device_config.spike_sorting.artifact_reject.cbpkt_header.type = 0xD1; + device_config.spike_sorting.noise_boundary[0].chan = 1; + device_config.spike_sorting.noise_boundary[0].afc[0] = 1.0f; + device_config.spike_sorting.statistics.nAutoalg = 5; + device_config.spike_sorting.statistics.cbpkt_header.type = 0xD3; + + ccf::extractDeviceConfig(device_config, ccf_data); + + EXPECT_FLOAT_EQ(ccf_data.isSS_Detect.fThreshold, 5.5f); + EXPECT_EQ(ccf_data.isSS_ArtifactReject.nMaxSimulChans, 3u); + EXPECT_EQ(ccf_data.isSS_NoiseBoundary[0].chan, 1u); + EXPECT_FLOAT_EQ(ccf_data.isSS_NoiseBoundary[0].afc[0], 1.0f); + EXPECT_EQ(ccf_data.isSS_Statistics.nAutoalg, 5u); +} + +TEST_F(CcfConfigTest, ExtractSysInfo) +{ + device_config.sysinfo.cbpkt_header.type = 0x10; // original type + device_config.sysinfo.cbpkt_header.dlen = 42; + + ccf::extractDeviceConfig(device_config, ccf_data); + + // extractDeviceConfig should override type to SYSSETSPKLEN + EXPECT_EQ(ccf_data.isSysInfo.cbpkt_header.type, cbPKTTYPE_SYSSETSPKLEN); + // Other fields preserved + EXPECT_EQ(ccf_data.isSysInfo.cbpkt_header.dlen, 42u); +} + +TEST_F(CcfConfigTest, ExtractWaveforms) +{ + device_config.waveform[0][0].chan = cbNUM_ANALOG_CHANS + 1; + device_config.waveform[0][0].trigNum = 0; + device_config.waveform[0][0].active = 1; + device_config.waveform[0][0].mode = 2; + + device_config.waveform[1][2].chan = cbNUM_ANALOG_CHANS + 2; + device_config.waveform[1][2].trigNum = 2; + device_config.waveform[1][2].active = 1; + + ccf::extractDeviceConfig(device_config, ccf_data); + + // Waveform data should be copied + EXPECT_EQ(ccf_data.isWaveform[0][0].chan, static_cast(cbNUM_ANALOG_CHANS + 1)); + EXPECT_EQ(ccf_data.isWaveform[0][0].mode, 2u); + EXPECT_EQ(ccf_data.isWaveform[1][2].chan, static_cast(cbNUM_ANALOG_CHANS + 2)); + + // Active flag must be cleared + EXPECT_EQ(ccf_data.isWaveform[0][0].active, 0u); + EXPECT_EQ(ccf_data.isWaveform[1][2].active, 0u); +} + +TEST_F(CcfConfigTest, ExtractSSStatus) +{ + device_config.spike_sorting.status.cntlNumUnits.nMode = ADAPT_ALWAYS; + device_config.spike_sorting.status.cntlNumUnits.fElapsedMinutes = 5.0f; + device_config.spike_sorting.status.cntlUnitStats.nMode = ADAPT_TIMED; + device_config.spike_sorting.status.cntlUnitStats.fElapsedMinutes = 10.0f; + device_config.spike_sorting.status.cbpkt_header.type = 0xD5; + + ccf::extractDeviceConfig(device_config, ccf_data); + + // Mode preserved + EXPECT_EQ(ccf_data.isSS_Status.cntlNumUnits.nMode, static_cast(ADAPT_ALWAYS)); + EXPECT_EQ(ccf_data.isSS_Status.cntlUnitStats.nMode, static_cast(ADAPT_TIMED)); + + // Elapsed minutes must be set to 99 + EXPECT_FLOAT_EQ(ccf_data.isSS_Status.cntlNumUnits.fElapsedMinutes, 99.0f); + EXPECT_FLOAT_EQ(ccf_data.isSS_Status.cntlUnitStats.fElapsedMinutes, 99.0f); +} + +TEST_F(CcfConfigTest, ExtractNTrodes) +{ + device_config.ntrodeinfo[0].ntrode = 1; + std::strncpy(device_config.ntrodeinfo[0].label, "Stereo1", cbLEN_STR_LABEL - 1); + device_config.ntrodeinfo[0].nSite = 2; + + ccf::extractDeviceConfig(device_config, ccf_data); + + EXPECT_EQ(ccf_data.isNTrodeInfo[0].ntrode, 1u); + EXPECT_STREQ(ccf_data.isNTrodeInfo[0].label, "Stereo1"); + EXPECT_EQ(ccf_data.isNTrodeInfo[0].nSite, 2u); +} + +TEST_F(CcfConfigTest, ExtractAdaptInfo) +{ + device_config.adaptinfo.nMode = 1; + device_config.adaptinfo.dLearningRate = 5e-12f; + device_config.adaptinfo.nRefChan1 = 3; + + ccf::extractDeviceConfig(device_config, ccf_data); + + EXPECT_EQ(ccf_data.isAdaptInfo.nMode, 1u); + EXPECT_FLOAT_EQ(ccf_data.isAdaptInfo.dLearningRate, 5e-12f); + EXPECT_EQ(ccf_data.isAdaptInfo.nRefChan1, 3u); +} + +TEST_F(CcfConfigTest, ExtractLnc) +{ + device_config.lnc.lncFreq = 60; + device_config.lnc.lncRefChan = 5; + device_config.lnc.cbpkt_header.type = 0xA6; + + ccf::extractDeviceConfig(device_config, ccf_data); + + EXPECT_EQ(ccf_data.isLnc.lncFreq, 60u); + EXPECT_EQ(ccf_data.isLnc.lncRefChan, 5u); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name buildConfigPackets tests +/// @{ + +TEST_F(CcfConfigTest, ChannelPackets) +{ + ccf_data.isChan[0].chan = 1; + ccf_data.isChan[0].proc = 1; + ccf_data.isChan[0].cbpkt_header.dlen = + (sizeof(cbPKT_CHANINFO) - sizeof(cbPKT_HEADER)) / 4; + + auto packets = ccf::buildConfigPackets(ccf_data); + + // Should produce at least one packet + ASSERT_GE(packets.size(), 1u); + + // Find the CHANSET packet + bool found = false; + for (const auto& pkt : packets) + { + if (pkt.cbpkt_header.type == cbPKTTYPE_CHANSET) + { + found = true; + // instrument should be proc - 1 (0-based) + EXPECT_EQ(pkt.cbpkt_header.instrument, 0u); + break; + } + } + EXPECT_TRUE(found) << "Expected a CHANSET packet"; +} + +TEST_F(CcfConfigTest, FilterPackets) +{ + ccf_data.filtinfo[0].filt = 1; + ccf_data.filtinfo[0].hpfreq = 300000; + ccf_data.filtinfo[0].cbpkt_header.dlen = + (sizeof(cbPKT_FILTINFO) - sizeof(cbPKT_HEADER)) / 4; + + auto packets = ccf::buildConfigPackets(ccf_data); + + ASSERT_GE(packets.size(), 1u); + + bool found = false; + for (const auto& pkt : packets) + { + if (pkt.cbpkt_header.type == cbPKTTYPE_FILTSET) + { + found = true; + break; + } + } + EXPECT_TRUE(found) << "Expected a FILTSET packet"; +} + +TEST_F(CcfConfigTest, EmptyFieldsSkipped) +{ + // Zero-initialized cbCCF should produce no packets + auto packets = ccf::buildConfigPackets(ccf_data); + EXPECT_EQ(packets.size(), 0u) << "Zero-initialized cbCCF should produce no packets"; +} + +TEST_F(CcfConfigTest, PacketOrder) +{ + // Populate multiple field types + ccf_data.filtinfo[0].filt = 1; + ccf_data.filtinfo[0].cbpkt_header.type = 0x01; + + ccf_data.isChan[0].chan = 1; + ccf_data.isChan[0].proc = 1; + + ccf_data.isSS_Statistics.cbpkt_header.type = 0x01; + ccf_data.isSS_Statistics.nAutoalg = 5; + + ccf_data.isSysInfo.cbpkt_header.type = 0x01; + + ccf_data.isLnc.cbpkt_header.type = 0x01; + + ccf_data.isAdaptInfo.cbpkt_header.type = 0x01; + + ccf_data.isSS_Status.cbpkt_header.type = 0x01; + + auto packets = ccf::buildConfigPackets(ccf_data); + + // Verify ordering: filters, channels, SS stats, sysinfo, LNC, adapt, SS status + ASSERT_GE(packets.size(), 7u); + + // Collect the types in order + std::vector types; + for (const auto& pkt : packets) + { + types.push_back(pkt.cbpkt_header.type); + } + + // Find positions + auto pos = [&types](uint16_t t) -> int { + for (int i = 0; i < static_cast(types.size()); ++i) + if (types[i] == t) return i; + return -1; + }; + + int filtPos = pos(cbPKTTYPE_FILTSET); + int chanPos = pos(cbPKTTYPE_CHANSET); + int ssStatPos = pos(cbPKTTYPE_SS_STATISTICSSET); + int sysPos = pos(cbPKTTYPE_SYSSETSPKLEN); + int lncPos = pos(cbPKTTYPE_LNCSET); + int adaptPos = pos(cbPKTTYPE_ADAPTFILTSET); + int statusPos = pos(cbPKTTYPE_SS_STATUSSET); + + ASSERT_NE(filtPos, -1); + ASSERT_NE(chanPos, -1); + ASSERT_NE(ssStatPos, -1); + ASSERT_NE(sysPos, -1); + ASSERT_NE(lncPos, -1); + ASSERT_NE(adaptPos, -1); + ASSERT_NE(statusPos, -1); + + EXPECT_LT(filtPos, chanPos) << "Filters must come before channels"; + EXPECT_LT(chanPos, ssStatPos) << "Channels must come before SS statistics"; + EXPECT_LT(ssStatPos, sysPos) << "SS statistics must come before sysinfo"; + EXPECT_LT(sysPos, lncPos) << "Sysinfo must come before LNC"; + EXPECT_LT(lncPos, adaptPos) << "LNC must come before adaptive filter"; + EXPECT_LT(adaptPos, statusPos) << "Adaptive filter must come before SS status"; +} + +TEST_F(CcfConfigTest, SSStatusSetsAdaptNever) +{ + ccf_data.isSS_Status.cbpkt_header.type = 0x01; + ccf_data.isSS_Status.cntlNumUnits.nMode = ADAPT_ALWAYS; + + auto packets = ccf::buildConfigPackets(ccf_data); + + bool found = false; + for (const auto& pkt : packets) + { + if (pkt.cbpkt_header.type == cbPKTTYPE_SS_STATUSSET) + { + found = true; + // Interpret as cbPKT_SS_STATUS + const auto* status = reinterpret_cast(&pkt); + EXPECT_EQ(status->cntlNumUnits.nMode, static_cast(ADAPT_NEVER)); + break; + } + } + EXPECT_TRUE(found) << "Expected an SS_STATUSSET packet"; +} + +TEST_F(CcfConfigTest, WaveformsClearedActive) +{ + ccf_data.isWaveform[0][0].chan = cbNUM_ANALOG_CHANS + 1; + ccf_data.isWaveform[0][0].active = 1; + ccf_data.isWaveform[0][0].mode = 2; + + auto packets = ccf::buildConfigPackets(ccf_data); + + bool found = false; + for (const auto& pkt : packets) + { + if (pkt.cbpkt_header.type == cbPKTTYPE_WAVEFORMSET) + { + found = true; + const auto* wf = reinterpret_cast(&pkt); + EXPECT_EQ(wf->active, 0u) << "Waveform active flag must be cleared"; + EXPECT_EQ(wf->mode, 2u) << "Other waveform fields should be preserved"; + break; + } + } + EXPECT_TRUE(found) << "Expected a WAVEFORMSET packet"; +} + +TEST_F(CcfConfigTest, NTrodePackets) +{ + ccf_data.isNTrodeInfo[0].ntrode = 1; + std::strncpy(ccf_data.isNTrodeInfo[0].label, "NT1", cbLEN_STR_LABEL - 1); + + auto packets = ccf::buildConfigPackets(ccf_data); + + bool found = false; + for (const auto& pkt : packets) + { + if (pkt.cbpkt_header.type == cbPKTTYPE_SETNTRODEINFO) + { + found = true; + const auto* nt = reinterpret_cast(&pkt); + EXPECT_EQ(nt->ntrode, 1u); + break; + } + } + EXPECT_TRUE(found) << "Expected a SETNTRODEINFO packet"; +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Round-trip test +/// @{ + +TEST_F(CcfConfigTest, RoundTrip) +{ + // Populate DeviceConfig with representative data + device_config.chaninfo[0].chan = 1; + device_config.chaninfo[0].proc = 1; + device_config.chaninfo[0].smpfilter = 13; + device_config.chaninfo[0].cbpkt_header.type = 0x40; + + device_config.filtinfo[cbFIRST_DIGITAL_FILTER].filt = 1; + device_config.filtinfo[cbFIRST_DIGITAL_FILTER].hpfreq = 250000; + device_config.filtinfo[cbFIRST_DIGITAL_FILTER].cbpkt_header.type = 0xA2; + + device_config.spike_sorting.detect.fThreshold = 4.5f; + device_config.spike_sorting.detect.cbpkt_header.type = 0xD0; + + device_config.spike_sorting.statistics.nAutoalg = 5; + device_config.spike_sorting.statistics.cbpkt_header.type = 0xD3; + + device_config.sysinfo.cbpkt_header.type = 0x10; + + device_config.lnc.lncFreq = 50; + device_config.lnc.cbpkt_header.type = 0xA6; + + device_config.adaptinfo.nMode = 1; + device_config.adaptinfo.cbpkt_header.type = 0xA4; + + device_config.spike_sorting.status.cbpkt_header.type = 0xD5; + device_config.spike_sorting.status.cntlNumUnits.nMode = ADAPT_ALWAYS; + + // Extract to CCF + ccf::extractDeviceConfig(device_config, ccf_data); + + // Build packets from CCF + auto packets = ccf::buildConfigPackets(ccf_data); + + // Verify expected packet types are present + std::set expected_types = { + cbPKTTYPE_FILTSET, + cbPKTTYPE_CHANSET, + cbPKTTYPE_SS_DETECTSET, + cbPKTTYPE_SS_STATISTICSSET, + cbPKTTYPE_SYSSETSPKLEN, + cbPKTTYPE_LNCSET, + cbPKTTYPE_ADAPTFILTSET, + cbPKTTYPE_SS_STATUSSET, + }; + + std::set actual_types; + for (const auto& pkt : packets) + { + actual_types.insert(pkt.cbpkt_header.type); + } + + for (uint16_t t : expected_types) + { + EXPECT_TRUE(actual_types.count(t)) + << "Missing packet type 0x" << std::hex << t; + } + + // Verify specific packet contents survived the round trip + for (const auto& pkt : packets) + { + if (pkt.cbpkt_header.type == cbPKTTYPE_CHANSET) + { + const auto* ch = reinterpret_cast(&pkt); + if (ch->chan == 1) + { + EXPECT_EQ(ch->smpfilter, 13u); + EXPECT_EQ(ch->cbpkt_header.instrument, 0u); // proc=1 -> instrument=0 + } + } + else if (pkt.cbpkt_header.type == cbPKTTYPE_FILTSET) + { + const auto* f = reinterpret_cast(&pkt); + if (f->filt == 1) + { + EXPECT_EQ(f->hpfreq, 250000u); + } + } + else if (pkt.cbpkt_header.type == cbPKTTYPE_SS_STATUSSET) + { + const auto* s = reinterpret_cast(&pkt); + EXPECT_EQ(s->cntlNumUnits.nMode, static_cast(ADAPT_NEVER)); + } + } +} + +/// @} diff --git a/tests/unit/test_ccf_xml.cpp b/tests/unit/test_ccf_xml.cpp new file mode 100644 index 00000000..cfbbb22b --- /dev/null +++ b/tests/unit/test_ccf_xml.cpp @@ -0,0 +1,272 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_ccf_xml.cpp +/// @brief Tests for CCF XML read/write using minimal fixture files +/// +/// Validates that CCFUtils can read XML CCF files and produce correct cbCCF data, +/// and that write → read round-trips preserve all key fields. +/// These tests do NOT require a device connection. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include + +#ifndef CCF_TEST_DATA_DIR +#error "CCF_TEST_DATA_DIR must be defined to locate test fixtures" +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Test fixture for CCF XML tests +/////////////////////////////////////////////////////////////////////////////////////////////////// +class CcfXmlTest : public ::testing::Test { +protected: + cbCCF ccf_data{}; + + void SetUp() override + { + std::memset(&ccf_data, 0, sizeof(ccf_data)); + } + + std::string fixturePath(const char* filename) { + return std::string(CCF_TEST_DATA_DIR) + "/" + filename; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Read tests +/// @{ + +TEST_F(CcfXmlTest, ReadRaw) +{ + std::string path = fixturePath("ccf_raw.ccf"); + + CCFUtils reader(false, &ccf_data); + auto res = reader.ReadCCF(path.c_str(), true); + ASSERT_GE(res, ccf::CCFRESULT_SUCCESS) << "ReadCCF failed with code " << res; + + // Channel 1 (index 0) + EXPECT_EQ(ccf_data.isChan[0].chan, 1u); + EXPECT_EQ(ccf_data.isChan[0].proc, 1u); + EXPECT_EQ(ccf_data.isChan[0].bank, 1u); + EXPECT_EQ(ccf_data.isChan[0].term, 1u); + + // ainpopts=64 (0x40 = cbAINP_RAWSTREAM) + EXPECT_EQ(ccf_data.isChan[0].ainpopts, 64u); + // spkopts=65792 (0x10100 = spike detection off) + EXPECT_EQ(ccf_data.isChan[0].spkopts, 65792u); + // smpfilter=0, smpgroup=0 (no continuous sampling) + EXPECT_EQ(ccf_data.isChan[0].smpfilter, 0u); + EXPECT_EQ(ccf_data.isChan[0].smpgroup, 0u); + // lncrate=10000 + EXPECT_EQ(ccf_data.isChan[0].lncrate, 10000u); + + // Channel 2 (index 1) — same config pattern + EXPECT_EQ(ccf_data.isChan[1].chan, 2u); + EXPECT_EQ(ccf_data.isChan[1].ainpopts, 64u); + EXPECT_EQ(ccf_data.isChan[1].spkopts, 65792u); + EXPECT_EQ(ccf_data.isChan[1].smpfilter, 0u); + EXPECT_EQ(ccf_data.isChan[1].smpgroup, 0u); + EXPECT_EQ(ccf_data.isChan[1].lncrate, 10000u); + + // Channel 3+ should be zero (not present in fixture) + EXPECT_EQ(ccf_data.isChan[2].chan, 0u); +} + +TEST_F(CcfXmlTest, ReadSpk1k) +{ + std::string path = fixturePath("ccf_spk_1k.ccf"); + + CCFUtils reader(false, &ccf_data); + auto res = reader.ReadCCF(path.c_str(), true); + ASSERT_GE(res, ccf::CCFRESULT_SUCCESS) << "ReadCCF failed with code " << res; + + // Channel 1 (index 0) + EXPECT_EQ(ccf_data.isChan[0].chan, 1u); + EXPECT_EQ(ccf_data.isChan[0].proc, 1u); + + // ainpopts=258 (0x102 = cbAINP_LNC | cbAINP_SPKSTREAM) + EXPECT_EQ(ccf_data.isChan[0].ainpopts, 258u); + // spkopts=65793 (0x10101 = spike extraction on) + EXPECT_EQ(ccf_data.isChan[0].spkopts, 65793u); + // smpfilter=6 (digital filter index 6) + EXPECT_EQ(ccf_data.isChan[0].smpfilter, 6u); + // smpgroup=2 (1 kHz continuous group) + EXPECT_EQ(ccf_data.isChan[0].smpgroup, 2u); + // lncrate=10000 + EXPECT_EQ(ccf_data.isChan[0].lncrate, 10000u); + + // Channel 2 (index 1) — same config + EXPECT_EQ(ccf_data.isChan[1].chan, 2u); + EXPECT_EQ(ccf_data.isChan[1].ainpopts, 258u); + EXPECT_EQ(ccf_data.isChan[1].spkopts, 65793u); + EXPECT_EQ(ccf_data.isChan[1].smpfilter, 6u); + EXPECT_EQ(ccf_data.isChan[1].smpgroup, 2u); +} + +TEST_F(CcfXmlTest, ReadDifferentConfigs) +{ + // Read both CCFs and verify they have DIFFERENT values for key fields + cbCCF raw{}, filtered{}; + std::memset(&raw, 0, sizeof(raw)); + std::memset(&filtered, 0, sizeof(filtered)); + + CCFUtils reader_raw(false, &raw); + auto r1 = reader_raw.ReadCCF(fixturePath("ccf_raw.ccf").c_str(), true); + ASSERT_GE(r1, ccf::CCFRESULT_SUCCESS); + + CCFUtils reader_filt(false, &filtered); + auto r2 = reader_filt.ReadCCF(fixturePath("ccf_spk_1k.ccf").c_str(), true); + ASSERT_GE(r2, ccf::CCFRESULT_SUCCESS); + + // Key differences between the two configs + EXPECT_NE(raw.isChan[0].ainpopts, filtered.isChan[0].ainpopts) + << "ainpopts should differ (raw=0x40 vs LNC+spk=0x102)"; + EXPECT_NE(raw.isChan[0].spkopts, filtered.isChan[0].spkopts) + << "spkopts should differ (spike detect off vs on)"; + EXPECT_NE(raw.isChan[0].smpfilter, filtered.isChan[0].smpfilter) + << "smpfilter should differ (0 vs 6)"; + EXPECT_NE(raw.isChan[0].smpgroup, filtered.isChan[0].smpgroup) + << "smpgroup should differ (0 vs 2)"; + // lncrate is the same in both + EXPECT_EQ(raw.isChan[0].lncrate, filtered.isChan[0].lncrate) + << "lncrate should be the same (10000)"; +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Write/Read round-trip tests +/// @{ + +TEST_F(CcfXmlTest, WriteReadRoundTrip) +{ + // Read the 64ch_1k fixture + std::string input_path = fixturePath("ccf_spk_1k.ccf"); + CCFUtils reader(false, &ccf_data); + auto res = reader.ReadCCF(input_path.c_str(), true); + ASSERT_GE(res, ccf::CCFRESULT_SUCCESS); + + // Write it out to a temp file + std::string temp_path = fixturePath("_test_roundtrip.ccf"); + CCFUtils writer(false, &ccf_data); + res = writer.WriteCCFNoPrompt(temp_path.c_str()); + ASSERT_EQ(res, ccf::CCFRESULT_SUCCESS) << "WriteCCFNoPrompt failed with code " << res; + + // Read the written file back + cbCCF ccf_data2{}; + std::memset(&ccf_data2, 0, sizeof(ccf_data2)); + CCFUtils reader2(false, &ccf_data2); + res = reader2.ReadCCF(temp_path.c_str(), true); + ASSERT_GE(res, ccf::CCFRESULT_SUCCESS) << "Round-trip ReadCCF failed with code " << res; + + // Compare key fields for first 2 channels + for (int i = 0; i < 2; ++i) + { + EXPECT_EQ(ccf_data.isChan[i].chan, ccf_data2.isChan[i].chan) + << "chan mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].ainpopts, ccf_data2.isChan[i].ainpopts) + << "ainpopts mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].spkopts, ccf_data2.isChan[i].spkopts) + << "spkopts mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].smpfilter, ccf_data2.isChan[i].smpfilter) + << "smpfilter mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].smpgroup, ccf_data2.isChan[i].smpgroup) + << "smpgroup mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].lncrate, ccf_data2.isChan[i].lncrate) + << "lncrate mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].proc, ccf_data2.isChan[i].proc) + << "proc mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].bank, ccf_data2.isChan[i].bank) + << "bank mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].term, ccf_data2.isChan[i].term) + << "term mismatch at index " << i; + } + + // Cleanup temp file + std::remove(temp_path.c_str()); +} + +TEST_F(CcfXmlTest, WriteReadRoundTripRaw) +{ + // Same round-trip test with the raw128 fixture + std::string input_path = fixturePath("ccf_raw.ccf"); + CCFUtils reader(false, &ccf_data); + auto res = reader.ReadCCF(input_path.c_str(), true); + ASSERT_GE(res, ccf::CCFRESULT_SUCCESS); + + std::string temp_path = fixturePath("_test_roundtrip_raw.ccf"); + CCFUtils writer(false, &ccf_data); + res = writer.WriteCCFNoPrompt(temp_path.c_str()); + ASSERT_EQ(res, ccf::CCFRESULT_SUCCESS); + + cbCCF ccf_data2{}; + std::memset(&ccf_data2, 0, sizeof(ccf_data2)); + CCFUtils reader2(false, &ccf_data2); + res = reader2.ReadCCF(temp_path.c_str(), true); + ASSERT_GE(res, ccf::CCFRESULT_SUCCESS); + + for (int i = 0; i < 2; ++i) + { + EXPECT_EQ(ccf_data.isChan[i].chan, ccf_data2.isChan[i].chan) + << "chan mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].ainpopts, ccf_data2.isChan[i].ainpopts) + << "ainpopts mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].spkopts, ccf_data2.isChan[i].spkopts) + << "spkopts mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].smpfilter, ccf_data2.isChan[i].smpfilter) + << "smpfilter mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].smpgroup, ccf_data2.isChan[i].smpgroup) + << "smpgroup mismatch at index " << i; + EXPECT_EQ(ccf_data.isChan[i].lncrate, ccf_data2.isChan[i].lncrate) + << "lncrate mismatch at index " << i; + } + + std::remove(temp_path.c_str()); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name buildConfigPackets integration +/// @{ + +TEST_F(CcfXmlTest, ReadThenBuildPackets) +{ + // Verify the full pipeline: read CCF → buildConfigPackets → packets have correct values + std::string path = fixturePath("ccf_spk_1k.ccf"); + CCFUtils reader(false, &ccf_data); + auto res = reader.ReadCCF(path.c_str(), true); + ASSERT_GE(res, ccf::CCFRESULT_SUCCESS); + + auto packets = ccf::buildConfigPackets(ccf_data); + + // Should have at least 2 CHANSET packets (one per channel in fixture) + int chan_count = 0; + for (const auto& pkt : packets) + { + if (pkt.cbpkt_header.type == cbPKTTYPE_CHANSET) + { + const auto* ch = reinterpret_cast(&pkt); + if (ch->chan == 1) + { + EXPECT_EQ(ch->ainpopts, 258u); + EXPECT_EQ(ch->spkopts, 65793u); + EXPECT_EQ(ch->smpfilter, 6u); + EXPECT_EQ(ch->smpgroup, 2u); + EXPECT_EQ(ch->lncrate, 10000u); + } + else if (ch->chan == 2) + { + EXPECT_EQ(ch->ainpopts, 258u); + EXPECT_EQ(ch->spkopts, 65793u); + } + ++chan_count; + } + } + EXPECT_GE(chan_count, 2) << "Expected at least 2 CHANSET packets"; +} + +/// @} diff --git a/tests/unit/test_clock_sync.cpp b/tests/unit/test_clock_sync.cpp new file mode 100644 index 00000000..4abb265c --- /dev/null +++ b/tests/unit/test_clock_sync.cpp @@ -0,0 +1,193 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_clock_sync.cpp +/// @author CereLink Development Team +/// @date 2025-02-18 +/// +/// @brief Unit tests for cbdev::ClockSync (probes-only with configurable α) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "clock_sync.h" +#include +#include +#include + +using namespace cbdev; +using SteadyTP = ClockSync::time_point; + +namespace { + +// Helper: create a time_point from a raw nanosecond count +SteadyTP tp_from_ns(int64_t ns) { + return SteadyTP(std::chrono::nanoseconds(ns)); +} + +// Helper: convert time_point to nanoseconds +int64_t tp_to_ns(SteadyTP tp) { + return std::chrono::duration_cast( + tp.time_since_epoch()).count(); +} + +} // anonymous namespace + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Basic State +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(ClockSyncTest, InitiallyNoSyncData) { + ClockSync sync; + + EXPECT_FALSE(sync.hasSyncData()); + EXPECT_FALSE(sync.toLocalTime(1000).has_value()); + EXPECT_FALSE(sync.toDeviceTime(tp_from_ns(1000)).has_value()); + EXPECT_FALSE(sync.getOffsetNs().has_value()); + EXPECT_FALSE(sync.getUncertaintyNs().has_value()); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Probe Samples (default α = 0.5, NTP midpoint) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(ClockSyncTest, ProbeSampleBasics) { + ClockSync sync; + + // T1 = 1000 ns, T3 = 2500 ns, T4 = 2000 ns + // RTT = 1000, α = 0.5 (default) + // offset = T3 - T1 - α * RTT = 2500 - 1000 - 500 = 1000 + // uncertainty = RTT/2 = 500 + sync.addProbeSample(tp_from_ns(1000), 2500, tp_from_ns(2000)); + + EXPECT_TRUE(sync.hasSyncData()); + ASSERT_TRUE(sync.getOffsetNs().has_value()); + EXPECT_EQ(*sync.getOffsetNs(), 1000); + ASSERT_TRUE(sync.getUncertaintyNs().has_value()); + EXPECT_EQ(*sync.getUncertaintyNs(), 500); +} + +TEST(ClockSyncTest, CustomAlpha) { + // With α = 2/3, the forward delay fraction shifts the estimate + ClockSync::Config config; + config.forward_delay_fraction = 2.0 / 3.0; + ClockSync sync(config); + + // Simulate known asymmetry: D1 = 1750 μs, D2 = 850 μs + // True offset = 100,000,000 ns (100 ms) + // T1 = 0, T3 = D1 + offset = 101,750,000, T4 = D1 + D2 = 2,600,000 + // RTT = 2,600,000, α = 2/3 + // offset = 101,750,000 - 0 - round(2/3 * 2,600,000) = 100,016,667 + // True offset is 100,000,000, error ≈ 16.7 μs + sync.addProbeSample(tp_from_ns(0), 101'750'000, tp_from_ns(2'600'000)); + + ASSERT_TRUE(sync.getOffsetNs().has_value()); + const int64_t estimated = *sync.getOffsetNs(); + const int64_t true_offset = 100'000'000; + const int64_t error = std::abs(estimated - true_offset); + EXPECT_LT(error, 20'000); + + EXPECT_EQ(*sync.getUncertaintyNs(), 1'300'000); +} + +TEST(ClockSyncTest, SymmetricPath) { + // With symmetric delays (D1 = D2), default α = 0.5 gives exact offset + ClockSync sync; + + // True offset = 100,000,000, D1 = D2 = 1,000,000 + // T1 = 0, T3 = D1 + offset = 101,000,000, T4 = D1 + D2 = 2,000,000 + // offset = 101,000,000 - 0 - 0.5 * 2,000,000 = 100,000,000 (exact) + sync.addProbeSample(tp_from_ns(0), 101'000'000, tp_from_ns(2'000'000)); + + EXPECT_EQ(*sync.getOffsetNs(), 100'000'000); + EXPECT_EQ(*sync.getUncertaintyNs(), 1'000'000); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Conversion Round-Trip +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(ClockSyncTest, ConversionRoundTrip) { + ClockSync sync; + + // Establish an offset with default α = 0.5 + // T1 = 10000, T3 = 15000, T4 = 11000 + // RTT = 1000, offset = 15000 - 10000 - 0.5 * 1000 = 4500 + sync.addProbeSample(tp_from_ns(10000), 15000, tp_from_ns(11000)); + + const uint64_t device_ns = 20000; + auto local = sync.toLocalTime(device_ns); + ASSERT_TRUE(local.has_value()); + + auto back = sync.toDeviceTime(*local); + ASSERT_TRUE(back.has_value()); + EXPECT_EQ(*back, device_ns); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Probe Expiry +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(ClockSyncTest, ProbeExpiry) { + ClockSync::Config config; + config.max_probe_age = std::chrono::seconds(1); // 1s expiry for testing + ClockSync sync(config); + + // Add probe at t=0 + sync.addProbeSample(tp_from_ns(0), 5100, tp_from_ns(200)); + EXPECT_TRUE(sync.hasSyncData()); + + // Add probe at t=2s (past the 1s expiry) — old probe should be pruned + const auto t2s = tp_from_ns(2'000'000'000); + sync.addProbeSample(t2s, uint64_t(2'000'005'000), tp_from_ns(2'000'000'400)); + + // Only the new probe remains + // RTT = 400, offset = 5000 - 0.5 * 400 = 4800 + ASSERT_TRUE(sync.getOffsetNs().has_value()); + EXPECT_EQ(*sync.getOffsetNs(), 4800); + EXPECT_EQ(*sync.getUncertaintyNs(), 200); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Best Probe Selection (minimum RTT) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(ClockSyncTest, MinRTTSelection) { + ClockSync sync; + + // Probe 1: RTT = 1000 (worse) + // T1 = 1000, T3 = 6500, T4 = 2000 + // offset = 6500 - 1000 - 0.5 * 1000 = 5000 + sync.addProbeSample(tp_from_ns(1000), 6500, tp_from_ns(2000)); + EXPECT_EQ(*sync.getOffsetNs(), 5000); + + // Probe 2: RTT = 200 (better — should be selected) + // T1 = 3000, T3 = 8000, T4 = 3200 + // offset = 8000 - 3000 - 0.5 * 200 = 4900 + sync.addProbeSample(tp_from_ns(3000), 8000, tp_from_ns(3200)); + + // Best probe is probe 2 (lowest RTT) + EXPECT_EQ(*sync.getOffsetNs(), 4900); + EXPECT_EQ(*sync.getUncertaintyNs(), 100); +} + +TEST(ClockSyncTest, BestProbeIsMinimumRTT) { + ClockSync sync; + + // Add 3 probes with different RTTs. The min-RTT probe should win + // regardless of insertion order. + + // Probe A: RTT = 500 + sync.addProbeSample(tp_from_ns(0), 10000, tp_from_ns(500)); + // offset = 10000 - 0 - 0.5 * 500 = 9750 + + // Probe B: RTT = 100 (best) + sync.addProbeSample(tp_from_ns(1000), 11000, tp_from_ns(1100)); + // offset = 11000 - 1000 - 0.5 * 100 = 9950 + + // Probe C: RTT = 300 + sync.addProbeSample(tp_from_ns(2000), 12000, tp_from_ns(2300)); + // offset = 12000 - 2000 - 0.5 * 300 = 9850 + + // Probe B should be selected (RTT = 100) + EXPECT_EQ(*sync.getOffsetNs(), 9950); + EXPECT_EQ(*sync.getUncertaintyNs(), 50); // RTT/2 = 100/2 = 50 +} diff --git a/tests/unit/test_cmp_parser.cpp b/tests/unit/test_cmp_parser.cpp new file mode 100644 index 00000000..34635704 --- /dev/null +++ b/tests/unit/test_cmp_parser.cpp @@ -0,0 +1,119 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_cmp_parser.cpp +/// @brief Unit tests for CMP (channel mapping) file parser +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "cmp_parser.h" + +#ifndef CMP_TEST_DATA_DIR +#define CMP_TEST_DATA_DIR "." +#endif + +static std::string testFile(const char* name) { + return std::string(CMP_TEST_DATA_DIR) + "/" + name; +} + +TEST(CmpParser, Parse8Channel) { + auto result = cbsdk::parseCmpFile(testFile("8ChannelDefaultMapping.cmp")); + ASSERT_TRUE(result.isOk()) << result.error(); + + const auto& positions = result.value(); + // 8 channels: bank A, electrodes 1-8 + EXPECT_EQ(positions.size(), 8u); + + // First entry: 0 1 A 1 chan1 → bank=1(A), term=0(electrode 1-1) + auto it = positions.find(cbsdk::cmpKey(1, 0)); + ASSERT_NE(it, positions.end()); + EXPECT_EQ(it->second[0], 0); // col + EXPECT_EQ(it->second[1], 1); // row + EXPECT_EQ(it->second[2], 1); // bank_letter (A=1) + EXPECT_EQ(it->second[3], 1); // electrode + + // Last entry: 3 0 A 8 chan8 → bank=1(A), term=7(electrode 8-1) + it = positions.find(cbsdk::cmpKey(1, 7)); + ASSERT_NE(it, positions.end()); + EXPECT_EQ(it->second[0], 3); // col + EXPECT_EQ(it->second[1], 0); // row + EXPECT_EQ(it->second[2], 1); // bank_letter (A=1) + EXPECT_EQ(it->second[3], 8); // electrode +} + +TEST(CmpParser, Parse128Channel) { + auto result = cbsdk::parseCmpFile(testFile("128ChannelDefaultMapping.cmp")); + ASSERT_TRUE(result.isOk()) << result.error(); + + const auto& positions = result.value(); + EXPECT_EQ(positions.size(), 128u); + + // First entry: 0 7 A 1 → bank=1, term=0 + auto it = positions.find(cbsdk::cmpKey(1, 0)); + ASSERT_NE(it, positions.end()); + EXPECT_EQ(it->second[0], 0); // col + EXPECT_EQ(it->second[1], 7); // row + + // Bank B entry: 0 5 B 1 chan33 → bank=2, term=0 + it = positions.find(cbsdk::cmpKey(2, 0)); + ASSERT_NE(it, positions.end()); + EXPECT_EQ(it->second[0], 0); // col + EXPECT_EQ(it->second[1], 5); // row + EXPECT_EQ(it->second[2], 2); // bank_letter (B=2) + + // Bank D, last electrode: 15 0 D 32 chan128 → bank=4, term=31 + it = positions.find(cbsdk::cmpKey(4, 31)); + ASSERT_NE(it, positions.end()); + EXPECT_EQ(it->second[0], 15); // col + EXPECT_EQ(it->second[1], 0); // row + EXPECT_EQ(it->second[2], 4); // bank_letter (D=4) + EXPECT_EQ(it->second[3], 32); // electrode +} + +TEST(CmpParser, Parse96Channel) { + auto result = cbsdk::parseCmpFile(testFile("96ChannelDefaultMapping.cmp")); + ASSERT_TRUE(result.isOk()) << result.error(); + + const auto& positions = result.value(); + EXPECT_EQ(positions.size(), 96u); + + // 96-channel has banks A, B, C (3 banks × 32 electrodes) + // Verify bank C exists + auto it = positions.find(cbsdk::cmpKey(3, 0)); + ASSERT_NE(it, positions.end()); + + // Verify bank D does NOT exist (only 96 channels) + it = positions.find(cbsdk::cmpKey(4, 0)); + EXPECT_EQ(it, positions.end()); +} + +TEST(CmpParser, BankOffset) { + // Load 8-channel CMP with bank_offset=4 (simulating port 2) + auto result = cbsdk::parseCmpFile(testFile("8ChannelDefaultMapping.cmp"), 4); + ASSERT_TRUE(result.isOk()) << result.error(); + + const auto& positions = result.value(); + EXPECT_EQ(positions.size(), 8u); + + // Bank A with offset 4 → absolute bank 5 + auto it = positions.find(cbsdk::cmpKey(5, 0)); + ASSERT_NE(it, positions.end()); + EXPECT_EQ(it->second[0], 0); // col + EXPECT_EQ(it->second[1], 1); // row + // position[2] stores the original bank letter index (A=1), not the offset bank + EXPECT_EQ(it->second[2], 1); + + // Original bank 1 should NOT have entries + it = positions.find(cbsdk::cmpKey(1, 0)); + EXPECT_EQ(it, positions.end()); +} + +TEST(CmpParser, NonexistentFile) { + auto result = cbsdk::parseCmpFile("/nonexistent/path.cmp"); + EXPECT_TRUE(result.isError()); +} + +TEST(CmpParser, CmpKeyUniqueness) { + // Verify different (bank, term) pairs produce different keys + EXPECT_NE(cbsdk::cmpKey(1, 0), cbsdk::cmpKey(2, 0)); + EXPECT_NE(cbsdk::cmpKey(1, 0), cbsdk::cmpKey(1, 1)); + EXPECT_EQ(cbsdk::cmpKey(3, 15), cbsdk::cmpKey(3, 15)); +} diff --git a/tests/unit/test_device_session.cpp b/tests/unit/test_device_session.cpp new file mode 100644 index 00000000..7e49d67c --- /dev/null +++ b/tests/unit/test_device_session.cpp @@ -0,0 +1,254 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_device_session.cpp +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Unit tests for cbdev::IDeviceSession (via createDeviceSession factory) +/// +/// Tests the device transport layer including socket creation, packet send/receive, +/// and configuration access. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "cbdev/device_session.h" +#include "cbdev/device_factory.h" +#include +#include +#include + +using namespace cbdev; + +/// Test fixture for DeviceSession tests +class DeviceSessionTest : public ::testing::Test { +protected: + void SetUp() override { + // Create unique session name for each test + test_name = "test_session_" + std::to_string(test_counter++); + } + + void TearDown() override { + // Cleanup happens automatically via RAII + } + + std::string test_name; + static int test_counter; +}; + +int DeviceSessionTest::test_counter = 0; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(DeviceSessionTest, ConnectionParams_Predefined_LegacyNSP) { + auto config = ConnectionParams::forDevice(DeviceType::LEGACY_NSP); + + EXPECT_EQ(config.type, DeviceType::LEGACY_NSP); + EXPECT_EQ(config.device_address, "192.168.137.128"); + EXPECT_EQ(config.client_address, ""); // Auto-detect + EXPECT_EQ(config.recv_port, 51002); + EXPECT_EQ(config.send_port, 51001); +} + +TEST_F(DeviceSessionTest, ConnectionParams_Predefined_Gemini) { + auto config = ConnectionParams::forDevice(DeviceType::NSP); + + EXPECT_EQ(config.type, DeviceType::NSP); + EXPECT_EQ(config.device_address, "192.168.137.128"); + EXPECT_EQ(config.client_address, ""); // Auto-detect + EXPECT_EQ(config.recv_port, 51001); // Same port for send & recv + EXPECT_EQ(config.send_port, 51001); +} + +TEST_F(DeviceSessionTest, ConnectionParams_Predefined_GeminiHub1) { + auto config = ConnectionParams::forDevice(DeviceType::HUB1); + + EXPECT_EQ(config.type, DeviceType::HUB1); + EXPECT_EQ(config.device_address, "192.168.137.200"); + EXPECT_EQ(config.client_address, ""); // Auto-detect + EXPECT_EQ(config.recv_port, 51002); // Same port for send & recv + EXPECT_EQ(config.send_port, 51002); +} + +TEST_F(DeviceSessionTest, ConnectionParams_Predefined_NPlay) { + auto config = ConnectionParams::forDevice(DeviceType::NPLAY); + + EXPECT_EQ(config.type, DeviceType::NPLAY); + EXPECT_EQ(config.device_address, "127.0.0.1"); + EXPECT_EQ(config.client_address, "127.0.0.1"); // Loopback, not 0.0.0.0 + EXPECT_EQ(config.recv_port, 51002); // LEGACY_NSP_RECV_PORT (bcast) + EXPECT_EQ(config.send_port, 51001); // LEGACY_NSP_SEND_PORT (cnt) +} + +TEST_F(DeviceSessionTest, ConnectionParams_Custom) { + auto config = ConnectionParams::custom("10.0.0.100", "10.0.0.1", 12345, 12346); + + EXPECT_EQ(config.type, DeviceType::CUSTOM); + EXPECT_EQ(config.device_address, "10.0.0.100"); + EXPECT_EQ(config.client_address, "10.0.0.1"); + EXPECT_EQ(config.recv_port, 12345); + EXPECT_EQ(config.send_port, 12346); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Lifecycle Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(DeviceSessionTest, Create_Loopback) { + // Use loopback address to avoid network interface requirements + auto config = ConnectionParams::custom("127.0.0.1", "127.0.0.1", 51001, 51002); + + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()) << "Error: " << result.error(); + + auto& session = result.value(); + EXPECT_TRUE(session->isConnected()); +} + +TEST_F(DeviceSessionTest, Create_BindToAny) { + // Bind to 0.0.0.0 (INADDR_ANY) - should always work + auto config = ConnectionParams::custom("127.0.0.1", "0.0.0.0", 51003, 51004); + + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()) << "Error: " << result.error(); + + auto& session = result.value(); + EXPECT_TRUE(session->isConnected()); +} + +TEST_F(DeviceSessionTest, MoveConstruction) { + auto config = ConnectionParams::custom("127.0.0.1", "0.0.0.0", 51005, 51006); + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()); + + // Move the unique_ptr + auto session2 = std::move(result.value()); + EXPECT_TRUE(session2->isConnected()); +} + +TEST_F(DeviceSessionTest, Destroy_ViaReset) { + auto config = ConnectionParams::custom("127.0.0.1", "0.0.0.0", 51007, 51008); + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()); + + auto session = std::move(result.value()); + EXPECT_TRUE(session->isConnected()); + + // Destroy via unique_ptr reset (RAII cleanup) + session.reset(); + EXPECT_EQ(session, nullptr); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Packet Send Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(DeviceSessionTest, SendPacket_Single) { + auto config = ConnectionParams::custom("127.0.0.1", "0.0.0.0", 51009, 51010); + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + + // Create test packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.type = 0x01; + pkt.cbpkt_header.dlen = 0; + + // Send packet + auto send_result = session->sendPacket(pkt); + EXPECT_TRUE(send_result.isOk()) << "Error: " << send_result.error(); +} + +TEST_F(DeviceSessionTest, SendPackets_Multiple) { + auto config = ConnectionParams::custom("127.0.0.1", "0.0.0.0", 51011, 51012); + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + + // Create test packets + std::vector pkts(5); + for (int i = 0; i < 5; ++i) { + std::memset(&pkts[i], 0, sizeof(cbPKT_GENERIC)); + pkts[i].cbpkt_header.type = 0x01 + i; + } + + // Send packets (coalesced into minimal datagrams) + auto send_result = session->sendPackets(pkts); + EXPECT_TRUE(send_result.isOk()) << "Error: " << send_result.error(); +} + +TEST_F(DeviceSessionTest, SendPacket_AfterDestroy) { + auto config = ConnectionParams::custom("127.0.0.1", "0.0.0.0", 51013, 51014); + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()); + + auto session = std::move(result.value()); + EXPECT_TRUE(session->isConnected()); + + // Destroy session via RAII + session.reset(); + EXPECT_EQ(session, nullptr); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Packet Receive Tests (Loopback) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// NOTE: Callback and statistics tests removed - those features moved to SdkSession +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Access Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(DeviceSessionTest, GetConnectionParams) { + // Use loopback and 0.0.0.0 for binding (guaranteed to work) + auto config = ConnectionParams::custom("127.0.0.1", "0.0.0.0", 51035, 51036); + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + const auto& retrieved_config = session->getConnectionParams(); + + EXPECT_EQ(retrieved_config.device_address, "127.0.0.1"); + EXPECT_EQ(retrieved_config.client_address, "0.0.0.0"); + EXPECT_EQ(retrieved_config.recv_port, 51035); + EXPECT_EQ(retrieved_config.send_port, 51036); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Utility Function Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(DeviceSessionTest, DetectLocalIP) { + std::string ip = detectLocalIP(); + + // Should return valid IP string + EXPECT_FALSE(ip.empty()); + + // On macOS, should return "0.0.0.0" (recommended for multi-interface systems) +#ifdef __APPLE__ + EXPECT_EQ(ip, "0.0.0.0"); +#endif +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Error Handling Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(DeviceSessionTest, Error_SendPacketsEmpty) { + auto config = ConnectionParams::custom("127.0.0.1", "0.0.0.0", 51029, 51030); + auto result = createDeviceSession(config, ProtocolVersion::PROTOCOL_CURRENT); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + + std::vector empty_pkts; + auto send_result = session->sendPackets(empty_pkts); + EXPECT_TRUE(send_result.isError()); +} diff --git a/tests/unit/test_instrument_id.cpp b/tests/unit/test_instrument_id.cpp new file mode 100644 index 00000000..c17060ef --- /dev/null +++ b/tests/unit/test_instrument_id.cpp @@ -0,0 +1,241 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_instrument_id.cpp +/// @brief Unit tests for cbproto::InstrumentId type +/// +/// Tests the critical 0-based/1-based conversion logic that fixes the indexing bug. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +using cbproto::InstrumentId; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Test fixture for InstrumentId tests +/////////////////////////////////////////////////////////////////////////////////////////////////// +class InstrumentIdTest : public ::testing::Test { +protected: + // Note: cbNSP1, cbMAXOPEN, etc. are defined in cbproto/types.h +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Construction Tests +/// @{ + +TEST_F(InstrumentIdTest, FromOneBased_FirstInstrument) { + InstrumentId id = InstrumentId::fromOneBased(cbNSP1); + + EXPECT_EQ(id.toOneBased(), 1); + EXPECT_EQ(id.toIndex(), 0); + EXPECT_EQ(id.toPacketField(), 0); + EXPECT_TRUE(id.isValid()); +} + +TEST_F(InstrumentIdTest, FromOneBased_SecondInstrument) { + InstrumentId id = InstrumentId::fromOneBased(cbNSP2); + + EXPECT_EQ(id.toOneBased(), 2); + EXPECT_EQ(id.toIndex(), 1); + EXPECT_EQ(id.toPacketField(), 1); + EXPECT_TRUE(id.isValid()); +} + +TEST_F(InstrumentIdTest, FromIndex_ZeroIndex) { + InstrumentId id = InstrumentId::fromIndex(0); + + EXPECT_EQ(id.toIndex(), 0); + EXPECT_EQ(id.toOneBased(), 1); + EXPECT_EQ(id.toPacketField(), 0); + EXPECT_TRUE(id.isValid()); +} + +TEST_F(InstrumentIdTest, FromIndex_MaxIndex) { + InstrumentId id = InstrumentId::fromIndex(cbMAXOPEN - 1); // Index 3 for cbMAXOPEN=4 + + EXPECT_EQ(id.toIndex(), 3); + EXPECT_EQ(id.toOneBased(), 4); + EXPECT_EQ(id.toPacketField(), 3); + EXPECT_TRUE(id.isValid()); +} + +TEST_F(InstrumentIdTest, FromPacketField_ZeroValue) { + // Packet header instrument field is 0-based + InstrumentId id = InstrumentId::fromPacketField(0); + + EXPECT_EQ(id.toPacketField(), 0); + EXPECT_EQ(id.toOneBased(), 1); + EXPECT_EQ(id.toIndex(), 0); + EXPECT_TRUE(id.isValid()); +} + +TEST_F(InstrumentIdTest, FromPacketField_MaxValue) { + // Packet header instrument field is 0-based + InstrumentId id = InstrumentId::fromPacketField(cbMAXOPEN - 1); // 3 for cbMAXOPEN=4 + + EXPECT_EQ(id.toPacketField(), 3); + EXPECT_EQ(id.toOneBased(), 4); + EXPECT_EQ(id.toIndex(), 3); + EXPECT_TRUE(id.isValid()); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Validation Tests +/// @{ + +TEST_F(InstrumentIdTest, Validation_ValidRange) { + for (uint8_t i = 1; i <= cbMAXOPEN; ++i) { + InstrumentId id = InstrumentId::fromOneBased(i); + EXPECT_TRUE(id.isValid()) << "ID " << (int)i << " should be valid"; + } +} + +TEST_F(InstrumentIdTest, Validation_InvalidZero) { + InstrumentId id = InstrumentId::fromOneBased(0); + EXPECT_FALSE(id.isValid()); +} + +TEST_F(InstrumentIdTest, Validation_InvalidTooHigh) { + InstrumentId id = InstrumentId::fromOneBased(cbMAXOPEN + 1); + EXPECT_FALSE(id.isValid()); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Round-Trip Conversion Tests +/// +/// These tests ensure that conversions are symmetric and don't lose information +/// @{ + +TEST_F(InstrumentIdTest, RoundTrip_OneBasedToIndexToOneBased) { + for (uint8_t oneBased = 1; oneBased <= cbMAXOPEN; ++oneBased) { + InstrumentId id1 = InstrumentId::fromOneBased(oneBased); + uint8_t idx = id1.toIndex(); + InstrumentId id2 = InstrumentId::fromIndex(idx); + + EXPECT_EQ(id1.toOneBased(), id2.toOneBased()) + << "Round-trip failed for 1-based ID " << (int)oneBased; + } +} + +TEST_F(InstrumentIdTest, RoundTrip_PacketFieldToOneBasedToPacketField) { + for (uint8_t pktField = 0; pktField < cbMAXOPEN; ++pktField) { + InstrumentId id1 = InstrumentId::fromPacketField(pktField); + uint8_t oneBased = id1.toOneBased(); + InstrumentId id2 = InstrumentId::fromOneBased(oneBased); + + EXPECT_EQ(id1.toPacketField(), id2.toPacketField()) + << "Round-trip failed for packet field " << (int)pktField; + } +} + +TEST_F(InstrumentIdTest, RoundTrip_IndexToPacketFieldToIndex) { + for (uint8_t idx = 0; idx < cbMAXOPEN; ++idx) { + InstrumentId id1 = InstrumentId::fromIndex(idx); + uint8_t pktField = id1.toPacketField(); + InstrumentId id2 = InstrumentId::fromPacketField(pktField); + + EXPECT_EQ(id1.toIndex(), id2.toIndex()) + << "Round-trip failed for index " << (int)idx; + } +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Comparison Operator Tests +/// @{ + +TEST_F(InstrumentIdTest, Equality_SameValue) { + InstrumentId id1 = InstrumentId::fromOneBased(cbNSP1); + InstrumentId id2 = InstrumentId::fromOneBased(cbNSP1); + + EXPECT_EQ(id1, id2); + EXPECT_FALSE(id1 != id2); +} + +TEST_F(InstrumentIdTest, Equality_DifferentValue) { + InstrumentId id1 = InstrumentId::fromOneBased(cbNSP1); + InstrumentId id2 = InstrumentId::fromOneBased(cbNSP2); + + EXPECT_NE(id1, id2); + EXPECT_FALSE(id1 == id2); +} + +TEST_F(InstrumentIdTest, Comparison_LessThan) { + InstrumentId id1 = InstrumentId::fromOneBased(1); + InstrumentId id2 = InstrumentId::fromOneBased(2); + + EXPECT_LT(id1, id2); + EXPECT_LE(id1, id2); + EXPECT_FALSE(id1 > id2); + EXPECT_FALSE(id1 >= id2); +} + +TEST_F(InstrumentIdTest, Comparison_GreaterThan) { + InstrumentId id1 = InstrumentId::fromOneBased(2); + InstrumentId id2 = InstrumentId::fromOneBased(1); + + EXPECT_GT(id1, id2); + EXPECT_GE(id1, id2); + EXPECT_FALSE(id1 < id2); + EXPECT_FALSE(id1 <= id2); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Bug Fix Verification Tests +/// +/// These tests specifically verify that the InstrumentId type fixes the indexing bug +/// described in upstream/scratch_1.md lines 504-551 +/// @{ + +TEST_F(InstrumentIdTest, BugFix_StandaloneModeMapping) { + // In standalone mode, the first instrument should map to index 0 + // This is what InstNetwork.cpp does: stores at [0] + + InstrumentId id = InstrumentId::fromOneBased(cbNSP1); + EXPECT_EQ(id.toIndex(), 0) << "Standalone mode: first instrument should map to index 0"; +} + +TEST_F(InstrumentIdTest, BugFix_ClientModeMapping) { + // In client mode, Central uses the packet instrument field directly as index + // Packet field 0 -> index 0, packet field 1 -> index 1, etc. + + for (uint8_t pktField = 0; pktField < cbMAXOPEN; ++pktField) { + InstrumentId id = InstrumentId::fromPacketField(pktField); + EXPECT_EQ(id.toIndex(), pktField) + << "Client mode: packet field " << (int)pktField + << " should map to index " << (int)pktField; + } +} + +TEST_F(InstrumentIdTest, BugFix_APIToArrayIndexConversion) { + // The bug: cbGetProcInfo(cbNSP1, ...) expects data at procinfo[0] in standalone + // but Central stores at procinfo[pkt.instrument] where pkt.instrument can be 0, 1, 2, 3 + + InstrumentId id = InstrumentId::fromOneBased(cbNSP1); // API uses 1-based + uint8_t arrayIndex = id.toIndex(); // Convert to array index + + EXPECT_EQ(arrayIndex, 0) + << "cbNSP1 (1-based) must convert to array index 0 for standalone mode"; +} + +TEST_F(InstrumentIdTest, BugFix_PacketToArrayIndexConversion) { + // Central receives packet with instrument field = 0 (0-based) + // It should store at index 0 + + cbPKT_HEADER pkt; + pkt.instrument = 0; // Packet field is 0-based + + InstrumentId id = InstrumentId::fromPacketField(pkt.instrument); + uint8_t arrayIndex = id.toIndex(); + + EXPECT_EQ(arrayIndex, 0) + << "Packet instrument=0 should map to array index 0"; +} + +/// @} diff --git a/tests/unit/test_native_types.cpp b/tests/unit/test_native_types.cpp new file mode 100644 index 00000000..8a132542 --- /dev/null +++ b/tests/unit/test_native_types.cpp @@ -0,0 +1,223 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_native_types.cpp +/// @author CereLink Development Team +/// @date 2026-02-08 +/// +/// @brief Unit tests for native-mode shared memory type definitions +/// +/// Validates struct sizes, constants, and layout assumptions for native-mode buffers. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include + +using namespace cbshm; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Constants Tests +/// @{ + +TEST(NativeTypesTest, ChannelCountConstants) { + EXPECT_EQ(NATIVE_NUM_FE_CHANS, 256u); + EXPECT_EQ(NATIVE_NUM_ANALOG_CHANS, 272u); + EXPECT_EQ(NATIVE_MAXCHANS, 284u); + EXPECT_EQ(NATIVE_MAXGROUPS, 8u); + EXPECT_EQ(NATIVE_MAXFILTS, 32u); +} + +TEST(NativeTypesTest, TransmitSlotSize) { + // Native slots are sized for cbPKT_MAX_SIZE (1024 bytes) + EXPECT_EQ(NATIVE_XMT_SLOT_WORDS, 256u); // 1024 / 4 + EXPECT_EQ(NATIVE_XMT_SLOT_WORDS * sizeof(uint32_t), cbPKT_MAX_SIZE); +} + +TEST(NativeTypesTest, SpikeCacheConstants) { + EXPECT_EQ(NATIVE_cbPKT_SPKCACHEPKTCNT, CENTRAL_cbPKT_SPKCACHEPKTCNT); + EXPECT_EQ(NATIVE_cbPKT_SPKCACHELINECNT, NATIVE_NUM_ANALOG_CHANS); + EXPECT_EQ(NATIVE_cbPKT_SPKCACHELINECNT, 272u); +} + +TEST(NativeTypesTest, ReceiveBufferLengthMatchesCbproto) { + EXPECT_EQ(NATIVE_cbRECBUFFLEN, cbRECBUFFLEN); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Size Comparison Tests +/// @{ + +TEST(NativeTypesTest, NativeConfigBufferSmallerThanCentral) { + // Native config buffer should be significantly smaller than Central's + size_t native_size = sizeof(NativeConfigBuffer); + size_t central_size = sizeof(CentralConfigBuffer); + + EXPECT_LT(native_size, central_size) + << "Native config buffer (" << native_size << " bytes) should be smaller than " + << "Central config buffer (" << central_size << " bytes)"; + + // Native should be roughly 1 MB range (order of magnitude check) + EXPECT_GT(native_size, 100 * 1024u) << "Native config buffer seems too small"; + EXPECT_LT(native_size, 10 * 1024 * 1024u) << "Native config buffer seems too large"; +} + +TEST(NativeTypesTest, NativeSpikeBufferSmallerThanCentral) { + size_t native_size = sizeof(NativeSpikeBuffer); + size_t central_size = sizeof(CentralSpikeBuffer); + + EXPECT_LT(native_size, central_size) + << "Native spike buffer (" << native_size << " bytes) should be smaller than " + << "Central spike buffer (" << central_size << " bytes)"; +} + +TEST(NativeTypesTest, NativeTransmitBufferSmallerThanCentral) { + size_t native_size = sizeof(NativeTransmitBuffer); + size_t central_size = sizeof(CentralTransmitBuffer); + + EXPECT_LT(native_size, central_size) + << "Native transmit buffer (" << native_size << " bytes) should be smaller than " + << "Central transmit buffer (" << central_size << " bytes)"; +} + +TEST(NativeTypesTest, NativeLocalTransmitBufferSmallerThanCentral) { + size_t native_size = sizeof(NativeTransmitBufferLocal); + size_t central_size = sizeof(CentralTransmitBufferLocal); + + EXPECT_LT(native_size, central_size) + << "Native local transmit buffer (" << native_size << " bytes) should be smaller than " + << "Central local transmit buffer (" << central_size << " bytes)"; +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Struct Layout Tests +/// @{ + +TEST(NativeTypesTest, ConfigBufferHasExpectedFields) { + // Heap-allocate: NativeConfigBuffer is multi-MB (too large for stack) + auto cfg = std::make_unique(); + std::memset(cfg.get(), 0, sizeof(NativeConfigBuffer)); + + // Verify key fields are accessible and zero-initialized + EXPECT_EQ(cfg->version, 0u); + EXPECT_EQ(cfg->sysflags, 0u); + EXPECT_EQ(cfg->instrument_status, 0u); + EXPECT_EQ(cfg->procinfo.proc, 0u); + EXPECT_EQ(cfg->adaptinfo.chan, 0u); + EXPECT_EQ(cfg->refelecinfo.chan, 0u); +} + +TEST(NativeTypesTest, TransmitBufferLayout) { + // Heap-allocate: NativeTransmitBuffer is ~5 MB (too large for stack) + auto xmt = std::make_unique(); + std::memset(xmt.get(), 0, sizeof(NativeTransmitBuffer)); + + EXPECT_EQ(xmt->transmitted, 0u); + EXPECT_EQ(xmt->headindex, 0u); + EXPECT_EQ(xmt->tailindex, 0u); + EXPECT_EQ(xmt->last_valid_index, 0u); + EXPECT_EQ(xmt->bufferlen, 0u); + + // Buffer should be large enough for the configured number of slots + EXPECT_EQ(sizeof(xmt->buffer) / sizeof(uint32_t), NATIVE_cbXMT_GLOBAL_BUFFLEN); +} + +TEST(NativeTypesTest, LocalTransmitBufferLayout) { + // Heap-allocate: NativeTransmitBufferLocal is ~2MB (too large for stack on Windows) + auto xmt = std::make_unique(); + std::memset(xmt.get(), 0, sizeof(NativeTransmitBufferLocal)); + + EXPECT_EQ(xmt->transmitted, 0u); + EXPECT_EQ(xmt->headindex, 0u); + EXPECT_EQ(xmt->tailindex, 0u); + EXPECT_EQ(sizeof(xmt->buffer) / sizeof(uint32_t), NATIVE_cbXMT_LOCAL_BUFFLEN); +} + +TEST(NativeTypesTest, SpikeCacheLayout) { + NativeSpikeCache cache = {}; + + EXPECT_EQ(cache.chid, 0u); + EXPECT_EQ(cache.pktcnt, 0u); + EXPECT_EQ(cache.pktsize, 0u); + EXPECT_EQ(cache.head, 0u); + EXPECT_EQ(cache.valid, 0u); +} + +TEST(NativeTypesTest, SpikeBufferLayout) { + // NativeSpikeBuffer is too large for stack (~109 MB), verify layout via sizeof/offsetof + static_assert(sizeof(NativeSpikeBuffer) > sizeof(NativeSpikeCache) * NATIVE_cbPKT_SPKCACHELINECNT, + "Spike buffer should contain NATIVE_cbPKT_SPKCACHELINECNT cache lines plus header"); + + // Verify cache line count + EXPECT_EQ(sizeof(NativeSpikeBuffer::cache) / sizeof(NativeSpikeCache), NATIVE_cbPKT_SPKCACHELINECNT); +} + +TEST(NativeTypesTest, PCStatusLayout) { + NativePCStatus status = {}; + + EXPECT_EQ(status.m_iBlockRecording, 0); + EXPECT_EQ(status.m_nPCStatusFlags, 0u); + EXPECT_EQ(status.m_nNumFEChans, 0u); + EXPECT_EQ(status.m_nNumTotalChans, 0u); + EXPECT_EQ(status.m_nGeminiSystem, 0u); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name CentralLegacyCFGBUFF Tests +/// @{ + +TEST(CentralLegacyTypesTest, SizeCloseToConfigBuffer) { + // CentralLegacyCFGBUFF and CentralConfigBuffer should be close in size. + // Differences: + // CentralConfigBuffer has instrument_status[4] (+16 bytes), no optiontable/colortable move (neutral) + // CentralLegacyCFGBUFF omits hwndCentral (saves ~8 bytes) and instrument_status (saves 16 bytes) + // isLnc position differs but size is the same + // So the difference should be small (under 1KB) + size_t legacy_size = sizeof(CentralLegacyCFGBUFF); + size_t config_size = sizeof(CentralConfigBuffer); + + // Both should be in the multi-MB range + EXPECT_GT(legacy_size, 1 * 1024 * 1024u) << "Legacy config buffer seems too small: " << legacy_size; + EXPECT_GT(config_size, 1 * 1024 * 1024u) << "Config buffer seems too small: " << config_size; + + // The difference should be small (instrument_status[4] = 16 bytes) + size_t diff = (legacy_size > config_size) ? (legacy_size - config_size) : (config_size - legacy_size); + EXPECT_LT(diff, 1024u) + << "Legacy (" << legacy_size << ") and CereLink (" << config_size + << ") config buffers differ by " << diff << " bytes (expected < 1KB)"; +} + +TEST(CentralLegacyTypesTest, FieldOrderMatchesCentral) { + // Verify that optiontable comes right after sysflags (Central's layout) + // In CereLink's cbConfigBuffer, optiontable is at the END + // Heap-allocate: CentralLegacyCFGBUFF is multi-MB (too large for stack) + auto legacy = std::make_unique(); + std::memset(legacy.get(), 0, sizeof(CentralLegacyCFGBUFF)); + + // Access fields to verify they compile and are accessible + legacy->version = 42; + legacy->sysflags = 1; + legacy->optiontable = {}; + legacy->colortable = {}; + legacy->sysinfo = {}; + legacy->procinfo[0] = {}; + legacy->procinfo[3] = {}; + legacy->bankinfo[0][0] = {}; + legacy->chaninfo[0] = {}; + legacy->chaninfo[CENTRAL_cbMAXCHANS - 1] = {}; + legacy->isLnc[0] = {}; + legacy->isLnc[3] = {}; + legacy->fileinfo = {}; + + EXPECT_EQ(legacy->version, 42u); +} + +/// @} diff --git a/tests/unit/test_packet_translation.cpp b/tests/unit/test_packet_translation.cpp new file mode 100644 index 00000000..e6dee79f --- /dev/null +++ b/tests/unit/test_packet_translation.cpp @@ -0,0 +1,337 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_packet_translation.cpp +/// @author CereLink Development Team +/// @date 2025-01-19 +/// +/// @brief Unit tests for packet translation functions +/// +/// Tests bidirectional translation between protocol versions for specific packet types: +/// - SYSPROTOCOLMONITOR (pre-410 ↔ current) +/// - NPLAY (pre-400 ↔ current) +/// - COMMENT (pre-400 ↔ current) +/// - CHANINFO (pre-410 ↔ current) +/// - CHANRESET (pre-420 ↔ current) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include "packet_test_helpers.h" +#include +#include + +using namespace test_helpers; +using namespace cbproto; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// SYSPROTOCOLMONITOR Translation Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(SYSPROTOCOLMONITOR_Translation, Pre410_to_Current_AddsCounterField) { + // Given: 3.11 SYSPROTOCOLMONITOR packet with sentpkts=100, no counter field + auto pkt_311 = make_311_SYSPROTOCOLMONITOR(100, 90000); // time=90000 ticks (3 seconds) + + // Prepare destination in current format + cbPKT_SYSPROTOCOLMONITOR dest = {}; + dest.cbpkt_header.time = ticks_to_ns(90000); // Pre-fill with translated time + dest.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + dest.cbpkt_header.type = cbPKTTYPE_SYSPROTOCOLMONITOR; + dest.cbpkt_header.dlen = 1; // Will be increased to 2 + + // When: Translate payload + const uint8_t* src_payload = &pkt_311[test_helpers::HEADER_SIZE_311]; + size_t result_dlen = PacketTranslator::translate_SYSPROTOCOLMONITOR_pre410_to_current( + src_payload, &dest); + + // Then: sentpkts preserved, counter filled from header time, dlen increased + EXPECT_EQ(dest.sentpkts, 100u); + EXPECT_EQ(dest.counter, ticks_to_ns(90000)); // Counter comes from header.time in 3.11 + EXPECT_EQ(result_dlen, 2u); // dlen increased by 1 + EXPECT_EQ(dest.cbpkt_header.dlen, 2u); +} + +TEST(SYSPROTOCOLMONITOR_Translation, Current_to_Pre410_DropsCounterField) { + // Given: Current SYSPROTOCOLMONITOR with sentpkts=200, counter=5000 + auto pkt_current = make_current_SYSPROTOCOLMONITOR(200, 5000); + + // Prepare destination buffer + uint8_t dest_payload[256] = {}; + + // When: Translate to pre-410 format + size_t result_dlen = PacketTranslator::translate_SYSPROTOCOLMONITOR_current_to_pre410( + pkt_current, dest_payload); + + // Then: sentpkts preserved, counter dropped, dlen decreased + uint32_t dest_sentpkts = *reinterpret_cast(dest_payload); + EXPECT_EQ(dest_sentpkts, 200u); + EXPECT_EQ(result_dlen, 1u); // dlen decreased by 1 +} + +TEST(SYSPROTOCOLMONITOR_Translation, RoundTrip_Pre410_to_Current_to_Pre410) { + // Test that round-trip translation is lossy (counter is lost) + auto pkt_311 = make_311_SYSPROTOCOLMONITOR(150, 60000); + + // 311 -> Current + cbPKT_SYSPROTOCOLMONITOR intermediate = {}; + intermediate.cbpkt_header.time = ticks_to_ns(60000); + intermediate.cbpkt_header.dlen = 1; + PacketTranslator::translate_SYSPROTOCOLMONITOR_pre410_to_current( + &pkt_311[test_helpers::HEADER_SIZE_311], &intermediate); + + // Current -> 311 + uint8_t result_311[256] = {}; + PacketTranslator::translate_SYSPROTOCOLMONITOR_current_to_pre410( + intermediate, result_311); + + // Verify sentpkts preserved + EXPECT_EQ(*reinterpret_cast(result_311), 150u); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// NPLAY Translation Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(NPLAY_Translation, Pre400_to_Current_TimeConversion) { + // Given: 3.11 NPLAY with times in 30kHz ticks + // 30000 ticks = 1 second, 60000 = 2 seconds, etc. + auto pkt_311 = make_311_NPLAY(30000, 60000, 90000, 120000, cbNPLAY_MODE_PAUSE); + + // Prepare destination - must initialize dlen to SOURCE packet's dlen + cbPKT_NPLAY dest = {}; + dest.cbpkt_header.dlen = pkt_311[7]; // Copy source dlen from 3.11 header (byte 7) + + // When: Translate + const uint8_t* src_payload = &pkt_311[test_helpers::HEADER_SIZE_311]; + size_t result_dlen = PacketTranslator::translate_NPLAY_pre400_to_current( + src_payload, &dest); + + // Then: Times converted from 30kHz ticks to nanoseconds + EXPECT_EQ(dest.ftime, ticks_to_ns(30000)); // 1 second in ns + EXPECT_EQ(dest.stime, ticks_to_ns(60000)); // 2 seconds in ns + EXPECT_EQ(dest.etime, ticks_to_ns(90000)); // 3 seconds in ns + EXPECT_EQ(dest.val, ticks_to_ns(120000)); // 4 seconds in ns + EXPECT_EQ(dest.mode, cbNPLAY_MODE_PAUSE); + + // dlen increases by 16 bytes (4 fields × (8 bytes - 4 bytes)) = 4 quadlets + // Source: 248 quadlets (cbPKTDLEN_NPLAY - 4), Result: 252 quadlets (cbPKTDLEN_NPLAY) + EXPECT_EQ(result_dlen, cbPKTDLEN_NPLAY); + EXPECT_EQ(dest.cbpkt_header.dlen, cbPKTDLEN_NPLAY); +} + +TEST(NPLAY_Translation, Current_to_Pre400_TimeNarrowing) { + // Given: Current NPLAY with times in nanoseconds + auto pkt_current = make_current_NPLAY( + ticks_to_ns(30000), // 1 second + ticks_to_ns(60000), // 2 seconds + ticks_to_ns(90000), // 3 seconds + ticks_to_ns(120000), // 4 seconds + cbNPLAY_MODE_PAUSE + ); + + // Verify packet was created correctly + ASSERT_EQ(pkt_current.cbpkt_header.dlen, cbPKTDLEN_NPLAY) << "Packet dlen not set correctly"; + + // When: Translate to pre-400 + uint8_t dest_payload[1024] = {}; // Increased size to match cbPKT_NPLAY + size_t result_dlen = PacketTranslator::translate_NPLAY_current_to_pre400( + pkt_current, dest_payload); + + // Then: Times converted back to 30kHz ticks + uint32_t dest_ftime = *reinterpret_cast(&dest_payload[0]); + uint32_t dest_stime = *reinterpret_cast(&dest_payload[4]); + uint32_t dest_etime = *reinterpret_cast(&dest_payload[8]); + uint32_t dest_val = *reinterpret_cast(&dest_payload[12]); + uint32_t dest_mode = *reinterpret_cast(&dest_payload[16]); + + EXPECT_EQ(dest_ftime, 30000u); + EXPECT_EQ(dest_stime, 60000u); + EXPECT_EQ(dest_etime, 90000u); + EXPECT_EQ(dest_val, 120000u); + EXPECT_EQ(dest_mode, cbNPLAY_MODE_PAUSE); + + // dlen decreases by 4 quadlets + EXPECT_EQ(result_dlen, cbPKTDLEN_NPLAY - 4u); +} + +TEST(NPLAY_Translation, RoundTrip_Exact_OnTickBoundaries) { + // Times that are exact multiples of tick period should round-trip perfectly + PROCTIME original_time = ticks_to_ns(45000); // Exactly 1.5 seconds + + auto pkt = make_current_NPLAY(original_time, 0, 0, 0, 0); + + // Current -> Pre400 + uint8_t buffer_311[sizeof(cbPKT_NPLAY)] = {}; + PacketTranslator::translate_NPLAY_current_to_pre400(pkt, buffer_311); + + // Pre400 -> Current + cbPKT_NPLAY result = pkt; + result.cbpkt_header.dlen = cbPKTDLEN_NPLAY - 4; // Simulate pre-400 dlen + PacketTranslator::translate_NPLAY_pre400_to_current(buffer_311, &result); + + // Should be exact (no precision loss on tick boundaries) + EXPECT_EQ(result.ftime, original_time); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// COMMENT Translation Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(COMMENT_Translation, Pre400_to_Current_FlagsZero_RGBA) { + // Given: 3.11 COMMENT with flags=0 (RGBA mode), data contains color + const char* comment_text = "Test comment"; + auto pkt_311 = make_311_COMMENT(0x00, 0xAABBCCDD, comment_text, 90000); + + // Prepare destination + cbPKT_COMMENT dest = {}; + dest.cbpkt_header.dlen = pkt_311[7]; // Copy dlen from source + + // When: Translate (header timestamp needed for timeStarted fallback) + const uint8_t* src_payload = &pkt_311[test_helpers::HEADER_SIZE_311]; + uint32_t hdr_timestamp = *reinterpret_cast(&pkt_311[0]); + size_t result_dlen = PacketTranslator::translate_COMMENT_pre400_to_current( + src_payload, &dest, hdr_timestamp); + + // Then: RGBA preserved, timeStarted from header, charset=0, comment preserved + EXPECT_EQ(dest.rgba, 0xAABBCCDDu); + EXPECT_EQ(dest.timeStarted, ticks_to_ns(90000)); // From header + EXPECT_EQ(dest.info.charset, 0u); + EXPECT_STREQ(reinterpret_cast(dest.comment), comment_text); + + // dlen increases by 2 quadlets (8 bytes for timeStarted field) + EXPECT_EQ(result_dlen, pkt_311[7] + 2u); +} + +TEST(COMMENT_Translation, Pre400_to_Current_FlagsOne_TimeStarted) { + // Given: 3.11 COMMENT with flags=1 (timeStarted mode), data contains time + const char* comment_text = "Another test"; + auto pkt_311 = make_311_COMMENT(0x01, 60000, comment_text, 90000); + + // Prepare destination + cbPKT_COMMENT dest = {}; + dest.cbpkt_header.dlen = pkt_311[7]; + + // When: Translate + const uint8_t* src_payload = &pkt_311[test_helpers::HEADER_SIZE_311]; + uint32_t hdr_timestamp = *reinterpret_cast(&pkt_311[0]); + size_t result_dlen = PacketTranslator::translate_COMMENT_pre400_to_current( + src_payload, &dest, hdr_timestamp); + + // Then: timeStarted from data field, rgba=0 + EXPECT_EQ(dest.timeStarted, ticks_to_ns(60000)); // From data field + EXPECT_EQ(dest.rgba, 0u); // Undefined in this mode + EXPECT_EQ(dest.info.charset, 0u); + EXPECT_STREQ(reinterpret_cast(dest.comment), comment_text); +} + +TEST(COMMENT_Translation, Current_to_Pre400_AlwaysUsesTimeStarted) { + // Given: Current COMMENT + const char* comment_text = "Modern comment"; + auto pkt_current = make_current_COMMENT(0, ticks_to_ns(75000), 0x11223344, comment_text); + + // When: Translate to pre-400 + uint8_t dest_payload[256] = {}; + size_t result_dlen = PacketTranslator::translate_COMMENT_current_to_pre400( + pkt_current, dest_payload); + + // Then: flags=0x01, data=timeStarted in ticks + EXPECT_EQ(dest_payload[1], 0x01u); // flags + uint32_t dest_data = *reinterpret_cast(&dest_payload[4]); + EXPECT_EQ(dest_data, 75000u); // timeStarted converted back to ticks + + // Comment text preserved + const char* dest_comment = reinterpret_cast(&dest_payload[8]); + EXPECT_STREQ(dest_comment, comment_text); + + // dlen decreases by 2 quadlets + EXPECT_EQ(result_dlen, cbPKTDLEN_COMMENT - 2u); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// CHANINFO Translation Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(CHANINFO_Translation, Pre410_to_Current_FieldExpansion) { + // Given: 3.11 CHANINFO with monsource as uint32_t + auto pkt_311 = make_311_CHANINFO(42, 0x12345678); + + // Prepare destination + cbPKT_CHANINFO dest = {}; + dest.cbpkt_header.dlen = pkt_311[7]; + + // When: Translate + const uint8_t* src_payload = &pkt_311[test_helpers::HEADER_SIZE_311]; + size_t result_dlen = PacketTranslator::translate_CHANINFO_pre410_to_current( + src_payload, &dest); + + // Then: monsource narrowed to moninst, monchan=0, new fields zeroed + EXPECT_EQ(dest.chan, 42u); + EXPECT_EQ(dest.moninst, 0x5678u); // Lower 16 bits of monsource + EXPECT_EQ(dest.monchan, 0u); // New field + EXPECT_EQ(dest.reserved[0], 0u); // New field + EXPECT_EQ(dest.reserved[1], 0u); // New field + EXPECT_EQ(dest.triginst, 0u); // New field + + // dlen increases by 1 quadlet (3 bytes added, rounded up) + EXPECT_EQ(result_dlen, pkt_311[7] + 1u); +} + +TEST(CHANINFO_Translation, Current_to_Pre410_FieldNarrowing) { + // Given: Current CHANINFO with moninst, monchan, triginst + auto pkt_current = make_current_CHANINFO(42, 0x1234, 0x5678); + + // When: Translate to pre-410 + uint8_t dest_payload[sizeof(cbPKT_CHANINFO)] = {}; + size_t result_dlen = PacketTranslator::translate_CHANINFO_current_to_pre410( + pkt_current, dest_payload); + + // Then: chan preserved, moninst expanded to monsource, monchan/triginst dropped + uint32_t dest_chan = *reinterpret_cast(&dest_payload[0]); + EXPECT_EQ(dest_chan, 42u); + + // Find monsource field (at specific offset in structure) + // For simplicity, we'll verify the dlen change + EXPECT_EQ(result_dlen, cbPKTDLEN_CHANINFO - 1u); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// CHANRESET Translation Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST(CHANRESET_Translation, Pre420_to_Current_InsertsMonChan) { + // Given: Pre-4.2 CHANRESET with monsource (single byte) + auto pkt_410 = make_410_CHANRESET(42, 5); + + // Prepare destination + cbPKT_CHANRESET dest = {}; + dest.cbpkt_header.dlen = 7; // Pre-4.2 dlen + + // When: Translate + const auto* src_pkt = reinterpret_cast(&pkt_410); + const uint8_t* src_payload = reinterpret_cast(&pkt_410) + cbPKT_HEADER_SIZE; + size_t result_dlen = PacketTranslator::translate_CHANRESET_pre420_to_current( + src_payload, &dest); + + // Then: chan preserved, moninst from monsource, monchan inserted + EXPECT_EQ(dest.chan, 42u); + EXPECT_EQ(dest.moninst, 5u); // From monsource + // Note: monchan field is inserted but value depends on memory layout + + // dlen stays at 7 (29 bytes → 30 bytes, both round to 7 quadlets) + EXPECT_EQ(result_dlen, 7u); +} + +TEST(CHANRESET_Translation, Current_to_Pre420_RemovesMonChan) { + // Given: Current CHANRESET with moninst and monchan + auto pkt_current = make_current_CHANRESET(42, 5, 10); + + // When: Translate to pre-420 + uint8_t dest_payload[256] = {}; + size_t result_dlen = PacketTranslator::translate_CHANRESET_current_to_pre420( + pkt_current, dest_payload); + + // Then: chan preserved, moninst preserved, monchan dropped + uint32_t dest_chan = *reinterpret_cast(&dest_payload[0]); + EXPECT_EQ(dest_chan, 42u); + + // dlen stays at 7 + EXPECT_EQ(result_dlen, 7u); +} diff --git a/tests/unit/test_protocol_structures.cpp b/tests/unit/test_protocol_structures.cpp new file mode 100644 index 00000000..c8fdb08e --- /dev/null +++ b/tests/unit/test_protocol_structures.cpp @@ -0,0 +1,216 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_protocol_structures.cpp +/// @brief Verify protocol structures match upstream ground truth +/// +/// These tests ensure that our protocol definitions are byte-for-byte compatible +/// with the upstream protocol (cbproto.h). +/// +/// CRITICAL: These structures must match exactly for compatibility with Central! +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Test fixture for protocol structure tests +/////////////////////////////////////////////////////////////////////////////////////////////////// +class ProtocolStructureTest : public ::testing::Test { +protected: + // Expected structure sizes from upstream/cbproto/cbproto.h +#ifdef CBPROTO_311 + static constexpr size_t EXPECTED_PROCTIME_SIZE = 4; // uint32_t + static constexpr size_t EXPECTED_HEADER_SIZE = 8; +#else + static constexpr size_t EXPECTED_PROCTIME_SIZE = 8; // uint64_t + static constexpr size_t EXPECTED_HEADER_SIZE = 16; +#endif +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Type Size Tests +/// @{ + +TEST_F(ProtocolStructureTest, PROCTIME_Size) { + EXPECT_EQ(sizeof(PROCTIME), EXPECTED_PROCTIME_SIZE) + << "PROCTIME size must match upstream protocol"; +} + +TEST_F(ProtocolStructureTest, cbPKT_HEADER_Size) { + EXPECT_EQ(sizeof(cbPKT_HEADER), EXPECTED_HEADER_SIZE) + << "cbPKT_HEADER size must match upstream protocol"; + + // Verify size constant matches + EXPECT_EQ(cbPKT_HEADER_SIZE, sizeof(cbPKT_HEADER)) + << "cbPKT_HEADER_SIZE constant must match sizeof(cbPKT_HEADER)"; +} + +TEST_F(ProtocolStructureTest, cbPKT_HEADER_32Size) { + EXPECT_EQ(cbPKT_HEADER_32SIZE, sizeof(cbPKT_HEADER) / 4) + << "cbPKT_HEADER_32SIZE must be header size divided by 4"; +} + +TEST_F(ProtocolStructureTest, cbPKT_GENERIC_Size) { + EXPECT_LE(sizeof(cbPKT_GENERIC), cbPKT_MAX_SIZE) + << "cbPKT_GENERIC size must not exceed cbPKT_MAX_SIZE"; +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Structure Layout Tests +/// +/// Verify that structure members are at the correct offsets (critical for binary compatibility) +/// @{ + +TEST_F(ProtocolStructureTest, cbPKT_HEADER_Layout) { + cbPKT_HEADER pkt; + + // Verify field offsets match upstream + EXPECT_EQ(offsetof(cbPKT_HEADER, time), 0) + << "time field must be at offset 0"; + + EXPECT_EQ(offsetof(cbPKT_HEADER, chid), EXPECTED_PROCTIME_SIZE) + << "chid field must follow time"; + + EXPECT_EQ(offsetof(cbPKT_HEADER, type), EXPECTED_PROCTIME_SIZE + 2) + << "type field must follow chid"; + + EXPECT_EQ(offsetof(cbPKT_HEADER, dlen), EXPECTED_PROCTIME_SIZE + 4) + << "dlen field must follow type"; + + EXPECT_EQ(offsetof(cbPKT_HEADER, instrument), EXPECTED_PROCTIME_SIZE + 6) + << "instrument field must follow dlen"; + + EXPECT_EQ(offsetof(cbPKT_HEADER, reserved), EXPECTED_PROCTIME_SIZE + 7) + << "reserved field must follow instrument"; +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Field Type Tests +/// @{ + +TEST_F(ProtocolStructureTest, cbPKT_HEADER_FieldTypes) { + cbPKT_HEADER pkt; + + // Verify field sizes + static_assert(sizeof(pkt.time) == sizeof(PROCTIME), "time field type mismatch"); + static_assert(sizeof(pkt.chid) == 2, "chid must be uint16_t"); + static_assert(sizeof(pkt.type) == 2, "type must be uint16_t"); + static_assert(sizeof(pkt.dlen) == 2, "dlen must be uint16_t"); + static_assert(sizeof(pkt.instrument) == 1, "instrument must be uint8_t"); + static_assert(sizeof(pkt.reserved) == 1, "reserved must be uint8_t"); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Constant Value Tests +/// +/// Verify protocol constants match upstream +/// @{ + +TEST_F(ProtocolStructureTest, VersionConstants) { + EXPECT_EQ(cbVERSION_MAJOR, 4) << "Protocol major version must be 4"; + EXPECT_EQ(cbVERSION_MINOR, 2) << "Protocol minor version must be 2"; +} + +TEST_F(ProtocolStructureTest, MaxEntityConstants) { + EXPECT_EQ(cbNSP1, 1) << "First NSP ID must be 1 (1-based)"; + EXPECT_EQ(cbMAXOPEN, 4) << "Maximum open instruments must be 4"; + EXPECT_EQ(cbMAXPROCS, 1) << "Processors per NSP must be 1"; + EXPECT_EQ(cbNUM_FE_CHANS, 256) << "Front-end channels must be 256"; +} + +TEST_F(ProtocolStructureTest, NetworkConstants) { + EXPECT_STREQ(cbNET_UDP_ADDR_INST, "192.168.137.1") + << "Instrument default address must match upstream"; + EXPECT_STREQ(cbNET_UDP_ADDR_CNT, "192.168.137.128") + << "Control address must match upstream"; + EXPECT_STREQ(cbNET_UDP_ADDR_BCAST, "192.168.137.255") + << "Broadcast address must match upstream"; + EXPECT_EQ(cbNET_UDP_PORT_BCAST, 51002) << "Broadcast port must be 51002"; + EXPECT_EQ(cbNET_UDP_PORT_CNT, 51001) << "Control port must be 51001"; +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Packet Header Initialization Tests +/// @{ + +TEST_F(ProtocolStructureTest, cbPKT_HEADER_Initialization) { + cbPKT_HEADER pkt = {}; + + // Verify zero-initialization works + EXPECT_EQ(pkt.time, 0); + EXPECT_EQ(pkt.chid, 0); + EXPECT_EQ(pkt.type, 0); + EXPECT_EQ(pkt.dlen, 0); + EXPECT_EQ(pkt.instrument, 0); + EXPECT_EQ(pkt.reserved, 0); +} + +TEST_F(ProtocolStructureTest, cbPKT_HEADER_InstrumentField) { + cbPKT_HEADER pkt = {}; + + // Critical test: instrument field is 0-based in packet + pkt.instrument = 0; // First instrument + EXPECT_EQ(pkt.instrument, 0) << "Packet instrument field is 0-based"; + + pkt.instrument = 3; // Fourth instrument (max for cbMAXOPEN=4) + EXPECT_EQ(pkt.instrument, 3) << "Max instrument value should be cbMAXOPEN-1"; +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Channel Count Calculation Tests +/// @{ + +TEST_F(ProtocolStructureTest, ChannelCountCalculations) { + // Verify channel count calculations + EXPECT_EQ(cbNUM_ANAIN_CHANS, 16 * cbMAXPROCS); + EXPECT_EQ(cbNUM_ANALOG_CHANS, cbNUM_FE_CHANS + cbNUM_ANAIN_CHANS); + EXPECT_EQ(cbNUM_ANAOUT_CHANS, 4 * cbMAXPROCS); + EXPECT_EQ(cbNUM_AUDOUT_CHANS, 2 * cbMAXPROCS); + EXPECT_EQ(cbNUM_ANALOGOUT_CHANS, cbNUM_ANAOUT_CHANS + cbNUM_AUDOUT_CHANS); + EXPECT_EQ(cbNUM_DIGIN_CHANS, 1 * cbMAXPROCS); + EXPECT_EQ(cbNUM_SERIAL_CHANS, 1 * cbMAXPROCS); + EXPECT_EQ(cbNUM_DIGOUT_CHANS, 4 * cbMAXPROCS); + + // Total channel count + size_t expected_total = cbNUM_ANALOG_CHANS + cbNUM_ANALOGOUT_CHANS + + cbNUM_DIGIN_CHANS + cbNUM_SERIAL_CHANS + cbNUM_DIGOUT_CHANS; + EXPECT_EQ(cbMAXCHANS, expected_total); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Packing Tests +/// +/// Verify that structures are tightly packed (critical for network protocol) +/// @{ + +TEST_F(ProtocolStructureTest, cbPKT_HEADER_Packing) { + // Calculate expected size manually + size_t expected_size = sizeof(PROCTIME) + // time + sizeof(uint16_t) + // chid + sizeof(uint16_t) + // type + sizeof(uint16_t) + // dlen + sizeof(uint8_t) + // instrument + sizeof(uint8_t); // reserved + + EXPECT_EQ(sizeof(cbPKT_HEADER), expected_size) + << "cbPKT_HEADER must be tightly packed (pragma pack(1))"; +} + +TEST_F(ProtocolStructureTest, cbPKT_HEADER_Multiple32Bits) { + // Header must be a multiple of uint32_t for alignment + EXPECT_EQ(sizeof(cbPKT_HEADER) % 4, 0) + << "cbPKT_HEADER size must be a multiple of 4 bytes"; +} + +/// @} diff --git a/tests/unit/test_sdk_handshake.cpp b/tests/unit/test_sdk_handshake.cpp new file mode 100644 index 00000000..d3eced49 --- /dev/null +++ b/tests/unit/test_sdk_handshake.cpp @@ -0,0 +1,246 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_sdk_handshake.cpp +/// @author CereLink Development Team +/// @date 2025-11-13 +/// +/// @brief Unit tests for SDK startup handshake and device communication +/// +/// Tests the complete startup sequence including runlevel commands, configuration requests, +/// and SYSREP response handling. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +// Include protocol types and session headers +#include +#include "cbshm/shmem_session.h" +#include "cbdev/device_session.h" +#include "cbsdk/sdk_session.h" + +using namespace cbsdk; + +/// Test fixture for SDK handshake tests +class SdkHandshakeTest : public ::testing::Test { +protected: + void SetUp() override { + test_name = "handshake_" + std::to_string(test_counter++); + } + + void TearDown() override { + // Cleanup happens automatically via RAII + } + + std::string test_name; + static int test_counter; +}; + +int SdkHandshakeTest::test_counter = 0; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkHandshakeTest, Config_AutoRunOption) { + SdkConfig config; + EXPECT_TRUE(config.autorun); // Default: auto-run enabled + + config.autorun = false; + EXPECT_FALSE(config.autorun); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Packet Structure Tests (without actual transmission) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkHandshakeTest, PacketHeader_RunlevelCommand) { + // Test packet structure for runlevel commands + cbPKT_SYSINFO sysinfo; + std::memset(&sysinfo, 0, sizeof(sysinfo)); + + // Fill header as setSystemRunLevel() does + sysinfo.cbpkt_header.time = 1; + sysinfo.cbpkt_header.chid = 0x8000; // cbPKTCHAN_CONFIGURATION + sysinfo.cbpkt_header.type = 0x92; // cbPKTTYPE_SYSSETRUNLEV + sysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; // Use macro for correct header size + sysinfo.cbpkt_header.instrument = 0; + + // Fill payload + sysinfo.runlevel = cbRUNLEVEL_RUNNING; + sysinfo.resetque = 0; + sysinfo.runflags = 0; + + // Verify header fields + EXPECT_EQ(sysinfo.cbpkt_header.chid, 0x8000); + EXPECT_EQ(sysinfo.cbpkt_header.type, 0x92); + EXPECT_GT(sysinfo.cbpkt_header.dlen, 0u); + + // Verify payload + EXPECT_EQ(sysinfo.runlevel, cbRUNLEVEL_RUNNING); +} + +TEST_F(SdkHandshakeTest, PacketHeader_ConfigurationRequest) { + // Test packet structure for configuration request + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + + // Fill header as requestConfiguration() does + pkt.cbpkt_header.time = 1; + pkt.cbpkt_header.chid = 0x8000; // cbPKTCHAN_CONFIGURATION + pkt.cbpkt_header.type = 0x88; // cbPKTTYPE_REQCONFIGALL + pkt.cbpkt_header.dlen = 0; // No payload + pkt.cbpkt_header.instrument = 0; + + // Verify header fields + EXPECT_EQ(pkt.cbpkt_header.chid, 0x8000); + EXPECT_EQ(pkt.cbpkt_header.type, 0x88); + EXPECT_EQ(pkt.cbpkt_header.dlen, 0u); // No payload for REQCONFIGALL +} + +TEST_F(SdkHandshakeTest, PacketSize_Runlevel) { + // Verify packet size calculation + cbPKT_SYSINFO sysinfo; + sysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; // Use macro for correct header size + + size_t packet_size = (sysinfo.cbpkt_header.dlen + cbPKT_HEADER_32SIZE) * 4; + + EXPECT_EQ(packet_size, sizeof(cbPKT_SYSINFO)); + EXPECT_GT(packet_size, 0u); + EXPECT_LE(packet_size, sizeof(cbPKT_GENERIC)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Runlevel Constants Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkHandshakeTest, RunlevelConstants_Values) { + // Verify runlevel constant values are as expected + EXPECT_EQ(cbRUNLEVEL_STARTUP, 10); + EXPECT_EQ(cbRUNLEVEL_HARDRESET, 20); + EXPECT_EQ(cbRUNLEVEL_STANDBY, 30); + EXPECT_EQ(cbRUNLEVEL_RESET, 40); + EXPECT_EQ(cbRUNLEVEL_RUNNING, 50); + EXPECT_EQ(cbRUNLEVEL_STRESSED, 60); + EXPECT_EQ(cbRUNLEVEL_ERROR, 70); + EXPECT_EQ(cbRUNLEVEL_SHUTDOWN, 80); +} + +TEST_F(SdkHandshakeTest, RunlevelConstants_Ordering) { + // Verify runlevels are in ascending order + EXPECT_LT(cbRUNLEVEL_STARTUP, cbRUNLEVEL_HARDRESET); + EXPECT_LT(cbRUNLEVEL_HARDRESET, cbRUNLEVEL_STANDBY); + EXPECT_LT(cbRUNLEVEL_STANDBY, cbRUNLEVEL_RESET); + EXPECT_LT(cbRUNLEVEL_RESET, cbRUNLEVEL_RUNNING); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Packet Type Constants Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkHandshakeTest, PacketTypes_Configuration) { + // Verify configuration packet types + EXPECT_EQ(cbPKTTYPE_SYSREP, 0x10); + EXPECT_EQ(cbPKTTYPE_SYSREPRUNLEV, 0x12); + EXPECT_EQ(cbPKTTYPE_SYSSETRUNLEV, 0x92); + EXPECT_EQ(cbPKTCHAN_CONFIGURATION, 0x8000); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Packet Creation Logic Tests (verifying setSystemRunLevel implementation) +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Note: We test packet creation logic directly rather than calling setSystemRunLevel() +// because that would require a running session with send threads. These tests verify +// that the packet construction logic is correct. + +TEST_F(SdkHandshakeTest, PacketCreation_RunlevelWithParameters) { + // This test recreates the logic from setSystemRunLevel() to verify packet construction + cbPKT_SYSINFO sysinfo; + std::memset(&sysinfo, 0, sizeof(sysinfo)); + + // Fill header (as setSystemRunLevel does) + sysinfo.cbpkt_header.time = 1; + sysinfo.cbpkt_header.chid = 0x8000; + sysinfo.cbpkt_header.type = 0x92; + sysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; // Use macro for correct header size + sysinfo.cbpkt_header.instrument = 0; + + // Fill payload with specific values + uint32_t test_runlevel = cbRUNLEVEL_RUNNING; + uint32_t test_resetque = 5; + uint32_t test_runflags = 2; + + sysinfo.runlevel = test_runlevel; + sysinfo.resetque = test_resetque; + sysinfo.runflags = test_runflags; + + // Copy to generic packet (as setSystemRunLevel does) + cbPKT_GENERIC pkt; + std::memcpy(&pkt, &sysinfo, sizeof(sysinfo)); + + // Verify header fields persisted + EXPECT_EQ(pkt.cbpkt_header.chid, 0x8000); + EXPECT_EQ(pkt.cbpkt_header.type, 0x92); + EXPECT_GT(pkt.cbpkt_header.dlen, 0u); + + // Verify payload by casting back + const cbPKT_SYSINFO* verify_sysinfo = reinterpret_cast(&pkt); + EXPECT_EQ(verify_sysinfo->runlevel, test_runlevel); + EXPECT_EQ(verify_sysinfo->resetque, test_resetque); + EXPECT_EQ(verify_sysinfo->runflags, test_runflags); +} + +TEST_F(SdkHandshakeTest, PacketCreation_AllRunlevels) { + // Verify packet creation works for all runlevel values + uint32_t runlevels[] = { + cbRUNLEVEL_STARTUP, + cbRUNLEVEL_HARDRESET, + cbRUNLEVEL_STANDBY, + cbRUNLEVEL_RESET, + cbRUNLEVEL_RUNNING, + cbRUNLEVEL_STRESSED, + cbRUNLEVEL_ERROR, + cbRUNLEVEL_SHUTDOWN + }; + + for (uint32_t runlevel : runlevels) { + cbPKT_SYSINFO sysinfo; + std::memset(&sysinfo, 0, sizeof(sysinfo)); + + sysinfo.cbpkt_header.chid = 0x8000; + sysinfo.cbpkt_header.type = 0x92; + sysinfo.cbpkt_header.dlen = cbPKTDLEN_SYSINFO; // Use macro for correct header size + sysinfo.runlevel = runlevel; + + cbPKT_GENERIC pkt; + std::memcpy(&pkt, &sysinfo, sizeof(sysinfo)); + + const cbPKT_SYSINFO* verify = reinterpret_cast(&pkt); + EXPECT_EQ(verify->runlevel, runlevel) << "Runlevel " << runlevel << " not preserved"; + } +} + +TEST_F(SdkHandshakeTest, PacketCreation_ConfigurationRequest) { + // Verify packet creation for requestConfiguration() + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + + // Fill header (as requestConfiguration does) + pkt.cbpkt_header.time = 1; + pkt.cbpkt_header.chid = 0x8000; + pkt.cbpkt_header.type = 0x88; // cbPKTTYPE_REQCONFIGALL + pkt.cbpkt_header.dlen = 0; + pkt.cbpkt_header.instrument = 0; + + // Verify all fields + EXPECT_EQ(pkt.cbpkt_header.time, 1u); + EXPECT_EQ(pkt.cbpkt_header.chid, 0x8000); + EXPECT_EQ(pkt.cbpkt_header.type, 0x88); + EXPECT_EQ(pkt.cbpkt_header.dlen, 0u); + EXPECT_EQ(pkt.cbpkt_header.instrument, 0u); +} + diff --git a/tests/unit/test_sdk_session.cpp b/tests/unit/test_sdk_session.cpp new file mode 100644 index 00000000..2cdc92d8 --- /dev/null +++ b/tests/unit/test_sdk_session.cpp @@ -0,0 +1,431 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_sdk_session.cpp +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Unit tests for cbsdk::SdkSession +/// +/// Tests the SDK orchestration of cbdev + cbshm with the two-stage pipeline. +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include // for memset/memcpy + +// Include protocol types and session headers +#include // Protocol types +#include "cbshm/shmem_session.h" // For transmit callback test +#include "cbdev/device_session.h" // For loopback test +#include "cbdev/device_factory.h" // For createDeviceSession +#include "cbsdk/sdk_session.h" // SDK orchestration + +using namespace cbsdk; + +/// Test fixture for SdkSession tests +class SdkSessionTest : public ::testing::Test { +protected: + void SetUp() override { + test_name = "test_sdk_" + std::to_string(test_counter++); + } + + void TearDown() override { + // Cleanup happens automatically via RAII + } + + std::string test_name; + static int test_counter; +}; + +int SdkSessionTest::test_counter = 0; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkSessionTest, Config_Default) { + SdkConfig config; + + EXPECT_EQ(config.device_type, DeviceType::LEGACY_NSP); + EXPECT_EQ(config.callback_queue_depth, 16384); + EXPECT_TRUE(config.autorun); // Default is to auto-run (full handshake) +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Creation Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkSessionTest, Create_Standalone_Loopback) { + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.autorun = false; // Don't auto-run (test mode) + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()) << "Error: " << result.error(); + + auto session = std::move(result.value()); // Move session out of Result + EXPECT_TRUE(session.isRunning()); // Session is started, just not in full auto-run mode +} + +TEST_F(SdkSessionTest, Create_MoveConstruction) { + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.autorun = false; + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()); + + // Move construct + SdkSession session2(std::move(result.value())); + EXPECT_TRUE(session2.isRunning()); // Session is started +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Session Lifecycle Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkSessionTest, StartStop) { + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.autorun = false; + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + + // Session is already started by create() + EXPECT_TRUE(session.isRunning()); + + // Give threads time to start + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Stop session + session.stop(); + EXPECT_FALSE(session.isRunning()); +} + +TEST_F(SdkSessionTest, StartTwice_Error) { + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.autorun = false; + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + + // Session is already started by create() + EXPECT_TRUE(session.isRunning()); + + // Try to start again - should fail + auto start_result = session.start(); + EXPECT_TRUE(start_result.isError()); + + session.stop(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Callback Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkSessionTest, SetCallbacks) { + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.autorun = false; + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + + bool packet_callback_invoked = false; + bool error_callback_invoked = false; + + session.registerPacketCallback([&packet_callback_invoked](const cbPKT_GENERIC& pkt) { + packet_callback_invoked = true; + }); + + session.setErrorCallback([&error_callback_invoked](const std::string& error) { + error_callback_invoked = true; + }); + + // Callbacks set successfully + EXPECT_FALSE(packet_callback_invoked); + EXPECT_FALSE(error_callback_invoked); +} + +TEST_F(SdkSessionTest, ReceivePackets_FromDevice) { + // Create SDK session (receiver) - receives real packets from connected device + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.autorun = false; + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Set up callback to count packets + std::atomic packets_received{0}; + session.registerPacketCallback([&packets_received](const cbPKT_GENERIC& pkt) { + packets_received.fetch_add(1); + }); + + // Session is already started by create() + EXPECT_TRUE(session.isRunning()); + + // Wait for packets from the device + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + // Stop session + session.stop(); + + // Verify packets were received from the device + EXPECT_GT(packets_received.load(), 0); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Statistics Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkSessionTest, Statistics_Valid) { + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.autorun = false; + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + + // With a real device, packets may already be flowing; just verify stats are consistent + auto stats = session.getStats(); + EXPECT_GE(stats.packets_received_from_device, stats.packets_stored_to_shmem); + EXPECT_EQ(stats.packets_dropped, 0); +} + +TEST_F(SdkSessionTest, Statistics_ResetStats) { + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.autorun = false; + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()); + + auto& session = result.value(); + + // Start and immediately stop (generates some internal activity) + session.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + session.stop(); + + // Reset stats + session.resetStats(); + + auto stats = session.getStats(); + EXPECT_EQ(stats.packets_received_from_device, 0); + EXPECT_EQ(stats.packets_stored_to_shmem, 0); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Configuration Access Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkSessionTest, GetConfig) { + SdkConfig config; + config.device_type = DeviceType::HUB1; + config.callback_queue_depth = 8192; + config.autorun = false; + + auto result = SdkSession::create(config); + ASSERT_TRUE(result.isOk()) << "Error: " << result.error(); + + auto& session = result.value(); + const auto& retrieved_config = session.getConfig(); + + EXPECT_EQ(retrieved_config.device_type, DeviceType::HUB1); + EXPECT_EQ(retrieved_config.callback_queue_depth, 8192); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// SPSC Queue Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +TEST_F(SdkSessionTest, SPSCQueue_PushPop) { + SPSCQueue queue; + + EXPECT_TRUE(queue.empty()); + EXPECT_EQ(queue.size(), 0); + + // Push items + EXPECT_TRUE(queue.push(1)); + EXPECT_TRUE(queue.push(2)); + EXPECT_TRUE(queue.push(3)); + + EXPECT_FALSE(queue.empty()); + EXPECT_EQ(queue.size(), 3); + + // Pop items + int val; + EXPECT_TRUE(queue.pop(val)); + EXPECT_EQ(val, 1); + + EXPECT_TRUE(queue.pop(val)); + EXPECT_EQ(val, 2); + + EXPECT_TRUE(queue.pop(val)); + EXPECT_EQ(val, 3); + + EXPECT_TRUE(queue.empty()); +} + +TEST_F(SdkSessionTest, SPSCQueue_Overflow) { + SPSCQueue queue; // Small queue (capacity 3) + + EXPECT_TRUE(queue.push(1)); + EXPECT_TRUE(queue.push(2)); + EXPECT_TRUE(queue.push(3)); + + // Queue should be full now (one slot reserved) + EXPECT_FALSE(queue.push(4)); // Should fail + + // Pop one item + int val; + EXPECT_TRUE(queue.pop(val)); + + // Now we can push again + EXPECT_TRUE(queue.push(4)); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Packet Transmission Tests +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// Helper to print packet header for debugging +void print_packet_header(const char* label, const cbPKT_GENERIC& pkt) { + printf("%s:\n", label); + printf(" time: 0x%08llu (%llu)\n", pkt.cbpkt_header.time, pkt.cbpkt_header.time); + printf(" chid: 0x%04X\n", pkt.cbpkt_header.chid); + printf(" type: 0x%02X\n", pkt.cbpkt_header.type); + printf(" dlen: %u\n", pkt.cbpkt_header.dlen); + printf(" instrument: %u\n", pkt.cbpkt_header.instrument); + + // Print first few dwords after header for debugging + const uint32_t* data = reinterpret_cast(&pkt); + printf(" First 8 dwords: "); + for (int i = 0; i < 8 && i < pkt.cbpkt_header.dlen + 2; i++) { + printf("0x%08X ", data[i]); + } + printf("\n"); +} + +// Test packet header construction +TEST_F(SdkSessionTest, PacketHeader_BasicFields) { + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + + // Set header fields + pkt.cbpkt_header.time = 1; + pkt.cbpkt_header.chid = 0x8000; + pkt.cbpkt_header.type = 0x92; + pkt.cbpkt_header.dlen = 10; + pkt.cbpkt_header.instrument = 0; + + print_packet_header("Basic header test", pkt); + + // Verify fields + EXPECT_EQ(pkt.cbpkt_header.time, 1u); + EXPECT_EQ(pkt.cbpkt_header.chid, 0x8000); + EXPECT_EQ(pkt.cbpkt_header.type, 0x92); + EXPECT_EQ(pkt.cbpkt_header.dlen, 10u); +} + +// Test packet size calculation +TEST_F(SdkSessionTest, PacketSize_Calculation) { + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.dlen = 10; + + // Calculate packet size (as device_session send thread does) + size_t packet_size = (pkt.cbpkt_header.dlen + 2) * 4; + + printf("dlen = %u\n", pkt.cbpkt_header.dlen); + printf("Calculated packet_size = %zu\n", packet_size); + + // Verify packet size makes sense + EXPECT_EQ(packet_size, 48u); // (10 + 2) * 4 = 48 bytes + EXPECT_GT(packet_size, 0u); + EXPECT_LT(packet_size, sizeof(cbPKT_GENERIC)); +} + +// Test transmit callback - enqueue and dequeue packet through shared memory +TEST_F(SdkSessionTest, TransmitCallback_RoundTrip) { + // Create shared memory session for testing (use short name to avoid length limits) + std::string name = "xmt_rt"; + auto shmem_result = cbshm::ShmemSession::create( + name, name + "_r", name + "_x", name + "_xl", + name + "_s", name + "_p", name + "_g", + cbshm::Mode::STANDALONE); + ASSERT_TRUE(shmem_result.isOk()) << "Failed to create shmem: " << shmem_result.error(); + auto shmem = std::move(shmem_result.value()); + + // Create a test packet with known values + cbPKT_GENERIC test_pkt; + std::memset(&test_pkt, 0, sizeof(test_pkt)); + test_pkt.cbpkt_header.time = 1; + test_pkt.cbpkt_header.chid = 0x8000; + test_pkt.cbpkt_header.type = 0x92; // cbPKTTYPE_SYSSETRUNLEV + test_pkt.cbpkt_header.dlen = 10; + test_pkt.cbpkt_header.instrument = 0; + + // Set some payload data (first few dwords after header) + uint32_t* payload = reinterpret_cast(&test_pkt); + payload[5] = 0xDEADBEEF; // Test pattern + payload[6] = 20; // Simulated runlevel value + + printf("Original packet before enqueue:\n"); + print_packet_header(" ", test_pkt); + + // Enqueue the packet + auto enqueue_result = shmem.enqueuePacket(test_pkt); + ASSERT_TRUE(enqueue_result.isOk()) << "Failed to enqueue: " << enqueue_result.error(); + + // Now dequeue using the callback + cbPKT_GENERIC retrieved_pkt; + std::memset(&retrieved_pkt, 0, sizeof(retrieved_pkt)); + + auto dequeue_result = shmem.dequeuePacket(retrieved_pkt); + ASSERT_TRUE(dequeue_result.isOk()) << "Failed to dequeue: " << dequeue_result.error(); + ASSERT_TRUE(dequeue_result.value()) << "dequeuePacket returned false (no packet available)"; + + printf("\nRetrieved packet after dequeue:\n"); + print_packet_header(" ", retrieved_pkt); + + // Verify all header fields match + EXPECT_EQ(retrieved_pkt.cbpkt_header.time, test_pkt.cbpkt_header.time) + << "Time field mismatch"; + EXPECT_EQ(retrieved_pkt.cbpkt_header.chid, test_pkt.cbpkt_header.chid) + << "Chid field mismatch"; + EXPECT_EQ(retrieved_pkt.cbpkt_header.type, test_pkt.cbpkt_header.type) + << "Type field mismatch"; + EXPECT_EQ(retrieved_pkt.cbpkt_header.dlen, test_pkt.cbpkt_header.dlen) + << "Dlen field mismatch"; + EXPECT_EQ(retrieved_pkt.cbpkt_header.instrument, test_pkt.cbpkt_header.instrument) + << "Instrument field mismatch"; + + // Verify payload data + const uint32_t* original_payload = reinterpret_cast(&test_pkt); + const uint32_t* retrieved_payload = reinterpret_cast(&retrieved_pkt); + EXPECT_EQ(retrieved_payload[5], original_payload[5]) + << "Payload[5] mismatch (test pattern)"; + EXPECT_EQ(retrieved_payload[6], original_payload[6]) + << "Payload[6] mismatch (simulated runlevel)"; + + printf("\nPayload comparison:\n"); + printf(" Original payload[5]: 0x%08X\n", original_payload[5]); + printf(" Retrieved payload[5]: 0x%08X\n", retrieved_payload[5]); + printf(" Original payload[6]: 0x%08X (%u)\n", original_payload[6], original_payload[6]); + printf(" Retrieved payload[6]: 0x%08X (%u)\n", retrieved_payload[6], retrieved_payload[6]); +} diff --git a/tests/unit/test_shmem_session.cpp b/tests/unit/test_shmem_session.cpp new file mode 100644 index 00000000..92ba7abd --- /dev/null +++ b/tests/unit/test_shmem_session.cpp @@ -0,0 +1,1926 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file test_shmem_session.cpp +/// @author CereLink Development Team +/// @date 2025-11-11 +/// +/// @brief Unit tests for ShmemSession class +/// +/// Tests shared memory session management with focus on THE KEY FIX: mode-independent indexing +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include // For packet types and cbNSP1-4 constants +#include +#include // For cbproto_protocol_version_t +#include +#include +#ifdef _WIN32 +#include // GetCurrentProcessId() +#else +#include // getpid() +#endif + +using namespace cbshm; +using namespace cbproto; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Test fixture for ShmemSession tests +/// +class ShmemSessionTest : public ::testing::Test { +protected: + void SetUp() override { + // Use unique names for each test to avoid conflicts + test_name = "test_shmem_" + std::to_string(test_counter++); + } + + void TearDown() override { + // Sessions are automatically closed in destructor + } + + std::string test_name; + static int test_counter; +}; + +int ShmemSessionTest::test_counter = 0; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Basic Lifecycle Tests +/// @{ + +TEST_F(ShmemSessionTest, CreateStandalone) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()) << "Failed to create standalone session: " << result.error(); + + auto& session = result.value(); + EXPECT_TRUE(session.isOpen()); + EXPECT_EQ(session.getMode(), Mode::STANDALONE); +} + +TEST_F(ShmemSessionTest, CreateAndDestroy) { + { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + EXPECT_TRUE(result.value().isOpen()); + } + // Session should be closed after scope exit +} + +TEST_F(ShmemSessionTest, MoveConstruction) { + auto result1 = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result1.isOk()); + + // Move construction + ShmemSession session2(std::move(result1.value())); + EXPECT_TRUE(session2.isOpen()); +} + +TEST_F(ShmemSessionTest, MoveAssignment) { + auto result1 = ShmemSession::create(test_name + "_1", test_name + "_1_rec", test_name + "_1_xmt", test_name + "_1_xmt_local", test_name + "_1_status", test_name + "_1_spk", test_name + "_1_signal", Mode::STANDALONE); + auto result2 = ShmemSession::create(test_name + "_2", test_name + "_2_rec", test_name + "_2_xmt", test_name + "_2_xmt_local", test_name + "_2_status", test_name + "_2_spk", test_name + "_2_signal", Mode::STANDALONE); + ASSERT_TRUE(result1.isOk()); + ASSERT_TRUE(result2.isOk()); + + // Move assignment + result1.value() = std::move(result2.value()); + EXPECT_TRUE(result1.value().isOpen()); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Instrument Status Tests +/// @{ + +TEST_F(ShmemSessionTest, InstrumentStatusInitiallyInactive) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // All instruments should be inactive initially + for (uint8_t i = 1; i <= cbMAXOPEN; ++i) { + auto id = InstrumentId::fromOneBased(i); + auto active_result = session.isInstrumentActive(id); + ASSERT_TRUE(active_result.isOk()); + EXPECT_FALSE(active_result.value()) << "Instrument " << (int)i << " should be inactive"; + } +} + +TEST_F(ShmemSessionTest, SetInstrumentActive) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Activate first instrument + auto id1 = InstrumentId::fromOneBased(cbNSP1); + auto set_result = session.setInstrumentActive(id1, true); + ASSERT_TRUE(set_result.isOk()); + + // Verify it's active + auto active_result = session.isInstrumentActive(id1); + ASSERT_TRUE(active_result.isOk()); + EXPECT_TRUE(active_result.value()); + + // Other instruments should still be inactive + auto id2 = InstrumentId::fromOneBased(cbNSP2); + auto active_result2 = session.isInstrumentActive(id2); + ASSERT_TRUE(active_result2.isOk()); + EXPECT_FALSE(active_result2.value()); +} + +TEST_F(ShmemSessionTest, GetFirstActiveInstrument) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // No active instruments initially + auto first_result = session.getFirstActiveInstrument(); + EXPECT_TRUE(first_result.isError()); + EXPECT_NE(first_result.error().find("No active"), std::string::npos); + + // Activate third instrument + auto id3 = InstrumentId::fromOneBased(cbNSP3); + ASSERT_TRUE(session.setInstrumentActive(id3, true).isOk()); + + // Should return third instrument (index 2, 1-based = 3) + first_result = session.getFirstActiveInstrument(); + ASSERT_TRUE(first_result.isOk()); + EXPECT_EQ(first_result.value().toOneBased(), cbNSP3); +} + +TEST_F(ShmemSessionTest, MultipleActiveInstruments) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Activate instruments 1 and 3 + ASSERT_TRUE(session.setInstrumentActive(InstrumentId::fromOneBased(cbNSP1), true).isOk()); + ASSERT_TRUE(session.setInstrumentActive(InstrumentId::fromOneBased(cbNSP3), true).isOk()); + + // First active should be cbNSP1 + auto first_result = session.getFirstActiveInstrument(); + ASSERT_TRUE(first_result.isOk()); + EXPECT_EQ(first_result.value().toOneBased(), cbNSP1); + + // Verify both are active + EXPECT_TRUE(session.isInstrumentActive(InstrumentId::fromOneBased(cbNSP1)).value()); + EXPECT_TRUE(session.isInstrumentActive(InstrumentId::fromOneBased(cbNSP3)).value()); + EXPECT_FALSE(session.isInstrumentActive(InstrumentId::fromOneBased(cbNSP2)).value()); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Packet Routing Tests (THE KEY FIX!) +/// @{ + +TEST_F(ShmemSessionTest, SetGetProcInfo_Instrument0) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Set PROCINFO for instrument 0 (cbNSP1) + cbPKT_PROCINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 1; + info.chancount = 256; + + auto id = InstrumentId::fromPacketField(0); + ASSERT_TRUE(session.setProcInfo(id, info).isOk()); + ASSERT_TRUE(session.setInstrumentActive(id, true).isOk()); + + // THE KEY FIX: Should be stored at index 0 (instrument 0) + auto get_result = session.getProcInfo(id); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 1); + EXPECT_EQ(get_result.value().chancount, 256); + + // Instrument should be marked active + auto active_result = session.isInstrumentActive(id); + ASSERT_TRUE(active_result.isOk()); + EXPECT_TRUE(active_result.value()); +} + +TEST_F(ShmemSessionTest, SetGetProcInfo_Instrument2) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Set PROCINFO for instrument 2 (cbNSP3) + cbPKT_PROCINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 3; + info.chancount = 128; + + auto id = InstrumentId::fromPacketField(2); + ASSERT_TRUE(session.setProcInfo(id, info).isOk()); + + // THE KEY FIX: Should be stored at index 2, NOT index 0! + auto get_result = session.getProcInfo(id); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 3); + EXPECT_EQ(get_result.value().chancount, 128); + + // Verify it's NOT stored at index 0 + auto id0 = InstrumentId::fromPacketField(0); + auto get_result0 = session.getProcInfo(id0); + ASSERT_TRUE(get_result0.isOk()); + EXPECT_NE(get_result0.value().proc, 3); // Should not have this data +} + +TEST_F(ShmemSessionTest, SetGetProcInfo_MultipleInstruments) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Set PROCINFO for instruments 0, 1, and 3 + for (uint8_t inst : {0, 1, 3}) { + cbPKT_PROCINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = inst + 1; + info.chancount = 100 + inst; + + auto id = InstrumentId::fromPacketField(inst); + ASSERT_TRUE(session.setProcInfo(id, info).isOk()); + } + + // Verify each instrument has correct data at correct index + for (uint8_t inst : {0, 1, 3}) { + auto id = InstrumentId::fromPacketField(inst); + auto get_result = session.getProcInfo(id); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, inst + 1); + EXPECT_EQ(get_result.value().chancount, 100 + inst); + } +} + +TEST_F(ShmemSessionTest, SetGetBankInfo) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Set BANKINFO for instrument 1 (cbNSP2), bank 3 + cbPKT_BANKINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 2; + info.bank = 3; + info.chancount = 32; + + auto id = InstrumentId::fromPacketField(1); + ASSERT_TRUE(session.setBankInfo(id, 3, info).isOk()); + + // Retrieve and verify + auto get_result = session.getBankInfo(id, 3); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 2); + EXPECT_EQ(get_result.value().bank, 3); + EXPECT_EQ(get_result.value().chancount, 32); +} + +TEST_F(ShmemSessionTest, SetGetFilterInfo) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Set FILTINFO for instrument 0 (cbNSP1), filter 5 + cbPKT_FILTINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 1; + info.filt = 5; + info.hpfreq = 250000; // 250 Hz in millihertz + + auto id = InstrumentId::fromPacketField(0); + ASSERT_TRUE(session.setFilterInfo(id, 5, info).isOk()); + + // Retrieve and verify + auto get_result = session.getFilterInfo(id, 5); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 1); + EXPECT_EQ(get_result.value().filt, 5); + EXPECT_EQ(get_result.value().hpfreq, 250000); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Configuration Read/Write Tests +/// @{ + +TEST_F(ShmemSessionTest, GetProcInfo_NotFound) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Try to get PROCINFO before storing anything + auto id = InstrumentId::fromOneBased(cbNSP1); + auto get_result = session.getProcInfo(id); + // Should succeed but return zeroed data + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 0); +} + +TEST_F(ShmemSessionTest, SetAndGetProcInfo) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create and set PROCINFO + cbPKT_PROCINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 2; + info.chancount = 512; + info.bankcount = 24; + + auto id = InstrumentId::fromOneBased(cbNSP2); + auto set_result = session.setProcInfo(id, info); + ASSERT_TRUE(set_result.isOk()); + + // Retrieve and verify + auto get_result = session.getProcInfo(id); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 2); + EXPECT_EQ(get_result.value().chancount, 512); + EXPECT_EQ(get_result.value().bankcount, 24); +} + +TEST_F(ShmemSessionTest, InvalidInstrumentId) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Try to use invalid instrument ID (0 is invalid, 1-4 are valid) + auto invalid_id = InstrumentId::fromOneBased(0); + EXPECT_FALSE(invalid_id.isValid()); + + auto get_result = session.getProcInfo(invalid_id); + EXPECT_TRUE(get_result.isError()); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Error Handling Tests +/// @{ + +TEST_F(ShmemSessionTest, OperationsOnClosedSession) { + // Create a session in a scope + std::string name = test_name; + { + auto result = ShmemSession::create(name, name + "_rec", name + "_xmt", name + "_xmt_local", name + "_status", name + "_spk", name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + } + // Session is now closed + + // Try to create a new session with same name should work + auto result2 = ShmemSession::create(name, name + "_rec", name + "_xmt", name + "_xmt_local", name + "_status", name + "_spk", name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result2.isOk()); +} + +TEST_F(ShmemSessionTest, StorePacket_InvalidInstrument) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create config packet with invalid instrument ID + // This should succeed (packet written to receive buffer) but skip config buffer update + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.instrument = 255; // Way out of range + pkt.cbpkt_header.type = cbPKTTYPE_PROCREP; + + auto store_result = session.storePacket(pkt); + EXPECT_TRUE(store_result.isOk()) << "Packet should be stored to receive buffer even with invalid instrument ID"; + + // Create non-config packet with invalid instrument ID - should also succeed + cbPKT_GENERIC lnc_pkt; + std::memset(&lnc_pkt, 0, sizeof(lnc_pkt)); + lnc_pkt.cbpkt_header.instrument = 83; // Invalid ID + lnc_pkt.cbpkt_header.type = 0x28; // cbPKTTYPE_LNCREP + + auto lnc_result = session.storePacket(lnc_pkt); + EXPECT_TRUE(lnc_result.isOk()) << "Non-config packet should succeed regardless of instrument ID"; +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Advanced Packet Handler Tests +/// @{ + +TEST_F(ShmemSessionTest, StorePacket_ADAPTFILTINFO) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create ADAPTFILTINFO packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 1; // cbNSP2 + pkt.cbpkt_header.type = cbPKTTYPE_ADAPTFILTREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_ADAPTFILTINFO; + + cbPKT_ADAPTFILTINFO* adapt_pkt = reinterpret_cast(&pkt); + adapt_pkt->chan = 10; + adapt_pkt->nMode = 1; // Filter continuous & spikes + adapt_pkt->dLearningRate = 0.05f; + adapt_pkt->nRefChan1 = 5; + adapt_pkt->nRefChan2 = 6; + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // TODO: Add getter method for adaptinfo and verify + // For now, just verify packet was stored without error +} + +TEST_F(ShmemSessionTest, StorePacket_REFELECFILTINFO) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create REFELECFILTINFO packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; // cbNSP1 + pkt.cbpkt_header.type = cbPKTTYPE_REFELECFILTREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_REFELECFILTINFO; + + cbPKT_REFELECFILTINFO* refelec_pkt = reinterpret_cast(&pkt); + refelec_pkt->chan = 15; + refelec_pkt->nMode = 2; // Filter spikes only + refelec_pkt->nRefChan = 8; + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); +} + +TEST_F(ShmemSessionTest, StorePacket_SS_STATUS) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create SS_STATUS packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_SS_STATUSREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_SS_STATUS; + + cbPKT_SS_STATUS* status_pkt = reinterpret_cast(&pkt); + status_pkt->cntlUnitStats.nMode = ADAPT_ALWAYS; // Always adapt unit stats + status_pkt->cntlNumUnits.nMode = ADAPT_ALWAYS; // Always adapt unit numbers + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); +} + +TEST_F(ShmemSessionTest, StorePacket_SS_DETECT) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create SS_DETECT packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_SS_DETECTREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_SS_DETECT; + + cbPKT_SS_DETECT* detect_pkt = reinterpret_cast(&pkt); + detect_pkt->fThreshold = -50.0f; // Detection threshold + detect_pkt->fMultiplier = 4.5f; + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); +} + +TEST_F(ShmemSessionTest, StorePacket_SS_ARTIF_REJECT) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create SS_ARTIF_REJECT packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_SS_ARTIF_REJECTREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_SS_ARTIF_REJECT; + + cbPKT_SS_ARTIF_REJECT* artif_pkt = reinterpret_cast(&pkt); + artif_pkt->nMaxSimulChans = 3; // Max simultaneous channels + artif_pkt->nRefractoryCount = 10; + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); +} + +TEST_F(ShmemSessionTest, StorePacket_SS_NOISE_BOUNDARY) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create SS_NOISE_BOUNDARY packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_SS_NOISE_BOUNDARYREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_SS_NOISE_BOUNDARY; + + cbPKT_SS_NOISE_BOUNDARY* noise_pkt = reinterpret_cast(&pkt); + noise_pkt->chan = 25; // 1-based channel ID + noise_pkt->afc[0] = -100.0f; // Center of ellipsoid (x coordinate) + noise_pkt->afc[1] = 0.0f; // Center of ellipsoid (y coordinate) + noise_pkt->afc[2] = 0.0f; // Center of ellipsoid (z coordinate) + + // Store packet - should be stored at index chan-1 (24) + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // Test boundary condition - channel 0 should be rejected + cbPKT_GENERIC pkt_invalid; + std::memset(&pkt_invalid, 0, sizeof(pkt_invalid)); + pkt_invalid.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt_invalid.cbpkt_header.instrument = 0; + pkt_invalid.cbpkt_header.type = cbPKTTYPE_SS_NOISE_BOUNDARYREP; + cbPKT_SS_NOISE_BOUNDARY* noise_pkt_invalid = reinterpret_cast(&pkt_invalid); + noise_pkt_invalid->chan = 0; // Invalid + + ASSERT_TRUE(session.storePacket(pkt_invalid).isOk()); // Should succeed but not store to config +} + +TEST_F(ShmemSessionTest, StorePacket_SS_STATISTICS) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create SS_STATISTICS packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_SS_STATISTICSREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_SS_STATISTICS; + + cbPKT_SS_STATISTICS* stats_pkt = reinterpret_cast(&pkt); + stats_pkt->nUpdateSpikes = 1000; + stats_pkt->nAutoalg = cbAUTOALG_PCA; + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); +} + +TEST_F(ShmemSessionTest, StorePacket_SS_MODEL) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create SS_MODELREP packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_SS_MODELREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_SS_MODELSET; + + cbPKT_SS_MODELSET* model_pkt = reinterpret_cast(&pkt); + model_pkt->chan = 10; // 0-based channel + model_pkt->unit_number = 1; // Unit 1 + model_pkt->valid = 1; + model_pkt->inverted = 0; + model_pkt->num_samples = 100; + model_pkt->mu_x[0] = 50.0f; + model_pkt->mu_x[1] = 75.0f; + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // Test boundary conditions + cbPKT_GENERIC pkt_invalid; + std::memset(&pkt_invalid, 0, sizeof(pkt_invalid)); + pkt_invalid.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt_invalid.cbpkt_header.instrument = 0; + pkt_invalid.cbpkt_header.type = cbPKTTYPE_SS_MODELREP; + cbPKT_SS_MODELSET* model_invalid = reinterpret_cast(&pkt_invalid); + model_invalid->chan = 9999; // Out of range + model_invalid->unit_number = 0; + + ASSERT_TRUE(session.storePacket(pkt_invalid).isOk()); // Should succeed but not store to config +} + +TEST_F(ShmemSessionTest, StorePacket_FS_BASIS) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create FS_BASISREP packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_FS_BASISREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_FS_BASIS; + + cbPKT_FS_BASIS* basis_pkt = reinterpret_cast(&pkt); + basis_pkt->chan = 20; // 1-based channel + basis_pkt->mode = 1; // PCA basis + basis_pkt->fs = cbAUTOALG_PCA; + + // Store packet - should be stored at index chan-1 (19) + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // Test invalid channel + cbPKT_GENERIC pkt_invalid; + std::memset(&pkt_invalid, 0, sizeof(pkt_invalid)); + pkt_invalid.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt_invalid.cbpkt_header.instrument = 0; + pkt_invalid.cbpkt_header.type = cbPKTTYPE_FS_BASISREP; + cbPKT_FS_BASIS* basis_invalid = reinterpret_cast(&pkt_invalid); + basis_invalid->chan = 0; // Invalid (1-based, so 0 is invalid) + + ASSERT_TRUE(session.storePacket(pkt_invalid).isOk()); // Should succeed but not store to config +} + +TEST_F(ShmemSessionTest, StorePacket_LNC) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create LNCREP packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 1; // cbNSP2 + pkt.cbpkt_header.type = cbPKTTYPE_LNCREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_LNC; + + cbPKT_LNC* lnc_pkt = reinterpret_cast(&pkt); + lnc_pkt->lncFreq = 60; // 60 Hz line noise + lnc_pkt->lncRefChan = 10; + lnc_pkt->lncGlobalMode = 1; + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); +} + +TEST_F(ShmemSessionTest, StorePacket_FILECFG) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create REPFILECFG packet with REC option + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_REPFILECFG; + pkt.cbpkt_header.dlen = cbPKTDLEN_FILECFG; + + cbPKT_FILECFG* file_pkt = reinterpret_cast(&pkt); + file_pkt->options = cbFILECFG_OPT_REC; // Recording + file_pkt->duration = 3600; // 1 hour + file_pkt->recording = 1; + std::strncpy(file_pkt->filename, "test_recording.nev", cbLEN_STR_COMMENT); + + // Store packet - should be stored + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // Create packet with non-REC/STOP/TIMEOUT option - should not be stored + cbPKT_GENERIC pkt_other; + std::memset(&pkt_other, 0, sizeof(pkt_other)); + pkt_other.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt_other.cbpkt_header.instrument = 0; + pkt_other.cbpkt_header.type = cbPKTTYPE_REPFILECFG; + cbPKT_FILECFG* file_other = reinterpret_cast(&pkt_other); + file_other->options = cbFILECFG_OPT_NONE; // Other option + + ASSERT_TRUE(session.storePacket(pkt_other).isOk()); // Succeeds but not stored to config +} + +TEST_F(ShmemSessionTest, StorePacket_NTRODEINFO) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create REPNTRODEINFO packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_REPNTRODEINFO; + pkt.cbpkt_header.dlen = cbPKTDLEN_NTRODEINFO; + + cbPKT_NTRODEINFO* ntrode_pkt = reinterpret_cast(&pkt); + ntrode_pkt->ntrode = 5; // 1-based NTrode ID + std::strncpy(ntrode_pkt->label, "Tetrode_1", cbLEN_STR_LABEL); + ntrode_pkt->nSite = 4; // Tetrode has 4 electrodes + ntrode_pkt->fs = cbAUTOALG_PCA; + + // Store packet - should be stored at index ntrode-1 (4) + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // Test invalid ntrode ID + cbPKT_GENERIC pkt_invalid; + std::memset(&pkt_invalid, 0, sizeof(pkt_invalid)); + pkt_invalid.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt_invalid.cbpkt_header.instrument = 0; + pkt_invalid.cbpkt_header.type = cbPKTTYPE_REPNTRODEINFO; + cbPKT_NTRODEINFO* ntrode_invalid = reinterpret_cast(&pkt_invalid); + ntrode_invalid->ntrode = 0; // Invalid (1-based) + + ASSERT_TRUE(session.storePacket(pkt_invalid).isOk()); // Succeeds but not stored to config +} + +TEST_F(ShmemSessionTest, StorePacket_WAVEFORM) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create WAVEFORMREP packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_WAVEFORMREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_WAVEFORM; + + cbPKT_AOUT_WAVEFORM* wave_pkt = reinterpret_cast(&pkt); + wave_pkt->chan = 2; // 0-based channel + wave_pkt->trigNum = 1; // 0-based trigger number + wave_pkt->mode = cbWAVEFORM_MODE_SINE; + wave_pkt->repeats = 5; + wave_pkt->wave.offset = 100; + wave_pkt->wave.sineFrequency = 1000; // 1 kHz + wave_pkt->wave.sineAmplitude = 500; + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // Test invalid indices + cbPKT_GENERIC pkt_invalid; + std::memset(&pkt_invalid, 0, sizeof(pkt_invalid)); + pkt_invalid.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt_invalid.cbpkt_header.instrument = 0; + pkt_invalid.cbpkt_header.type = cbPKTTYPE_WAVEFORMREP; + cbPKT_AOUT_WAVEFORM* wave_invalid = reinterpret_cast(&pkt_invalid); + wave_invalid->chan = 999; // Out of range + wave_invalid->trigNum = 0; + + ASSERT_TRUE(session.storePacket(pkt_invalid).isOk()); // Succeeds but not stored to config +} + +TEST_F(ShmemSessionTest, StorePacket_NPLAY) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create NPLAYREP packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_NPLAYREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_NPLAY; + + cbPKT_NPLAY* nplay_pkt = reinterpret_cast(&pkt); + nplay_pkt->mode = cbNPLAY_MODE_CONFIG; // Request config + nplay_pkt->flags = cbNPLAY_FLAG_CONF; + nplay_pkt->val = 0; + nplay_pkt->speed = 1.0; + std::strncpy(nplay_pkt->fname, "playback_file.nev", sizeof(nplay_pkt->fname)); + + // Store packet + ASSERT_TRUE(session.storePacket(pkt).isOk()); +} + +TEST_F(ShmemSessionTest, StorePacket_NM_VideoSource) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create NMREP packet for video source + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_NMREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_NM; + + cbPKT_NM* nm_pkt = reinterpret_cast(&pkt); + nm_pkt->mode = cbNM_MODE_SETVIDEOSOURCE; + nm_pkt->flags = 2; // 1-based video source ID + std::strncpy(nm_pkt->name, "Camera_1", cbLEN_STR_LABEL); + nm_pkt->value = 30000; // 30 fps (in milli-fps) + + // Store packet - should be stored at index flags-1 (1) + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // Test invalid video source ID + cbPKT_GENERIC pkt_invalid; + std::memset(&pkt_invalid, 0, sizeof(pkt_invalid)); + pkt_invalid.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt_invalid.cbpkt_header.instrument = 0; + pkt_invalid.cbpkt_header.type = cbPKTTYPE_NMREP; + cbPKT_NM* nm_invalid = reinterpret_cast(&pkt_invalid); + nm_invalid->mode = cbNM_MODE_SETVIDEOSOURCE; + nm_invalid->flags = 0; // Invalid (1-based) + + ASSERT_TRUE(session.storePacket(pkt_invalid).isOk()); // Succeeds but not stored to config +} + +TEST_F(ShmemSessionTest, StorePacket_NM_TrackableObject) { + auto result = ShmemSession::create(test_name, test_name + "_rec", test_name + "_xmt", test_name + "_xmt_local", test_name + "_status", test_name + "_spk", test_name + "_signal", Mode::STANDALONE); + ASSERT_TRUE(result.isOk()); + auto& session = result.value(); + + // Create NMREP packet for trackable object + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_NMREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_NM; + + cbPKT_NM* nm_pkt = reinterpret_cast(&pkt); + nm_pkt->mode = cbNM_MODE_SETTRACKABLE; + nm_pkt->flags = 3; // 1-based trackable object ID + std::strncpy(nm_pkt->name, "LED_Marker", cbLEN_STR_LABEL); + nm_pkt->value = (4 << 16) | 1; // 4 points, type 1 + + // Store packet - should be stored at index flags-1 (2) + ASSERT_TRUE(session.storePacket(pkt).isOk()); +} + +// Note: cbNM_MODE_SETRPOS does not exist in upstream cbproto.h +// Reset test removed - if reset functionality is needed, use a different mode + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Native-Mode ShmemSession Tests +/// @{ + +class NativeShmemSessionTest : public ::testing::Test { +protected: + void SetUp() override { + test_name = "test_native_" + std::to_string(test_counter++); + } + + // Helper to create a native STANDALONE session + Result createNativeSession() { + return ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::NATIVE); + } + + std::string test_name; + static int test_counter; +}; + +int NativeShmemSessionTest::test_counter = 0; + +TEST_F(NativeShmemSessionTest, CreateNativeStandalone) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << "Failed to create native session: " << result.error(); + + auto& session = result.value(); + EXPECT_TRUE(session.isOpen()); + EXPECT_EQ(session.getMode(), Mode::STANDALONE); + EXPECT_EQ(session.getLayout(), ShmemLayout::NATIVE); +} + +TEST_F(NativeShmemSessionTest, NativeConfigBufferAccessor) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Native layout should return native config buffer + EXPECT_NE(session.getNativeConfigBuffer(), nullptr); + // Central accessor should return nullptr for native layout + EXPECT_EQ(session.getConfigBuffer(), nullptr); +} + +TEST_F(NativeShmemSessionTest, CreateAndDestroy) { + { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()); + EXPECT_TRUE(result.value().isOpen()); + } + // Session destroyed, shared memory released +} + +TEST_F(NativeShmemSessionTest, SingleInstrumentOnly) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Instrument 1 (index 0) should work + auto id1 = InstrumentId::fromOneBased(1); + auto set_result = session.setInstrumentActive(id1, true); + ASSERT_TRUE(set_result.isOk()); + + auto active_result = session.isInstrumentActive(id1); + ASSERT_TRUE(active_result.isOk()); + EXPECT_TRUE(active_result.value()); + + // Instrument 2 (index 1) should fail in native mode + auto id2 = InstrumentId::fromOneBased(2); + auto set_result2 = session.setInstrumentActive(id2, true); + EXPECT_TRUE(set_result2.isError()) << "Native mode should reject instrument index > 0"; +} + +TEST_F(NativeShmemSessionTest, GetFirstActiveInstrument) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // No active instruments initially + auto first_result = session.getFirstActiveInstrument(); + EXPECT_TRUE(first_result.isError()); + + // Activate the only instrument + auto id = InstrumentId::fromOneBased(1); + ASSERT_TRUE(session.setInstrumentActive(id, true).isOk()); + + first_result = session.getFirstActiveInstrument(); + ASSERT_TRUE(first_result.isOk()); + EXPECT_EQ(first_result.value().toOneBased(), 1); +} + +TEST_F(NativeShmemSessionTest, SetAndGetProcInfo) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_PROCINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 1; + info.chancount = 284; + info.bankcount = 16; + + auto id = InstrumentId::fromOneBased(1); + ASSERT_TRUE(session.setProcInfo(id, info).isOk()); + + auto get_result = session.getProcInfo(id); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 1); + EXPECT_EQ(get_result.value().chancount, 284); + EXPECT_EQ(get_result.value().bankcount, 16); +} + +TEST_F(NativeShmemSessionTest, SetAndGetProcInfo_RejectMultiInstrument) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_PROCINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 2; + + // Setting procinfo for instrument 2 should fail + auto id2 = InstrumentId::fromOneBased(2); + auto set_result = session.setProcInfo(id2, info); + EXPECT_TRUE(set_result.isError()); +} + +TEST_F(NativeShmemSessionTest, SetAndGetBankInfo) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_BANKINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 1; + info.bank = 3; + info.chancount = 32; + + auto id = InstrumentId::fromOneBased(1); + ASSERT_TRUE(session.setBankInfo(id, 3, info).isOk()); + + auto get_result = session.getBankInfo(id, 3); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 1); + EXPECT_EQ(get_result.value().bank, 3); + EXPECT_EQ(get_result.value().chancount, 32); +} + +TEST_F(NativeShmemSessionTest, SetAndGetFilterInfo) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_FILTINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 1; + info.filt = 5; + info.hpfreq = 250000; + + auto id = InstrumentId::fromOneBased(1); + ASSERT_TRUE(session.setFilterInfo(id, 5, info).isOk()); + + auto get_result = session.getFilterInfo(id, 5); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().filt, 5); + EXPECT_EQ(get_result.value().hpfreq, 250000); +} + +TEST_F(NativeShmemSessionTest, SetAndGetChanInfo) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_CHANINFO info; + std::memset(&info, 0, sizeof(info)); + info.chan = 10; + info.proc = 1; + info.bank = 1; + std::strncpy(info.label, "chan_10", cbLEN_STR_LABEL); + + ASSERT_TRUE(session.setChanInfo(10, info).isOk()); + + auto get_result = session.getChanInfo(10); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().chan, 10); + EXPECT_STREQ(get_result.value().label, "chan_10"); +} + +TEST_F(NativeShmemSessionTest, ChanInfo_RejectOutOfRange) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Channel 284 is valid (0-based: 0..283), 284 is out of range + cbPKT_CHANINFO info; + std::memset(&info, 0, sizeof(info)); + + auto set_result = session.setChanInfo(cbshm::NATIVE_MAXCHANS, info); + EXPECT_TRUE(set_result.isError()) << "Channel " << cbshm::NATIVE_MAXCHANS << " should be out of range for native mode"; +} + +TEST_F(NativeShmemSessionTest, StorePacket_PROCINFO) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_PROCREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_PROCINFO; + + cbPKT_PROCINFO* proc_pkt = reinterpret_cast(&pkt); + proc_pkt->proc = 1; + proc_pkt->chancount = 284; + + // storePacket writes to receive buffer only (config parsing done at device layer) + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + // Verify packet was stored to receive buffer + uint32_t received = 0, available = 0; + ASSERT_TRUE(session.getReceiveBufferStats(received, available).isOk()); + EXPECT_EQ(received, 1u); +} + +TEST_F(NativeShmemSessionTest, StorePacket_AnyInstrument) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // storePacket writes to receive buffer regardless of instrument field + // (config parsing is done at device layer, not in storePacket) + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 1; + pkt.cbpkt_header.type = cbPKTTYPE_PROCREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_PROCINFO; + + // Store should succeed (packet goes to receive buffer) + auto store_result = session.storePacket(pkt); + EXPECT_TRUE(store_result.isOk()); + + // Verify it went to receive buffer + uint32_t received = 0, available = 0; + ASSERT_TRUE(session.getReceiveBufferStats(received, available).isOk()); + EXPECT_EQ(received, 1u); +} + +TEST_F(NativeShmemSessionTest, StorePacket_CHANINFO) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.instrument = 0; + pkt.cbpkt_header.type = cbPKTTYPE_CHANREP; + pkt.cbpkt_header.dlen = cbPKTDLEN_CHANINFO; + + cbPKT_CHANINFO* chan_pkt = reinterpret_cast(&pkt); + chan_pkt->chan = 50; + chan_pkt->proc = 1; + chan_pkt->bank = 2; + std::strncpy(chan_pkt->label, "elec050", cbLEN_STR_LABEL); + + // storePacket writes to receive buffer (config parsing at device layer) + ASSERT_TRUE(session.storePacket(pkt).isOk()); + + uint32_t received = 0, available = 0; + ASSERT_TRUE(session.getReceiveBufferStats(received, available).isOk()); + EXPECT_EQ(received, 1u); +} + +TEST_F(NativeShmemSessionTest, NspStatus) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + auto id = InstrumentId::fromOneBased(1); + + // Set NSP status + ASSERT_TRUE(session.setNspStatus(id, NSPStatus::NSP_FOUND).isOk()); + + auto get_result = session.getNspStatus(id); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value(), NSPStatus::NSP_FOUND); + + // Instrument 2 should fail + auto id2 = InstrumentId::fromOneBased(2); + EXPECT_TRUE(session.setNspStatus(id2, NSPStatus::NSP_FOUND).isError()); +} + +TEST_F(NativeShmemSessionTest, GeminiSystem) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Default should be false + auto get_result = session.isGeminiSystem(); + ASSERT_TRUE(get_result.isOk()); + EXPECT_FALSE(get_result.value()); + + // Set to true + ASSERT_TRUE(session.setGeminiSystem(true).isOk()); + + get_result = session.isGeminiSystem(); + ASSERT_TRUE(get_result.isOk()); + EXPECT_TRUE(get_result.value()); +} + +TEST_F(NativeShmemSessionTest, TransmitQueueRoundTrip) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Enqueue a packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.type = 0x01; + pkt.cbpkt_header.dlen = 4; + pkt.data_u32[0] = 0xDEADBEEF; + + ASSERT_FALSE(session.hasTransmitPackets()); + ASSERT_TRUE(session.enqueuePacket(pkt).isOk()); + EXPECT_TRUE(session.hasTransmitPackets()); + + // Dequeue and verify + cbPKT_GENERIC out_pkt; + auto deq_result = session.dequeuePacket(out_pkt); + ASSERT_TRUE(deq_result.isOk()); + EXPECT_TRUE(deq_result.value()); + EXPECT_EQ(out_pkt.cbpkt_header.type, 0x01); + EXPECT_EQ(out_pkt.data_u32[0], 0xDEADBEEF); + + EXPECT_FALSE(session.hasTransmitPackets()); +} + +TEST_F(NativeShmemSessionTest, LocalTransmitQueueRoundTrip) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.type = 0x42; + pkt.cbpkt_header.dlen = 2; + pkt.data_u32[0] = 0xCAFEBABE; + + ASSERT_FALSE(session.hasLocalTransmitPackets()); + ASSERT_TRUE(session.enqueueLocalPacket(pkt).isOk()); + EXPECT_TRUE(session.hasLocalTransmitPackets()); + + cbPKT_GENERIC out_pkt; + auto deq_result = session.dequeueLocalPacket(out_pkt); + ASSERT_TRUE(deq_result.isOk()); + EXPECT_TRUE(deq_result.value()); + EXPECT_EQ(out_pkt.cbpkt_header.type, 0x42); + EXPECT_EQ(out_pkt.data_u32[0], 0xCAFEBABE); + + EXPECT_FALSE(session.hasLocalTransmitPackets()); +} + +TEST_F(NativeShmemSessionTest, ReceiveBufferStoreAndStats) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Initially empty + uint32_t received = 0, available = 0; + ASSERT_TRUE(session.getReceiveBufferStats(received, available).isOk()); + EXPECT_EQ(received, 0u); + + // Store a few packets + constexpr int NUM_PKTS = 5; + for (int i = 0; i < NUM_PKTS; i++) { + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.type = static_cast(i + 1); + pkt.cbpkt_header.dlen = 4; + pkt.data_u32[0] = 100 + i; + + ASSERT_TRUE(session.storePacket(pkt).isOk()); + } + + // Check stats - received count should match number of packets stored + ASSERT_TRUE(session.getReceiveBufferStats(received, available).isOk()); + EXPECT_EQ(received, static_cast(NUM_PKTS)); + // available is in word-based ring buffer units, should be > 0 + EXPECT_GT(available, 0u); +} + +TEST_F(NativeShmemSessionTest, NumTotalChans) { + auto result = createNativeSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + auto chans_result = session.getNumTotalChans(); + ASSERT_TRUE(chans_result.isOk()); + // Native mode init sets total channels to NATIVE_MAXCHANS (284) + EXPECT_EQ(chans_result.value(), cbshm::NATIVE_MAXCHANS); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name CENTRAL_COMPAT ShmemSession Tests +/// @{ + +class CentralCompatShmemSessionTest : public ::testing::Test { +protected: + void SetUp() override { + test_name = "test_compat_" + std::to_string(test_counter++); + } + + // Helper to create a CENTRAL_COMPAT STANDALONE session (for testing) + Result createCompatSession() { + return ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::CENTRAL_COMPAT); + } + + std::string test_name; + static int test_counter; +}; + +int CentralCompatShmemSessionTest::test_counter = 0; + +TEST_F(CentralCompatShmemSessionTest, CreateCompatStandalone) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << "Failed to create compat session: " << result.error(); + + auto& session = result.value(); + EXPECT_TRUE(session.isOpen()); + EXPECT_EQ(session.getMode(), Mode::STANDALONE); + EXPECT_EQ(session.getLayout(), ShmemLayout::CENTRAL_COMPAT); +} + +TEST_F(CentralCompatShmemSessionTest, LegacyConfigBufferAccessor) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // CENTRAL_COMPAT should return legacy config buffer + EXPECT_NE(session.getLegacyConfigBuffer(), nullptr); + // Central accessor should return nullptr for compat layout + EXPECT_EQ(session.getConfigBuffer(), nullptr); + // Native accessor should also return nullptr + EXPECT_EQ(session.getNativeConfigBuffer(), nullptr); +} + +TEST_F(CentralCompatShmemSessionTest, IsInstrumentActive_AlwaysTrue) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // In CENTRAL_COMPAT mode, all instruments report as active + for (uint8_t i = 1; i <= cbMAXOPEN; ++i) { + auto id = InstrumentId::fromOneBased(i); + auto active_result = session.isInstrumentActive(id); + ASSERT_TRUE(active_result.isOk()); + EXPECT_TRUE(active_result.value()) << "Instrument " << (int)i << " should be active in compat mode"; + } +} + +TEST_F(CentralCompatShmemSessionTest, SetInstrumentActive_ReturnsError) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Setting instrument status should fail (read-only in compat mode) + auto id = InstrumentId::fromOneBased(1); + auto set_result = session.setInstrumentActive(id, true); + EXPECT_TRUE(set_result.isError()); +} + +TEST_F(CentralCompatShmemSessionTest, GetFirstActiveInstrument) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Should always return instrument 0 (first) + auto first_result = session.getFirstActiveInstrument(); + ASSERT_TRUE(first_result.isOk()); + EXPECT_EQ(first_result.value().toIndex(), 0); +} + +TEST_F(CentralCompatShmemSessionTest, SetAndGetProcInfo) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_PROCINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 2; + info.chancount = 512; + info.bankcount = 24; + + // Set procinfo for instrument 2 (index 1) + auto id = InstrumentId::fromOneBased(2); + auto set_result = session.setProcInfo(id, info); + ASSERT_TRUE(set_result.isOk()); + + // Retrieve and verify + auto get_result = session.getProcInfo(id); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 2); + EXPECT_EQ(get_result.value().chancount, 512); + EXPECT_EQ(get_result.value().bankcount, 24); +} + +TEST_F(CentralCompatShmemSessionTest, SetAndGetBankInfo) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_BANKINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 1; + info.bank = 5; + info.chancount = 32; + + auto id = InstrumentId::fromOneBased(1); + ASSERT_TRUE(session.setBankInfo(id, 5, info).isOk()); + + auto get_result = session.getBankInfo(id, 5); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().proc, 1); + EXPECT_EQ(get_result.value().bank, 5); + EXPECT_EQ(get_result.value().chancount, 32); +} + +TEST_F(CentralCompatShmemSessionTest, SetAndGetFilterInfo) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_FILTINFO info; + std::memset(&info, 0, sizeof(info)); + info.proc = 1; + info.filt = 10; + info.hpfreq = 300000; + + auto id = InstrumentId::fromOneBased(1); + ASSERT_TRUE(session.setFilterInfo(id, 10, info).isOk()); + + auto get_result = session.getFilterInfo(id, 10); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().filt, 10); + EXPECT_EQ(get_result.value().hpfreq, 300000); +} + +TEST_F(CentralCompatShmemSessionTest, SetAndGetChanInfo) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_CHANINFO info; + std::memset(&info, 0, sizeof(info)); + info.chan = 100; + info.proc = 1; + info.bank = 4; + std::strncpy(info.label, "elec100", cbLEN_STR_LABEL); + + ASSERT_TRUE(session.setChanInfo(100, info).isOk()); + + auto get_result = session.getChanInfo(100); + ASSERT_TRUE(get_result.isOk()); + EXPECT_EQ(get_result.value().chan, 100); + EXPECT_STREQ(get_result.value().label, "elec100"); +} + +TEST_F(CentralCompatShmemSessionTest, InstrumentFilter_DefaultNoFilter) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + EXPECT_EQ(session.getInstrumentFilter(), -1); +} + +TEST_F(CentralCompatShmemSessionTest, InstrumentFilter_SetAndGet) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + session.setInstrumentFilter(2); + EXPECT_EQ(session.getInstrumentFilter(), 2); + + session.setInstrumentFilter(-1); + EXPECT_EQ(session.getInstrumentFilter(), -1); +} + +TEST_F(CentralCompatShmemSessionTest, InstrumentFilter_FiltersReceiveBuffer) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // STANDALONE compat session initializes procinfo version to current, + // so readReceiveBuffer correctly parses current-format packets written by storePacket. + ASSERT_EQ(session.getCompatProtocolVersion(), CBPROTO_PROTOCOL_CURRENT); + + // Store packets from different instruments with realistic timestamps + uint32_t dlen = 4; + + for (uint8_t inst = 0; inst < 4; ++inst) { + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.time = 1000000000ULL + inst * 33333ULL; // Realistic nanosecond timestamps + pkt.cbpkt_header.chid = 1; // Non-configuration packet + pkt.cbpkt_header.instrument = inst; + pkt.cbpkt_header.type = 0x01; + pkt.cbpkt_header.dlen = dlen; + pkt.data_u32[0] = 0xAA00 + inst; + + ASSERT_TRUE(session.storePacket(pkt).isOk()); + } + + // Set filter to instrument 2 only + session.setInstrumentFilter(2); + + // Read packets - should only get the one from instrument 2 + cbPKT_GENERIC read_pkts[10]; + size_t packets_read = 0; + auto read_result = session.readReceiveBuffer(read_pkts, 10, packets_read); + ASSERT_TRUE(read_result.isOk()) << read_result.error(); + EXPECT_EQ(packets_read, 1u); + EXPECT_EQ(read_pkts[0].cbpkt_header.instrument, 2); + EXPECT_EQ(read_pkts[0].data_u32[0], 0xAA02u); +} + +TEST_F(CentralCompatShmemSessionTest, InstrumentFilter_NoFilter_ReturnsAll) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // Store packets from different instruments with realistic timestamps + uint32_t dlen = 4; + + for (uint8_t inst = 0; inst < 3; ++inst) { + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.time = 2000000000ULL + inst * 33333ULL; // Realistic nanosecond timestamps + pkt.cbpkt_header.chid = 1; + pkt.cbpkt_header.instrument = inst; + pkt.cbpkt_header.type = 0x01; + pkt.cbpkt_header.dlen = dlen; + pkt.data_u32[0] = 0xBB00 + inst; + + ASSERT_TRUE(session.storePacket(pkt).isOk()); + } + + // No filter set (default -1) - should get all packets + cbPKT_GENERIC read_pkts[10]; + size_t packets_read = 0; + auto read_result = session.readReceiveBuffer(read_pkts, 10, packets_read); + ASSERT_TRUE(read_result.isOk()) << read_result.error(); + EXPECT_EQ(packets_read, 3u); +} + +TEST_F(CentralCompatShmemSessionTest, TransmitQueueRoundTrip) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.type = 0x05; + pkt.cbpkt_header.dlen = 4; + pkt.data_u32[0] = 0x12345678; + + ASSERT_FALSE(session.hasTransmitPackets()); + ASSERT_TRUE(session.enqueuePacket(pkt).isOk()); + EXPECT_TRUE(session.hasTransmitPackets()); + + cbPKT_GENERIC out_pkt; + auto deq_result = session.dequeuePacket(out_pkt); + ASSERT_TRUE(deq_result.isOk()); + EXPECT_TRUE(deq_result.value()); + EXPECT_EQ(out_pkt.cbpkt_header.type, 0x05); + EXPECT_EQ(out_pkt.data_u32[0], 0x12345678u); + + EXPECT_FALSE(session.hasTransmitPackets()); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Central Compat Protocol Translation Tests +/// @{ + +class CentralCompatProtocolTest : public ::testing::Test { +protected: + void SetUp() override { + test_name = "test_proto_" + std::to_string(test_counter++); + } + + Result createCompatSession() { + return ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::CENTRAL_COMPAT); + } + + std::string test_name; + static int test_counter; +}; + +int CentralCompatProtocolTest::test_counter = 0; + +/// @brief Test protocol version detection: STANDALONE initializes to current +TEST_F(CentralCompatProtocolTest, DetectProtocol_StandaloneDefault) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + // STANDALONE compat session initializes procinfo version to current (4.2) + EXPECT_EQ(session.getCompatProtocolVersion(), CBPROTO_PROTOCOL_CURRENT); +} + +/// @brief Test that readReceiveBuffer correctly parses current-format packets +TEST_F(CentralCompatProtocolTest, ReadCurrentFormat_WithRealisticTimestamps) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + EXPECT_EQ(session.getCompatProtocolVersion(), CBPROTO_PROTOCOL_CURRENT); + + // Store packets (current format, written by storePacket) + for (uint8_t i = 0; i < 3; ++i) { + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.time = 5000000000ULL + i * 33333ULL; + pkt.cbpkt_header.chid = 1; + pkt.cbpkt_header.instrument = i; + pkt.cbpkt_header.type = 0x01; + pkt.cbpkt_header.dlen = 4; + pkt.data_u32[0] = 0xCC00 + i; + + ASSERT_TRUE(session.storePacket(pkt).isOk()); + } + + // Read packets (current-format parsing, no translation) + cbPKT_GENERIC read_pkts[10]; + size_t packets_read = 0; + auto read_result = session.readReceiveBuffer(read_pkts, 10, packets_read); + ASSERT_TRUE(read_result.isOk()) << read_result.error(); + EXPECT_EQ(packets_read, 3u); + + for (size_t i = 0; i < packets_read; ++i) { + EXPECT_EQ(read_pkts[i].cbpkt_header.instrument, i); + EXPECT_EQ(read_pkts[i].data_u32[0], 0xCC00u + i); + EXPECT_EQ(read_pkts[i].cbpkt_header.dlen, 4u); + } +} + +/// @brief Test version detection: 3.11 +TEST_F(CentralCompatProtocolTest, DetectProtocol_311) { + auto writer_result = createCompatSession(); + ASSERT_TRUE(writer_result.isOk()) << writer_result.error(); + auto& writer = writer_result.value(); + + // Set version to 3.11 (major=3, minor=11) + auto* cfg = writer.getLegacyConfigBuffer(); + ASSERT_NE(cfg, nullptr); + cfg->procinfo[0].version = (3 << 16) | 11; + + std::string name = test_name; + auto reader_result = ShmemSession::create( + name + "_cfg", name + "_rec", name + "_xmt", + name + "_xmt_local", name + "_status", name + "_spk", + name + "_signal", Mode::CLIENT, ShmemLayout::CENTRAL_COMPAT); + ASSERT_TRUE(reader_result.isOk()) << reader_result.error(); + EXPECT_EQ(reader_result.value().getCompatProtocolVersion(), CBPROTO_PROTOCOL_311); +} + +/// @brief Test version detection: 4.0 +TEST_F(CentralCompatProtocolTest, DetectProtocol_400) { + // Create standalone, set version, then open CLIENT to pick it up + auto writer_result = createCompatSession(); + ASSERT_TRUE(writer_result.isOk()) << writer_result.error(); + auto& writer = writer_result.value(); + + auto* cfg = writer.getLegacyConfigBuffer(); + ASSERT_NE(cfg, nullptr); + cfg->procinfo[0].version = (4 << 16) | 0; // major=4, minor=0 + + std::string name = test_name; + auto reader_result = ShmemSession::create( + name + "_cfg", name + "_rec", name + "_xmt", + name + "_xmt_local", name + "_status", name + "_spk", + name + "_signal", Mode::CLIENT, ShmemLayout::CENTRAL_COMPAT); + ASSERT_TRUE(reader_result.isOk()) << reader_result.error(); + EXPECT_EQ(reader_result.value().getCompatProtocolVersion(), CBPROTO_PROTOCOL_400); +} + +/// @brief Test version detection: 4.1 +TEST_F(CentralCompatProtocolTest, DetectProtocol_410) { + auto writer_result = createCompatSession(); + ASSERT_TRUE(writer_result.isOk()) << writer_result.error(); + auto& writer = writer_result.value(); + + auto* cfg = writer.getLegacyConfigBuffer(); + ASSERT_NE(cfg, nullptr); + cfg->procinfo[0].version = (4 << 16) | 1; // major=4, minor=1 + + std::string name = test_name; + auto reader_result = ShmemSession::create( + name + "_cfg", name + "_rec", name + "_xmt", + name + "_xmt_local", name + "_status", name + "_spk", + name + "_signal", Mode::CLIENT, ShmemLayout::CENTRAL_COMPAT); + ASSERT_TRUE(reader_result.isOk()) << reader_result.error(); + EXPECT_EQ(reader_result.value().getCompatProtocolVersion(), CBPROTO_PROTOCOL_410); +} + +/// @brief Test version detection: 4.2 (current) +TEST_F(CentralCompatProtocolTest, DetectProtocol_Current_42) { + auto writer_result = createCompatSession(); + ASSERT_TRUE(writer_result.isOk()) << writer_result.error(); + auto& writer = writer_result.value(); + + auto* cfg = writer.getLegacyConfigBuffer(); + ASSERT_NE(cfg, nullptr); + cfg->procinfo[0].version = (4 << 16) | 2; // major=4, minor=2 + + std::string name = test_name; + auto reader_result = ShmemSession::create( + name + "_cfg", name + "_rec", name + "_xmt", + name + "_xmt_local", name + "_status", name + "_spk", + name + "_signal", Mode::CLIENT, ShmemLayout::CENTRAL_COMPAT); + ASSERT_TRUE(reader_result.isOk()) << reader_result.error(); + EXPECT_EQ(reader_result.value().getCompatProtocolVersion(), CBPROTO_PROTOCOL_CURRENT); +} + +/// @brief Test readReceiveBuffer with instrument filter and current-format packets +TEST_F(CentralCompatProtocolTest, InstrumentFilterWithCurrentProtocol) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + EXPECT_EQ(session.getCompatProtocolVersion(), CBPROTO_PROTOCOL_CURRENT); + + // Store packets from 4 different instruments + for (uint8_t inst = 0; inst < 4; ++inst) { + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.time = 3000000000ULL + inst * 33333ULL; + pkt.cbpkt_header.chid = 1; + pkt.cbpkt_header.instrument = inst; + pkt.cbpkt_header.type = 0x01; + pkt.cbpkt_header.dlen = 4; + pkt.data_u32[0] = 0xDD00 + inst; + + ASSERT_TRUE(session.storePacket(pkt).isOk()); + } + + // Filter for instrument 3 only + session.setInstrumentFilter(3); + + cbPKT_GENERIC read_pkts[10]; + size_t packets_read = 0; + auto read_result = session.readReceiveBuffer(read_pkts, 10, packets_read); + ASSERT_TRUE(read_result.isOk()) << read_result.error(); + EXPECT_EQ(packets_read, 1u); + EXPECT_EQ(read_pkts[0].cbpkt_header.instrument, 3); + EXPECT_EQ(read_pkts[0].data_u32[0], 0xDD03u); +} + +/// @brief Test transmit round-trip with current protocol (no translation) +TEST_F(CentralCompatProtocolTest, TransmitRoundTrip_CurrentProtocol) { + auto result = createCompatSession(); + ASSERT_TRUE(result.isOk()) << result.error(); + auto& session = result.value(); + + EXPECT_EQ(session.getCompatProtocolVersion(), CBPROTO_PROTOCOL_CURRENT); + + // Enqueue a packet + cbPKT_GENERIC pkt; + std::memset(&pkt, 0, sizeof(pkt)); + pkt.cbpkt_header.type = 0x10; + pkt.cbpkt_header.dlen = 4; + pkt.cbpkt_header.instrument = 1; + pkt.data_u32[0] = 0xFEEDFACE; + + ASSERT_TRUE(session.enqueuePacket(pkt).isOk()); + EXPECT_TRUE(session.hasTransmitPackets()); + + // Dequeue and verify (no translation, so data should match exactly) + cbPKT_GENERIC out_pkt; + auto deq_result = session.dequeuePacket(out_pkt); + ASSERT_TRUE(deq_result.isOk()); + EXPECT_TRUE(deq_result.value()); + EXPECT_EQ(out_pkt.cbpkt_header.type, 0x10); + EXPECT_EQ(out_pkt.cbpkt_header.dlen, 4u); + EXPECT_EQ(out_pkt.cbpkt_header.instrument, 1); + EXPECT_EQ(out_pkt.data_u32[0], 0xFEEDFACEu); + + EXPECT_FALSE(session.hasTransmitPackets()); +} + +/// @brief Non-compat layout always detects CURRENT protocol +TEST_F(CentralCompatProtocolTest, NativeLayout_AlwaysCurrent) { + std::string name = test_name; + auto result = ShmemSession::create( + name + "_cfg", name + "_rec", name + "_xmt", + name + "_xmt_local", name + "_status", name + "_spk", + name + "_signal", Mode::STANDALONE, ShmemLayout::NATIVE); + ASSERT_TRUE(result.isOk()) << result.error(); + EXPECT_EQ(result.value().getCompatProtocolVersion(), CBPROTO_PROTOCOL_CURRENT); +} + +/// @brief CENTRAL layout always detects CURRENT protocol +TEST_F(CentralCompatProtocolTest, CentralLayout_AlwaysCurrent) { + std::string name = test_name; + auto result = ShmemSession::create( + name + "_cfg", name + "_rec", name + "_xmt", + name + "_xmt_local", name + "_status", name + "_spk", + name + "_signal", Mode::STANDALONE, ShmemLayout::CENTRAL); + ASSERT_TRUE(result.isOk()) << result.error(); + EXPECT_EQ(result.value().getCompatProtocolVersion(), CBPROTO_PROTOCOL_CURRENT); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @name Owner Liveness Tests +/// @{ + +class OwnerLivenessTest : public ::testing::Test { +protected: + void SetUp() override { + test_name = "test_liveness_" + std::to_string(test_counter++); + } + + std::string test_name; + static int test_counter; +}; + +int OwnerLivenessTest::test_counter = 0; + +TEST_F(OwnerLivenessTest, StandaloneWritesOwnerPid) { + auto result = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::NATIVE); + ASSERT_TRUE(result.isOk()) << result.error(); + + auto* cfg = result.value().getNativeConfigBuffer(); + ASSERT_NE(cfg, nullptr); +#ifdef _WIN32 + EXPECT_EQ(cfg->owner_pid, GetCurrentProcessId()); +#else + EXPECT_EQ(cfg->owner_pid, static_cast(getpid())); +#endif +} + +TEST_F(OwnerLivenessTest, StandaloneAlwaysReturnsTrue) { + auto result = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::NATIVE); + ASSERT_TRUE(result.isOk()) << result.error(); + + // isOwnerAlive() is only meaningful for CLIENT — STANDALONE always returns true + EXPECT_TRUE(result.value().isOwnerAlive()); +} + +TEST_F(OwnerLivenessTest, ClientDetectsLiveOwner) { + // Create STANDALONE (sets owner_pid to current process) + auto standalone = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::NATIVE); + ASSERT_TRUE(standalone.isOk()) << standalone.error(); + + // Create CLIENT on same segments + auto client = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::CLIENT, ShmemLayout::NATIVE); + ASSERT_TRUE(client.isOk()) << client.error(); + + // Owner (this process) is alive + EXPECT_TRUE(client.value().isOwnerAlive()); +} + +TEST_F(OwnerLivenessTest, ClientDetectsDeadOwner) { + // Create STANDALONE, then set a fake dead PID + auto standalone = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::NATIVE); + ASSERT_TRUE(standalone.isOk()) << standalone.error(); + + // Overwrite owner_pid with a PID that (almost certainly) doesn't exist + auto* cfg = standalone.value().getNativeConfigBuffer(); + ASSERT_NE(cfg, nullptr); + cfg->owner_pid = 4000000000u; // Well above any real PID + + // Create CLIENT on same segments + auto client = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::CLIENT, ShmemLayout::NATIVE); + ASSERT_TRUE(client.isOk()) << client.error(); + + // Owner PID doesn't exist — should detect as dead + EXPECT_FALSE(client.value().isOwnerAlive()); +} + +TEST_F(OwnerLivenessTest, ClientTreatsZeroPidAsAlive) { + // Create STANDALONE, then clear owner_pid to simulate pre-liveness segments + auto standalone = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::NATIVE); + ASSERT_TRUE(standalone.isOk()) << standalone.error(); + + auto* cfg = standalone.value().getNativeConfigBuffer(); + ASSERT_NE(cfg, nullptr); + cfg->owner_pid = 0; + + // Create CLIENT on same segments + auto client = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::CLIENT, ShmemLayout::NATIVE); + ASSERT_TRUE(client.isOk()) << client.error(); + + // PID 0 = unknown — assume alive (backward compat) + EXPECT_TRUE(client.value().isOwnerAlive()); +} + +TEST_F(OwnerLivenessTest, CentralCompatAlwaysReturnsTrue) { + // Liveness check only applies to NATIVE layout + auto standalone = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::STANDALONE, ShmemLayout::CENTRAL); + ASSERT_TRUE(standalone.isOk()) << standalone.error(); + + auto client = ShmemSession::create( + test_name + "_cfg", test_name + "_rec", test_name + "_xmt", + test_name + "_xmt_local", test_name + "_status", test_name + "_spk", + test_name + "_signal", Mode::CLIENT, ShmemLayout::CENTRAL); + ASSERT_TRUE(client.isOk()) << client.error(); + + // Non-NATIVE layout always returns true (no PID field) + EXPECT_TRUE(client.value().isOwnerAlive()); +} + +/// @} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @brief Run all tests +/// +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt deleted file mode 100644 index d8b3d96a..00000000 --- a/tools/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -if(${CBSDK_BUILD_TOOLS}) - add_subdirectory(n2h5) - add_subdirectory(loop_tester) -endif(${CBSDK_BUILD_TOOLS}) diff --git a/tools/loop_tester/CMakeLists.txt b/tools/loop_tester/CMakeLists.txt deleted file mode 100644 index b2a3b150..00000000 --- a/tools/loop_tester/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ -cmake_minimum_required(VERSION 3.15) - -# Fetch LSL library from the cboulay/apple_framework branch -# This branch has fixes for macOS framework support -include(FetchContent) -FetchContent_Declare( - lsl - GIT_REPOSITORY https://github.com/sccn/liblsl.git - GIT_TAG cboulay/apple_framework - GIT_SHALLOW TRUE - EXCLUDE_FROM_ALL -) -set(LSL_BUILD_STATIC ON CACHE BOOL "Build static library") -set(LSL_FRAMEWORK OFF CACHE BOOL "Don't build as framework") -FetchContent_MakeAvailable(lsl) - -# First application -- LSL sawtooth stream source -# Generates a 30 kHz sawtooth signal in first 2 channels and constant values in remaining channels -add_executable(sawtooth_lsl sawtooth_lsl.cpp) -target_link_libraries(sawtooth_lsl PRIVATE lsl) -target_compile_features(sawtooth_lsl PRIVATE cxx_std_17) - -# Second application -- cbsdk sawtooth validator (trial buffer version) -# Connects to nPlayServer and validates the received sawtooth signal -add_executable(sawtooth_cbsdk sawtooth_cbsdk.cpp) -target_link_libraries(sawtooth_cbsdk PRIVATE cbsdk_static) -target_compile_features(sawtooth_cbsdk PRIVATE cxx_std_17) - -# Third application -- cbsdk sawtooth validator (callback version) -# Uses cbSdkRegisterCallback to bypass trial buffer system -add_executable(sawtooth_cbsdk_callback sawtooth_cbsdk_callback.cpp) -target_link_libraries(sawtooth_cbsdk_callback PRIVATE cbsdk_static) -target_compile_features(sawtooth_cbsdk_callback PRIVATE cxx_std_17) - -# Fourth application -- cbsdk spike pattern validator -# Validates spike event timing from spikes_lsl.py test pattern -add_executable(spikes_cbsdk spikes_cbsdk.cpp) -target_link_libraries(spikes_cbsdk PRIVATE cbsdk_static) -target_compile_features(spikes_cbsdk PRIVATE cxx_std_17) - -# Install all executables -install(TARGETS sawtooth_lsl sawtooth_cbsdk sawtooth_cbsdk_callback spikes_cbsdk - RUNTIME DESTINATION bin -) \ No newline at end of file diff --git a/tools/loop_tester/README.md b/tools/loop_tester/README.md deleted file mode 100644 index 35451beb..00000000 --- a/tools/loop_tester/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Loop Tester - -This tool comprises 2 executables. The first is an LSL stream source that generates a 30 kHz sawtooth signal in the first 2 channels and constant values in the remaining 128 channels, then streams it with specific identifiers. The second executable is a cbsdk client that connects to nPlayServer, receives the data, and checks that the data matches what is expected. It also checks that the timestamps are reasonable and that no packets are lost. - -For this to work, you must have a special build of nPlayServer that includes LSL support (built with -DUSE_LSL=ON). Please contact Blackrock support to obtain this version of nPlayServer. - -1. Run lsl_sawtooth (the LSL stream source). -2. Run nPlayServer with the appropriate command line arguments to enable LSL support. For example, to emulate a Gemini Hub: - `nPlayServer --device=LSL ` -3. Run cbsdk_sawtooth (the cbsdk client). diff --git a/tools/loop_tester/sawtooth_cbsdk.cpp b/tools/loop_tester/sawtooth_cbsdk.cpp deleted file mode 100644 index 22e97ffc..00000000 --- a/tools/loop_tester/sawtooth_cbsdk.cpp +++ /dev/null @@ -1,340 +0,0 @@ -/** - * cbsdk Sawtooth Validator - * - * Connects to nPlayServer (configured with LSL support) and validates: - * - Sawtooth signal on channels 1-2 - * - Constant values on channels 3-130 - * - Timestamp continuity - * - No packet loss - * - * This is used to test long-running cbsdk connections. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Configuration constants -constexpr uint32_t INST = 0; -constexpr int NUM_CHANNELS = 128; -constexpr uint32_t SAMPLE_GROUP = 6; // Sample group to monitor -constexpr int16_t SAWTOOTH_MIN = -32768; -constexpr int16_t SAWTOOTH_MAX = 32767; -constexpr int32_t UNIQUE_VALUES = SAWTOOTH_MAX - SAWTOOTH_MIN + 1; // 65536 -constexpr int SAWTOOTH_TOLERANCE = 1; // Allow deviations -constexpr bool LEGACY_TIMESTAMPS = true; // We know the timestamps increment 1-by-1. - -// Global flag for graceful shutdown -std::atomic keep_running{true}; - -void signal_handler(int signal) { - std::cout << "\nReceived signal " << signal << ", shutting down gracefully..." << std::endl; - keep_running = false; -} - -void handleResult(cbSdkResult res, const char* operation) { - if (res != CBSDKRESULT_SUCCESS) { - std::cerr << operation << " failed with code " << res << std::endl; - exit(1); - } -} - -int main(int argc, char* argv[]) { - // Set up signal handlers - std::signal(SIGINT, signal_handler); - std::signal(SIGTERM, signal_handler); - - // Parse command line arguments - std::string inst_ip = ""; - int inst_port = cbNET_UDP_PORT_CNT; - - if (argc > 1) inst_ip = argv[1]; - if (argc > 2) inst_port = std::stoi(argv[2]); - - std::cout << "cbsdk Sawtooth Validator" << std::endl; - std::cout << "Connecting to: " << (inst_ip.empty() ? "default" : inst_ip) << ":" << inst_port << std::endl; - std::cout << std::endl; - - // Open connection - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - cbSdkConnection con{}; - con.szOutIP = inst_ip.empty() ? nullptr : inst_ip.c_str(); - con.nOutPort = inst_port; - con.szInIP = ""; - con.nInPort = inst_port; - - cbSdkResult res = cbSdkOpen(INST, conType, con); - handleResult(res, "cbSdkOpen"); - - std::cout << "Connection established!" << std::endl; - std::this_thread::sleep_for(std::chrono::seconds(1)); - - // Configure channels: enable continuous sampling on SAMPLE_GROUP and disable spike processing - std::cout << "Configuring channels for continuous sampling on group " << SAMPLE_GROUP << "..." << std::endl; - for (int chan_ix = 0; chan_ix < cbNUM_ANALOG_CHANS; ++chan_ix) { - cbPKT_CHANINFO chanInfo; - - // Get current channel configuration - res = cbSdkGetChannelConfig(INST, chan_ix + 1, &chanInfo); - if (res != CBSDKRESULT_SUCCESS) { - std::cerr << "Warning: cbSdkGetChannelConfig failed for channel " << (chan_ix + 1) << std::endl; - continue; - } - - // Disable DC offset correction by clearing the flag - chanInfo.ainpopts &= ~cbAINP_OFFSET_CORRECT; - - // Enable continuous sampling on SAMPLE_GROUP for first NUM_CHANNELS channels - chanInfo.smpgroup = (chan_ix < NUM_CHANNELS) ? SAMPLE_GROUP : 0; - - // Disable spike processing - chanInfo.spkopts = cbAINPSPK_NOSORT; - - // Apply the modified configuration - res = cbSdkSetChannelConfig(INST, chan_ix + 1, &chanInfo); - if (res != CBSDKRESULT_SUCCESS) { - std::cerr << "Warning: cbSdkSetChannelConfig failed for channel " << (chan_ix + 1) << std::endl; - } - } - std::cout << "Channel configuration complete." << std::endl; - std::this_thread::sleep_for(std::chrono::seconds(1)); - - // Configure trial (enable continuous data) - // Use 10x buffer size to avoid overflows - constexpr uint32_t BUFFER_SIZE = cbSdk_CONTINUOUS_DATA_SAMPLES * 10; - res = cbSdkSetTrialConfig( - INST, - 1, // bActive - 0, 0, 0, // begin trigger - 0, 0, 0, // end trigger - 0, // waveforms - BUFFER_SIZE, // continuous - using larger buffer - 0, // events - 0, // comments - 0 // tracking - ); - handleResult(res, "cbSdkSetTrialConfig"); - - std::cout << "Trial configured for continuous data" << std::endl; - - // Allocate trial data structure - cbSdkTrialCont trialCont{}; - trialCont.group = SAMPLE_GROUP; - trialCont.num_samples = cbSdk_CONTINUOUS_DATA_SAMPLES; - - // Wait for channels to become available (may take a moment for backend to start sampling) - std::cout << "Waiting for channels in sample group " << SAMPLE_GROUP << "..." << std::endl; - const auto wait_start = std::chrono::steady_clock::now(); - constexpr auto wait_timeout = std::chrono::seconds(5); // 5 second timeout - - while (trialCont.count == 0) { - res = cbSdkInitTrialData(INST, 0, nullptr, &trialCont, nullptr, nullptr); - handleResult(res, "cbSdkInitTrialData"); - - if (trialCont.count > 0) { - break; // Channels found! - } - - // Check timeout - const auto elapsed = std::chrono::steady_clock::now() - wait_start; - if (elapsed >= wait_timeout) { - std::cerr << "Error: Timeout waiting for channels in sample group " << SAMPLE_GROUP << std::endl; - std::cerr << "No channels became available after " - << std::chrono::duration_cast(wait_timeout).count() - << " seconds" << std::endl; - cbSdkClose(INST); - return 1; - } - - // Small delay before retrying - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - std::cout << "Sample group " << SAMPLE_GROUP << " has " << trialCont.count << " channels" << std::endl; - std::cout << "Expected sample rate: " << trialCont.sample_rate << " Hz" << std::endl; - - // Pre-allocate buffers once (maximum possible size matching our BUFFER_SIZE) - const size_t max_buffer_size = BUFFER_SIZE * trialCont.count; - std::vector samples(max_buffer_size); - std::vector timestamps(BUFFER_SIZE); - - // Set buffer pointers (they won't change) - trialCont.samples = samples.data(); - trialCont.timestamps = timestamps.data(); - - // Validation state - int64_t total_samples_received = 0; - int64_t sawtooth_errors = 0; - int64_t constant_errors = 0; - int64_t timestamp_errors = 0; - int64_t read_count = 0; - PROCTIME last_timestamp = 0; - int16_t last_sample_ch0 = 0; - int16_t last_sample_ch1 = 0; - bool first_read = true; - - // For sawtooth phase tracking using timestamps - // offset = timestamp - phase, so phase = (timestamp - offset) % UNIQUE_VALUES - int64_t timestamp_offset = -1; - - auto start_time = std::chrono::steady_clock::now(); - auto last_stats_time = start_time; - constexpr auto max_runtime = std::chrono::seconds(20); // 20 second timeout - - std::cout << "\nStarting validation loop (will run for 20 seconds)..." << std::endl; - std::cout << "Press Ctrl+C to stop early." << std::endl; - std::cout << std::endl; - - while (keep_running) { - // Check if we've exceeded the runtime limit - auto elapsed = std::chrono::steady_clock::now() - start_time; - if (elapsed >= max_runtime) { - std::cout << "\nReached 20 second timeout, exiting..." << std::endl; - break; - } - // Initialize trial to get available sample count - trialCont.num_samples = BUFFER_SIZE; - res = cbSdkInitTrialData(INST, 0, nullptr, &trialCont, nullptr, nullptr); - if (res != CBSDKRESULT_SUCCESS) { - std::cerr << "cbSdkInitTrialData failed: " << res << std::endl; - break; - } - - if (trialCont.num_samples > 0) { - // Get trial data (bActive = 1 to advance read pointer) - res = cbSdkGetTrialData(INST, 1, nullptr, &trialCont, nullptr, nullptr); - if (res != CBSDKRESULT_SUCCESS) { - std::cerr << "cbSdkGetTrialData failed: " << res << std::endl; - break; - } - - read_count++; - // Log first few reads for debugging - if (read_count <= 3) { - std::cout << "Read #" << read_count << ": Got " << trialCont.num_samples - << " samples, first_ts=" << timestamps[0]; - if (trialCont.num_samples > 1) { - std::cout << ", last_ts=" << timestamps[trialCont.num_samples - 1]; - } - std::cout << std::endl; - } - - // Validate data - for (uint32_t samp = 0; samp < trialCont.num_samples; samp++) { - const PROCTIME current_timestamp = timestamps[samp]; - - // Check timestamp continuity - if (!first_read) { - // Timestamps should increase monotonically - if ((LEGACY_TIMESTAMPS && current_timestamp != (last_timestamp + 1)) || - (!LEGACY_TIMESTAMPS && current_timestamp < last_timestamp)) { - timestamp_errors++; - // Log detailed error information for first few errors - if (timestamp_errors <= 5) { - std::cerr << "TIMESTAMP ERROR #" << timestamp_errors << ":" << std::endl; - std::cerr << " Sample index: " << samp << " of " << trialCont.num_samples << std::endl; - std::cerr << " Last timestamp: " << last_timestamp << std::endl; - std::cerr << " Current timestamp: " << current_timestamp << std::endl; - std::cerr << " Expected: " << (last_timestamp + 1) << std::endl; - std::cerr << " Delta: " << static_cast(current_timestamp - last_timestamp) << std::endl; - std::cerr << " Last sample ch0: " << last_sample_ch0 << std::endl; - std::cerr << " Current sample ch0: " << samples[samp * trialCont.count] << std::endl; - } - } - - int16_t expected_ch0 = (last_sample_ch0 + 1) % UNIQUE_VALUES; - if (samples[samp * trialCont.count + 0] != expected_ch0) { - // Sawtooth channel 0 should increment by 1 each sample - sawtooth_errors++; - if (sawtooth_errors <= 5) { - std::cerr << "SAWTOOTH ERROR #" << sawtooth_errors << " on channel 0:" << std::endl; - std::cerr << " Sample index: " << samp << " of " << trialCont.num_samples << std::endl; - std::cerr << " Last sample ch0: " << last_sample_ch0 << std::endl; - std::cerr << " Current sample ch0: " << samples[samp * trialCont.count + 0] << std::endl; - std::cerr << " Expected: " << expected_ch0 << std::endl; - } - } - int16_t expected_ch1 = (last_sample_ch1 - 1) % UNIQUE_VALUES; - if (samples[samp * trialCont.count + 1] != expected_ch1) { - // Sawtooth channel 1 should decrement by 1 each sample - sawtooth_errors++; - if (sawtooth_errors <= 5) { - std::cerr << "SAWTOOTH ERROR #" << sawtooth_errors << " on channel 1:" << std::endl; - std::cerr << " Sample index: " << samp << " of " << trialCont.num_samples << std::endl; - std::cerr << " Last sample ch1: " << last_sample_ch1 << std::endl; - std::cerr << " Current sample ch1: " << samples[samp * trialCont.count + 1] << std::endl; - std::cerr << " Expected: " << expected_ch1 << std::endl; - } - } - } - last_timestamp = current_timestamp; - last_sample_ch0 = samples[samp * trialCont.count + 0]; - last_sample_ch1 = samples[samp * trialCont.count + 1]; - first_read = false; - - // Validate constant channels - for (uint32_t ch = 2; ch < trialCont.count; ch++) { - if (samples[samp * trialCont.count + ch] != static_cast(trialCont.chan[ch] * 100)) { - constant_errors++; - } - } - } - total_samples_received += trialCont.num_samples; - } - - // Print statistics every 10 seconds - auto now = std::chrono::steady_clock::now(); - auto stats_elapsed = std::chrono::duration_cast( - now - last_stats_time - ).count(); - - if (stats_elapsed >= 10) { - auto total_elapsed = std::chrono::duration_cast( - now - start_time - ).count(); - - const double avg_rate = total_samples_received / (total_elapsed + 1); - - std::cout << "Running for " << total_elapsed << "s" - << " | Samples: " << total_samples_received - << " | Rate: " << static_cast(avg_rate) << " Hz" - << std::endl; - - std::cout << " Errors - Sawtooth: " << sawtooth_errors - << ", Constant: " << constant_errors - << ", Timestamp: " << timestamp_errors - << std::endl; - - last_stats_time = now; - } - - // Small delay to avoid busy-waiting - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - // Final statistics - auto total_elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time - ).count(); - - std::cout << "\n=== Final Statistics ===" << std::endl; - std::cout << "Total runtime: " << total_elapsed << " seconds" << std::endl; - std::cout << "Total samples received: " << total_samples_received << std::endl; - std::cout << "Average sample rate: " << (total_samples_received / (total_elapsed + 1)) << " Hz" << std::endl; - std::cout << "Sawtooth errors: " << sawtooth_errors << std::endl; - std::cout << "Constant value errors: " << constant_errors << std::endl; - std::cout << "Timestamp errors: " << timestamp_errors << std::endl; - - // Close connection - cbSdkClose(INST); - std::cout << "\nConnection closed." << std::endl; - - return (sawtooth_errors > 0 || constant_errors > 0 || timestamp_errors > 0) ? 1 : 0; -} \ No newline at end of file diff --git a/tools/loop_tester/sawtooth_cbsdk_callback.cpp b/tools/loop_tester/sawtooth_cbsdk_callback.cpp deleted file mode 100644 index 26520725..00000000 --- a/tools/loop_tester/sawtooth_cbsdk_callback.cpp +++ /dev/null @@ -1,216 +0,0 @@ -/** - * cbsdk Sawtooth Validator - Callback Version - * - * Connects to nPlayServer and validates timestamps using callbacks - * (bypassing the trial buffer system). - * - * This tests whether duplicate timestamps appear in the callback path - * or only in the trial buffer path. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -// Configuration constants -constexpr uint32_t INST = 0; -constexpr int NUM_CHANNELS = 128; -constexpr uint32_t SAMPLE_GROUP = 6; // Sample group to monitor -constexpr int TEST_DURATION_SEC = 20; - -// Global state for callback -std::atomic keep_running{true}; -std::atomic packet_count{0}; -std::atomic duplicate_count{0}; -std::atomic jump_count{0}; -std::atomic last_timestamp{0}; -std::atomic last_data{0}; - -void signal_handler(int signal) { - std::cout << "\nReceived signal " << signal << ", shutting down gracefully..." << std::endl; - keep_running = false; -} - -void handleResult(cbSdkResult res, const char* operation) { - if (res != CBSDKRESULT_SUCCESS) { - std::cerr << operation << " failed with code " << res << std::endl; - exit(1); - } -} - -// Callback function for continuous packets -void groupCallback(uint32_t nInstance, const cbSdkPktType type, const void* pEventData, void* pCallbackData) -{ - // Cast event data to group packet - const cbPKT_GROUP* pPkt = static_cast(pEventData); - - packet_count++; - - PROCTIME current_ts = pPkt->cbpkt_header.time; - PROCTIME prev_ts = last_timestamp.load(); - - // Only check after first packet - if (prev_ts != 0) { - int64_t delta = static_cast(current_ts) - static_cast(prev_ts); - - // Check for duplicates (first 50000 packets to avoid spam) - if (packet_count <= 50000) { - if (delta == 0) { - duplicate_count++; - fprintf(stderr, "[CALLBACK] DUPLICATE #%llu! ts=%llu, data[0]=%d (prev_ts=%llu, prev_data=%d)\n", - (unsigned long long)duplicate_count.load(), - (unsigned long long)current_ts, - pPkt->data[0], - (unsigned long long)prev_ts, - last_data.load()); - } else if (delta != 1) { - jump_count++; - fprintf(stderr, "[CALLBACK] JUMP #%llu! ts=%llu, delta=%lld, data[0]=%d\n", - (unsigned long long)jump_count.load(), - (unsigned long long)current_ts, - delta, - pPkt->data[0]); - } - } - } - - last_timestamp.store(current_ts); - last_data.store(pPkt->data[0]); -} - -int main(int argc, char* argv[]) { - // Set up signal handlers - std::signal(SIGINT, signal_handler); - std::signal(SIGTERM, signal_handler); - - // Parse command line arguments - std::string inst_ip = ""; - int inst_port = cbNET_UDP_PORT_CNT; - - if (argc > 1) inst_ip = argv[1]; - if (argc > 2) inst_port = std::stoi(argv[2]); - - std::cout << "cbsdk Sawtooth Validator (Callback Version)" << std::endl; - std::cout << "Connecting to: " << (inst_ip.empty() ? "default" : inst_ip) << ":" << inst_port << std::endl; - std::cout << std::endl; - - // Open connection - cbSdkConnectionType conType = CBSDKCONNECTION_DEFAULT; - cbSdkConnection con{}; - con.szOutIP = inst_ip.empty() ? nullptr : inst_ip.c_str(); - con.nOutPort = inst_port; - con.szInIP = ""; - con.nInPort = inst_port; - - cbSdkResult res = cbSdkOpen(INST, conType, con); - handleResult(res, "cbSdkOpen"); - - std::cout << "Connection established!" << std::endl; - std::this_thread::sleep_for(std::chrono::seconds(1)); - - // // Configure channels: enable continuous sampling on SAMPLE_GROUP - // std::cout << "Configuring channels for continuous sampling on group " << SAMPLE_GROUP << "..." << std::endl; - // for (int chan_ix = 0; chan_ix < cbNUM_ANALOG_CHANS; ++chan_ix) { - // cbPKT_CHANINFO chanInfo; - // - // // Get current channel configuration - // res = cbSdkGetChannelConfig(INST, chan_ix + 1, &chanInfo); - // if (res != CBSDKRESULT_SUCCESS) { - // std::cerr << "Warning: cbSdkGetChannelConfig failed for channel " << (chan_ix + 1) << std::endl; - // continue; - // } - // - // // Disable DC offset correction - // chanInfo.ainpopts &= ~cbAINP_OFFSET_CORRECT; - // - // // Enable continuous sampling on SAMPLE_GROUP for first NUM_CHANNELS channels - // chanInfo.smpgroup = (chan_ix < NUM_CHANNELS) ? SAMPLE_GROUP : 0; - // - // // Disable spike processing - // chanInfo.spkopts = cbAINPSPK_NOSORT; - // - // // Apply the modified configuration - // res = cbSdkSetChannelConfig(INST, chan_ix + 1, &chanInfo); - // if (res != CBSDKRESULT_SUCCESS) { - // std::cerr << "Warning: cbSdkSetChannelConfig failed for channel " << (chan_ix + 1) << std::endl; - // } - // } - // std::cout << "Channel configuration complete." << std::endl; - // std::this_thread::sleep_for(std::chrono::seconds(1)); - - // Register callback for continuous packets - std::cout << "Registering callback for continuous packets..." << std::endl; - res = cbSdkRegisterCallback(INST, CBSDKCALLBACK_CONTINUOUS, groupCallback, nullptr); - handleResult(res, "cbSdkRegisterCallback"); - - std::cout << "\nCallback registered. Monitoring for " << TEST_DURATION_SEC << " seconds..." << std::endl; - std::cout << "Press Ctrl+C to stop early." << std::endl; - std::cout << std::endl; - - auto start_time = std::chrono::steady_clock::now(); - auto last_stats_time = start_time; - constexpr auto max_runtime = std::chrono::seconds(TEST_DURATION_SEC); - - while (keep_running) { - // Check if we've exceeded the runtime limit - auto elapsed = std::chrono::steady_clock::now() - start_time; - if (elapsed >= max_runtime) { - std::cout << "\nReached " << TEST_DURATION_SEC << " second timeout, exiting..." << std::endl; - break; - } - - // Print statistics every 5 seconds - auto stats_elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - last_stats_time - ).count(); - - if (stats_elapsed >= 5) { - auto total_elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time - ).count(); - - const uint64_t pkts = packet_count.load(); - const double avg_rate = pkts / (total_elapsed + 1); - - std::cout << "Running for " << total_elapsed << "s" - << " | Packets: " << pkts - << " | Rate: " << static_cast(avg_rate) << " Hz" - << std::endl; - - std::cout << " Duplicates: " << duplicate_count.load() - << ", Jumps: " << jump_count.load() - << std::endl; - - last_stats_time = std::chrono::steady_clock::now(); - } - - // Small delay to avoid busy-waiting - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - // Unregister callback - cbSdkUnRegisterCallback(INST, CBSDKCALLBACK_CONTINUOUS); - - // Final statistics - auto total_elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time - ).count(); - - std::cout << "\n=== Final Statistics ===" << std::endl; - std::cout << "Total runtime: " << total_elapsed << " seconds" << std::endl; - std::cout << "Total packets received: " << packet_count.load() << std::endl; - std::cout << "Average packet rate: " << (packet_count.load() / (total_elapsed + 1)) << " Hz" << std::endl; - std::cout << "Duplicate timestamps: " << duplicate_count.load() << std::endl; - std::cout << "Timestamp jumps: " << jump_count.load() << std::endl; - - // Close connection - cbSdkClose(INST); - std::cout << "\nConnection closed." << std::endl; - - return (duplicate_count.load() > 0 || jump_count.load() > 0) ? 1 : 0; -} diff --git a/tools/loop_tester/sawtooth_cerelink.py b/tools/loop_tester/sawtooth_cerelink.py deleted file mode 100644 index 0302febe..00000000 --- a/tools/loop_tester/sawtooth_cerelink.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -import sys -import time - -import numpy as np -from cerelink import cbpy - -logging.basicConfig(level=logging.NOTSET) -logger = logging.getLogger(__name__) - -def main( - n_chans: int = 128, -): - smpgroup = 6 - con_info = cbpy.open(parameter=cbpy.defaultConParams()) - - for g in range(1, 7): - # Disable all channels in all groups - chan_infos = cbpy.get_sample_group(g) - for ch_info in chan_infos: - cbpy.set_channel_config(ch_info["chan"], chaninfo={"smpgroup": 0}) - - for ch in range(1, n_chans): - cbpy.set_channel_config(ch, chaninfo={"smpgroup": smpgroup, "ainpopts": 0}) - - time.sleep(1.0) - - chan_infos = cbpy.get_sample_group(smpgroup) - n_chans = len(chan_infos) - n_buffer = 102400 - timestamps_buffer = np.zeros(n_buffer, dtype=np.uint64) - samples_buffer = np.zeros((n_buffer, n_chans), dtype=np.int16) - - cbpy.trial_config(activate=True, n_continuous=-1) - - # Track last values from previous chunk for continuity checking - last_timestamp = None - last_ch0_sample = None - last_ch1_sample = None - - try: - while True: - # Subsequent calls: reuse the allocated buffers - data = cbpy.trial_continuous( - seek=True, - group=smpgroup, - timestamps=timestamps_buffer, - samples=samples_buffer - ) - if data["num_samples"] > 0: - ts = data["timestamps"] - samps = data["samples"] - n_samps = data["num_samples"] - - # Prepend last values from previous chunk to check cross-chunk continuity - if last_timestamp is not None: - ts_with_prev = np.concatenate([[last_timestamp], ts[:n_samps]]) - ch0_with_prev = np.concatenate([[last_ch0_sample], samps[:n_samps, 0]]) - ch1_with_prev = np.concatenate([[last_ch1_sample], samps[:n_samps, 1]]) - else: - # First chunk - no previous data - ts_with_prev = ts[:n_samps] - ch0_with_prev = samps[:n_samps, 0] - ch1_with_prev = samps[:n_samps, 1] - - # Check timestamps - td_good = np.diff(ts_with_prev) == 1 - if not np.all(td_good): - bad_idx = np.where(~td_good)[0] - for idx in bad_idx: - logger.warning(f"Non-consecutive timestamps at index {idx}: {ts_with_prev[idx]} -> {ts_with_prev[idx+1]}") - - # Check channel 0: incrementing sawtooth (wraps from 32767 to -32768) - ch0_diff = np.diff(ch0_with_prev) - ch0_expected = np.ones_like(ch0_diff) - # Data are int16 so the diff result wraps itself around - if not np.array_equal(ch0_diff, ch0_expected): - bad_idx = np.where(ch0_diff != ch0_expected)[0] - for idx in bad_idx[:5]: # Log first 5 issues - logger.warning(f"Channel 0 non-consecutive at index {idx}: {ch0_with_prev[idx]} -> {ch0_with_prev[idx+1]} (diff={ch0_diff[idx]})") - - # Check channel 1: decrementing sawtooth (wraps from -32768 to 32767) - ch1_diff = np.diff(ch1_with_prev) - ch1_expected = -np.ones_like(ch1_diff) - if not np.array_equal(ch1_diff, ch1_expected): - bad_idx = np.where(ch1_diff != ch1_expected)[0] - for idx in bad_idx[:5]: # Log first 5 issues - logger.warning(f"Channel 1 non-consecutive at index {idx}: {ch1_with_prev[idx]} -> {ch1_with_prev[idx+1]} (diff={ch1_diff[idx]})") - - # Check constant channels (2+) - expected_const_dat = 100 * np.ones((n_samps, 1), dtype=np.int16) * np.arange(3, n_chans + 1)[None, :] - if not np.array_equal(samps[:n_samps, 2:], expected_const_dat): - logger.warning(f"Data mismatch detected in samples[:, 2:]") - - # Save last values for next iteration - last_timestamp = ts[n_samps - 1] - last_ch0_sample = samps[n_samps - 1, 0] - last_ch1_sample = samps[n_samps - 1, 1] - except KeyboardInterrupt: - cbpy.close() - - -if __name__ == "__main__": - b_try_no_args = False - try: - import typer - - typer.run(main) - except ModuleNotFoundError: - print("Please install typer to use CLI args; using defaults.") - b_try_no_args = True - if b_try_no_args: - main() diff --git a/tools/loop_tester/sawtooth_lsl.cpp b/tools/loop_tester/sawtooth_lsl.cpp deleted file mode 100644 index 96e36839..00000000 --- a/tools/loop_tester/sawtooth_lsl.cpp +++ /dev/null @@ -1,154 +0,0 @@ -/** - * LSL Sawtooth Stream Source - * - * Generates a 30 kHz LSL stream with: - * - Channel 1-2: Sawtooth signal (ramps from -32768 to 32767) - * - Channels 3-130: Constant values (channel_index * 100) - * - * This is used to test long-running cbsdk connections via nPlayServer with LSL support. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -// Configuration constants -constexpr int NUM_CHANNELS = 128; -constexpr int16_t SAWTOOTH_MIN = -32768; -constexpr int16_t SAWTOOTH_MAX = 32767; -constexpr int32_t UNIQUE_VALUES = SAWTOOTH_MAX - SAWTOOTH_MIN + 1; // 65536 -constexpr int SAMPLE_RATE = 30000; // 30 kHz -constexpr int CHUNK_SIZE = 100; // Send 100 samples at a time - -// Global flag for graceful shutdown -std::atomic keep_running{true}; - -void signal_handler(const int signal) { - std::cout << "\nReceived signal " << signal << ", shutting down gracefully..." << std::endl; - keep_running = false; -} - -int main(int argc, char* argv[]) { - // Set up signal handlers for graceful shutdown - std::signal(SIGINT, signal_handler); - std::signal(SIGTERM, signal_handler); - - try { - // Parse command line arguments - std::string stream_name = "SawtoothTest"; - if (argc > 1) { - stream_name = argv[1]; - } - - std::cout << "Creating LSL stream: " << stream_name << std::endl; - std::cout << " Channels: " << NUM_CHANNELS << std::endl; - std::cout << " Sample Rate: " << SAMPLE_RATE << " Hz" << std::endl; - std::cout << " Chunk Size: " << CHUNK_SIZE << " samples" << std::endl; - std::cout << std::endl; - - // Create stream info - lsl::stream_info info( - stream_name, // Stream name - "Simulation", // Content type - NUM_CHANNELS, // Number of channels - SAMPLE_RATE, // Sampling rate - lsl::cf_int16, // Channel format (16-bit integers) - std::string("SawtoothTest_") + std::to_string(time(nullptr)) // Unique source ID - ); - - // Add channel metadata - lsl::xml_element channels = info.desc().append_child("channels"); - for (int i = 0; i < NUM_CHANNELS; i++) { - lsl::xml_element ch = channels.append_child("channel"); - ch.append_child_value("label", "Chan" + std::to_string(i + 1)); - ch.append_child_value("type", i < 2 ? "Sawtooth" : "Constant"); - ch.append_child_value("unit", "quarter-microvolts"); - } - - // Create outlet - lsl::stream_outlet outlet(info, CHUNK_SIZE, 30); // 30 seconds max buffering - - std::cout << "LSL outlet created. Waiting for connections..." << std::endl; - std::cout << "Press Ctrl+C to stop." << std::endl; - std::cout << std::endl; - - // Prepare data buffer - LSL expects vector of samples, where each sample is a vector of channel values - std::vector> chunk(CHUNK_SIZE, std::vector(NUM_CHANNELS)); - - int64_t sample_counter = 0; - auto start_time = std::chrono::steady_clock::now(); - auto next_send_time = start_time; - const auto chunk_duration = std::chrono::microseconds( - (1000000LL * CHUNK_SIZE) / SAMPLE_RATE - ); - - // Statistics - uint64_t chunks_sent = 0; - auto last_stats_time = start_time; - - while (keep_running) { - // Generate chunk of data - for (int sample = 0; sample < CHUNK_SIZE; sample++) { - for (int ch = 0; ch < NUM_CHANNELS; ch++) { - int16_t value; - if (ch == 0) { - // Ramps from -32768 to 32767, 1 integer per sample. - value = static_cast(SAWTOOTH_MIN + sample_counter % UNIQUE_VALUES); - } else if (ch == 1) { - // Ramps in reverse order - value = static_cast(SAWTOOTH_MAX - sample_counter % UNIQUE_VALUES); - } else { - // Channels 3-: Constant value based on channel number - value = static_cast((ch + 1) * 100); - } - chunk[sample][ch] = value; - } - sample_counter++; - } - - // Wait until it's time to send this chunk (maintain timing) - std::this_thread::sleep_until(next_send_time); - - // Send chunk with automatic timestamp - outlet.push_chunk(chunk); - - chunks_sent++; - next_send_time += chunk_duration; - - // Print statistics every 10 seconds - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast( - now - last_stats_time - ).count(); - - if (elapsed >= 10) { - auto total_elapsed = std::chrono::duration_cast( - now - start_time - ).count(); - - std::cout << "Running for " << total_elapsed << "s" - << " | Chunks sent: " << chunks_sent - << " | Samples: " << sample_counter - << " | Rate: " << (chunks_sent * CHUNK_SIZE / (total_elapsed + 1)) << " Hz" - << std::endl; - - last_stats_time = now; - } - } - - std::cout << "\nShutting down..." << std::endl; - std::cout << "Total chunks sent: " << chunks_sent << std::endl; - std::cout << "Total samples sent: " << sample_counter << std::endl; - - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; - return 1; - } - - return 0; -} \ No newline at end of file diff --git a/tools/loop_tester/sawtooth_pycbsdk.py b/tools/loop_tester/sawtooth_pycbsdk.py deleted file mode 100644 index dd7e1a1d..00000000 --- a/tools/loop_tester/sawtooth_pycbsdk.py +++ /dev/null @@ -1,138 +0,0 @@ -import sys -import logging - -from pycbsdk import cbsdk -from pycbsdk.cbhw.packet.common import CBChannelType -import time -import numpy as np - - -logging.basicConfig(level=logging.NOTSET) -logger = logging.getLogger(__name__) - - -class DummyApp: - def __init__(self, nchans: int, duration=21.0, t_step=1 / 30_000): - n_samples = int(np.ceil(duration * 30_000)) - self._nchans = nchans - self._t_step = t_step - self._buffer = np.zeros((n_samples, nchans), dtype=np.int16) - self._ts = np.zeros((n_samples,), dtype=np.int64) - self._write_index = 0 - self._duplicate_count = 0 - self._jump_count = 0 - - def handle_frame(self, pkt): - if self._write_index < self._buffer.shape[0]: - self._buffer[self._write_index, :] = memoryview(pkt.data[: self._nchans]) - self._ts[self._write_index] = pkt.header.time - - # DEBUG: Log first 50000 packets to see all timestamps - if 0 < self._write_index < 50000: - delta = self._ts[self._write_index] - self._ts[self._write_index - 1] - if delta == 0: - self._duplicate_count += 1 - print(f"[PYCBSDK] DUPLICATE at index {self._write_index}: ts={self._ts[self._write_index]}, data[0]={self._buffer[self._write_index, 0]}, prev_data[0]={self._buffer[self._write_index - 1, 0]}") - elif delta != 1: - self._jump_count += 1 - print(f"[PYCBSDK] JUMP at index {self._write_index}: ts={self._ts[self._write_index]}, delta={delta}, data[0]={self._buffer[self._write_index, 0]}") - - self._write_index += 1 - - def finish(self): - print(f"\n=== Timestamp Analysis ===") - print(f"Total duplicates detected: {self._duplicate_count}") - print(f"Total jumps detected: {self._jump_count}") - - b_ts = self._ts > 0 - if np.any(b_ts): - avg_isi = np.mean(np.diff(self._ts[b_ts])) - ts_elapsed = self._ts[b_ts][-1] - self._ts[b_ts][0] + avg_isi - s_elapsed = ts_elapsed * self._t_step - n_samps = np.sum(b_ts) - print( - f"Collected {n_samps} samples in {s_elapsed} s\t({n_samps / s_elapsed:.2f} Hz)." - ) - - -def main( - duration: float = 11.0, - smpgroup: int = 6, - nchans: int = 128, - inst_addr: str = "127.0.0.1", - inst_port: int = 51001, - client_addr: str = "127.0.0.1", - client_port: int = 51002, - recv_bufsize: int = (8 if sys.platform == "win32" else 6) * 1024 * 1024, - protocol: str = "4.1", - loglevel: str = "debug", -): - """ - Run the application: - - Configure the connection to the nsp - - Create an app, then register it is a callback that receives smp frames and updates internal state - """ - # Handle logger arguments - loglevel = { - "debug": logging.DEBUG, - "info": logging.INFO, - "warning": logging.WARNING, - }[loglevel.lower()] - logger.setLevel(loglevel) - - # Create connection to the device. - params_obj = cbsdk.create_params( - inst_addr=inst_addr, - inst_port=inst_port, - client_addr=client_addr, - client_port=client_port, - recv_bufsize=recv_bufsize, - protocol=protocol, - ) - nsp_obj = cbsdk.get_device(params_obj) - if cbsdk.connect(nsp_obj, startup_sequence=True) != 50: - logger.error(f"Could not connect to device with params {params_obj}") - sys.exit(-1) - config = cbsdk.get_config(nsp_obj) - - # Disable all channels - for chtype in [CBChannelType.FrontEnd, CBChannelType.AnalogIn]: - cbsdk.set_all_channels_disable(nsp_obj, chtype) - - # Enable first nchans at smpgroup. For smpgroup < 5, this also updates the smpfilter. - for ch in range(1, nchans + 1): - _ = cbsdk.set_channel_config(nsp_obj, ch, "smpgroup", smpgroup) - - # Create a dummy app. - t_step = 1 / (1e9 if config["b_gemini"] else config["sysfreq"]) - app = DummyApp(nchans, duration=duration, t_step=t_step) - - time.sleep(2.0) - - # Register callbacks to update the app's state when appropriate packets are received. - _ = cbsdk.register_group_callback(nsp_obj, smpgroup, app.handle_frame) - - t_start = time.time() - try: - t_elapsed = time.time() - t_start - while t_elapsed < duration: - time.sleep(1.0) - t_elapsed = time.time() - t_start - except KeyboardInterrupt: - pass - finally: - app.finish() - _ = cbsdk.disconnect(nsp_obj) - - -if __name__ == "__main__": - b_try_no_args = False - try: - import typer - - typer.run(main) - except ModuleNotFoundError: - print("Please install typer to use CLI args; using defaults.") - b_try_no_args = True - if b_try_no_args: - main() diff --git a/tools/loop_tester/spikes_cbsdk.cpp b/tools/loop_tester/spikes_cbsdk.cpp deleted file mode 100644 index a0c24f1e..00000000 --- a/tools/loop_tester/spikes_cbsdk.cpp +++ /dev/null @@ -1,427 +0,0 @@ -/** - * spikes_cbsdk.cpp - * - * Validates spike event timing from the spikes_lsl.py test pattern. - * - * Expected pattern: - * - Single spikes (9s): 36 spikes rotating through channels 1-4, every 0.25s (7500 samples) - * - Burst period (1s): 99 spikes on all channels simultaneously, every 0.01s (300 samples) - * - * Synchronization strategy: - * 1. Look for burst period (4 channels spiking within ~1ms window) - * 2. First non-coincident spike after burst marks start of single-spike pattern - * 3. Validate single-spike timing matches expected pattern - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -constexpr uint32_t INST = 0; -constexpr uint32_t SAMPLE_RATE = 30000; -constexpr float SAMPLES_TO_MS = 1000.0f / SAMPLE_RATE; - -// Expected pattern parameters -constexpr uint32_t SINGLE_SPIKE_INTERVAL = 7500; // 0.25s in samples -constexpr uint32_t BURST_SPIKE_INTERVAL = 300; // 0.01s in samples -constexpr uint32_t TOLERANCE_SAMPLES = 100; // Tolerance for timing (±3.3ms) - -// Channels to monitor (1-based) -constexpr uint16_t TEST_CHANNELS[] = {1, 2, 3, 4}; -constexpr size_t NUM_TEST_CHANNELS = 4; - -struct SpikeEvent { - PROCTIME timestamp; - uint16_t channel; - uint16_t unit; -}; - -enum class PatternState { - SEARCHING_FOR_BURST, - IN_BURST, - VALIDATING_SINGLE_SPIKES -}; - -class SpikeValidator { -public: - SpikeValidator() : state_(PatternState::SEARCHING_FOR_BURST), - burst_spike_count_(0), - pattern_start_time_(0), - next_expected_channel_(0), - single_spike_count_(0), - timing_errors_(0) {} - - void processEvents(const std::vector& events) { - for (const auto& event : events) { - // Only process test channels - bool is_test_channel = false; - for (const auto& ch : TEST_CHANNELS) { - if (event.channel == ch) { - is_test_channel = true; - break; - } - } - if (!is_test_channel) continue; - - switch (state_) { - case PatternState::SEARCHING_FOR_BURST: - checkForBurstStart(event); - break; - - case PatternState::IN_BURST: - processBurstEvent(event); - break; - - case PatternState::VALIDATING_SINGLE_SPIKES: - validateSingleSpike(event); - break; - } - } - } - - void printStatus() const { - std::cout << "\n=== Spike Validation Status ===\n"; - switch (state_) { - case PatternState::SEARCHING_FOR_BURST: - std::cout << "State: Searching for burst period...\n"; - std::cout << "Recent events in window: " << recent_events_.size() << "\n"; - break; - - case PatternState::IN_BURST: - std::cout << "State: Processing burst period\n"; - std::cout << "Burst spikes detected: " << burst_spike_count_ << " / 99\n"; - break; - - case PatternState::VALIDATING_SINGLE_SPIKES: - std::cout << "State: Validating single spike pattern\n"; - std::cout << "Single spikes validated: " << single_spike_count_ << " / 36\n"; - std::cout << "Next expected channel: " << TEST_CHANNELS[next_expected_channel_] << "\n"; - std::cout << "Timing errors: " << timing_errors_ << "\n"; - - if (single_spike_count_ >= 36) { - std::cout << "\n*** PATTERN VALIDATED SUCCESSFULLY! ***\n"; - std::cout << "Total timing errors: " << timing_errors_ << " / " << single_spike_count_ << "\n"; - - // Reset to validate next cycle - const_cast(this)->resetForNextCycle(); - } - break; - } - std::cout << "===============================\n"; - } - -private: - void checkForBurstStart(const SpikeEvent& event) { - // Keep a sliding window of recent events (last 50ms) - const PROCTIME window_duration = 50 * SAMPLE_RATE / 1000; // 50ms in samples - - // Add new event - recent_events_.push_back(event); - - // Remove old events outside window - while (!recent_events_.empty() && - event.timestamp - recent_events_.front().timestamp > window_duration) { - recent_events_.pop_front(); - } - - // Check if we have simultaneous spikes on all 4 channels - // (within 1ms = 30 samples) - if (recent_events_.size() >= NUM_TEST_CHANNELS) { - if (checkForCoincidentSpikes()) { - std::cout << "\n*** BURST PERIOD DETECTED! ***\n"; - state_ = PatternState::IN_BURST; - burst_start_time_ = event.timestamp; - burst_spike_count_ = 0; - recent_events_.clear(); - } - } - } - - bool checkForCoincidentSpikes() { - // Look for a time window where all 4 channels spiked - const PROCTIME coincidence_window = 30; // 1ms in samples - - for (auto it = recent_events_.begin(); it != recent_events_.end(); ++it) { - // Count how many different channels spiked within coincidence_window - std::set channels_in_window; - - for (auto check_it = it; check_it != recent_events_.end(); ++check_it) { - if (check_it->timestamp - it->timestamp > coincidence_window) { - break; - } - channels_in_window.insert(check_it->channel); - } - - if (channels_in_window.size() >= NUM_TEST_CHANNELS) { - return true; - } - } - - return false; - } - - void processBurstEvent(const SpikeEvent& event) { - burst_spike_count_++; - - // Check if we've seen enough burst spikes to be confident - // After ~50 burst spikes, start watching for single spikes - if (burst_spike_count_ > 50) { - // Check if this might be the first single spike - // (only one channel spikes, not all 4) - recent_events_.push_back(event); - - // Keep only last 2ms of events - const PROCTIME window = 60; // 2ms in samples - while (!recent_events_.empty() && - event.timestamp - recent_events_.front().timestamp > window) { - recent_events_.pop_front(); - } - - // If we only see 1 channel in the recent window, burst is over - std::set channels_in_window; - for (const auto& evt : recent_events_) { - channels_in_window.insert(evt.channel); - } - - if (channels_in_window.size() == 1) { - std::cout << "\n*** BURST PERIOD ENDED (detected " << burst_spike_count_ << " spikes) ***\n"; - std::cout << "*** STARTING SINGLE SPIKE VALIDATION ***\n"; - - state_ = PatternState::VALIDATING_SINGLE_SPIKES; - pattern_start_time_ = event.timestamp; - next_expected_channel_ = 0; // Expect channel 1 (index 0) - single_spike_count_ = 0; - timing_errors_ = 0; - - // Process this event as the first single spike - validateSingleSpike(event); - } - } - } - - void validateSingleSpike(const SpikeEvent& event) { - // Check if this is the expected channel - uint16_t expected_ch = TEST_CHANNELS[next_expected_channel_]; - - if (event.channel != expected_ch) { - std::cout << "WARNING: Expected channel " << expected_ch - << " but got " << event.channel << "\n"; - return; - } - - // Check timing - PROCTIME expected_time = pattern_start_time_ + (single_spike_count_ * SINGLE_SPIKE_INTERVAL); - int64_t time_error = static_cast(event.timestamp) - static_cast(expected_time); - - if (std::abs(time_error) > TOLERANCE_SAMPLES) { - std::cout << "TIMING ERROR: Spike " << single_spike_count_ - << " on channel " << event.channel - << " is " << (time_error * SAMPLES_TO_MS) << " ms off\n"; - timing_errors_++; - } - - // Move to next expected channel (cycles 0,1,2,3,0,1,2,3...) - next_expected_channel_ = (next_expected_channel_ + 1) % NUM_TEST_CHANNELS; - single_spike_count_++; - - if (single_spike_count_ % 9 == 0) { - std::cout << "Validated " << single_spike_count_ << " / 36 single spikes\n"; - } - } - - void resetForNextCycle() { - state_ = PatternState::SEARCHING_FOR_BURST; - burst_spike_count_ = 0; - single_spike_count_ = 0; - timing_errors_ = 0; - recent_events_.clear(); - std::cout << "\nResetting to validate next 10-second cycle...\n\n"; - } - - PatternState state_; - std::deque recent_events_; - - // Burst detection - PROCTIME burst_start_time_; - uint32_t burst_spike_count_; - - // Single spike validation - PROCTIME pattern_start_time_; - uint32_t next_expected_channel_; // Index into TEST_CHANNELS - uint32_t single_spike_count_; - uint32_t timing_errors_; -}; - -int main(int argc, char* argv[]) -{ - cbSdkResult res; - - // Open connection - std::cout << "Opening cbsdk connection...\n"; - res = cbSdkOpen(INST, CBSDKCONNECTION_DEFAULT, {}); - if (res != CBSDKRESULT_SUCCESS) { - std::cerr << "Failed to open cbsdk: " << res << "\n"; - return 1; - } - std::cout << "Connected!\n"; - - // Check connection type - cbSdkConnectionType conType; - cbSdkInstrumentType instType; - res = cbSdkGetType(INST, &conType, &instType); - if (res == CBSDKRESULT_SUCCESS) { - std::cout << "Connection type: " << (conType == CBSDKCONNECTION_UDP ? "UDP" : "Central") << "\n"; - } - - // Configure all channels: enable spike detection ONLY on channels 1-4, disable everything else - std::cout << "\nConfiguring channels...\n"; - for (uint16_t ch = 1; ch <= 128; ch++) { - cbPKT_CHANINFO chaninfo; - - // Get current channel configuration - res = cbSdkGetChannelConfig(INST, ch, &chaninfo); - if (res != CBSDKRESULT_SUCCESS) { - continue; // Channel doesn't exist - } - - // Check if this is a test channel (1-4) - bool is_test_channel = false; - for (const auto& test_ch : TEST_CHANNELS) { - if (ch == test_ch) { - is_test_channel = true; - break; - } - } - - if (is_test_channel) { - // Test channels: disable continuous, enable spike detection - chaninfo.ainpopts &= ~cbAINP_RAWSTREAM; - chaninfo.ainpopts |= cbAINP_SPKSTREAM; - chaninfo.ainpopts |= cbAINP_SPKPROC; - - res = cbSdkSetChannelConfig(INST, ch, &chaninfo); - if (res == CBSDKRESULT_SUCCESS) { - std::cout << " Channel " << ch << ": Spike detection enabled\n"; - } - } else { - // All other channels: disable both continuous and spike detection - chaninfo.ainpopts &= ~cbAINP_RAWSTREAM; - chaninfo.ainpopts &= ~cbAINP_SPKSTREAM; - chaninfo.ainpopts &= ~cbAINP_SPKPROC; - - res = cbSdkSetChannelConfig(INST, ch, &chaninfo); - // Only print if we actually changed something - // (no output to avoid cluttering for 124 channels) - } - } - - std::cout << "\nChannel configuration complete!\n"; - - // Configure trial collection (enable event collection) - std::cout << "Configuring trial collection...\n"; - res = cbSdkSetTrialConfig(INST, - 1, // bActive = true - 0, 0, 0, // begchan, begmask, begval (start immediately) - 0, 0, 0, // endchan, endmask, endval (no auto-end) - 0, // uWaveforms = 0 (not collecting waveforms) - 0, // uConts = 0 (not collecting continuous) - cbSdk_EVENT_DATA_SAMPLES, // uEvents (default buffer size) - 0, // uComments = 0 - 0); // uTrackings = 0 - if (res != CBSDKRESULT_SUCCESS) { - std::cerr << "Failed to configure trial: " << res << "\n"; - cbSdkClose(INST); - return 1; - } - std::cout << "Trial configured for event collection\n"; - - // Allocate trial event structure and buffers - // Pre-allocate to cbSdk_EVENT_DATA_SAMPLES capacity so cbSdkGetTrialData can fill as many events as available - auto trialEvent = std::make_unique(); - std::vector event_ts(cbSdk_EVENT_DATA_SAMPLES); - std::vector event_channels(cbSdk_EVENT_DATA_SAMPLES); - std::vector event_units(cbSdk_EVENT_DATA_SAMPLES); - - // Set up trial event structure with our pre-allocated buffers - trialEvent->num_events = cbSdk_EVENT_DATA_SAMPLES; // Buffer capacity - trialEvent->timestamps = event_ts.data(); - trialEvent->channels = event_channels.data(); - trialEvent->units = event_units.data(); - trialEvent->waveforms = nullptr; - - std::cout << "Allocated event buffers (capacity: " << cbSdk_EVENT_DATA_SAMPLES << " events)\n"; - - std::cout << "\nListening for spike events on channels 1-4...\n"; - std::cout << "Searching for burst period to synchronize...\n"; - std::cout << "Will run for 30 seconds...\n"; - - SpikeValidator validator; - uint32_t read_count = 0; - auto start_time = std::chrono::steady_clock::now(); - const auto max_duration = std::chrono::seconds(30); - - // Main loop - run for 30 seconds - uint32_t total_events_received = 0; - while (std::chrono::steady_clock::now() - start_time < max_duration) { - // Reset num_events to buffer capacity before each call - uint32_t buffer_capacity = cbSdk_EVENT_DATA_SAMPLES; - trialEvent->num_events = buffer_capacity; - - // Get trial events (with seek to advance buffer) - res = cbSdkGetTrialData(INST, 1, trialEvent.get(), nullptr, nullptr, nullptr); - - if (res == CBSDKRESULT_SUCCESS) { - if (trialEvent->num_events > 0) { - read_count++; - total_events_received += trialEvent->num_events; - - // Debug output for first few reads - if (read_count <= 5) { - std::cout << "Read #" << read_count << ": Received " << trialEvent->num_events << " events\n"; - } - - // Convert to vector of SpikeEvent for easier processing - std::vector events; - for (uint32_t i = 0; i < trialEvent->num_events; i++) { - events.push_back({ - event_ts[i], - event_channels[i], - event_units[i] - }); - } - - // Process events - validator.processEvents(events); - - // Print status every 100 reads (~3 seconds at 30Hz) - if (read_count % 100 == 0) { - std::cout << "Total events received so far: " << total_events_received << "\n"; - validator.printStatus(); - } - } - } else { - std::cerr << "cbSdkGetTrialData failed with error: " << res << "\n"; - } - - // Sleep briefly to avoid busy-waiting - std::this_thread::sleep_for(std::chrono::milliseconds(33)); // ~30 Hz polling - } - - std::cout << "\n30 seconds elapsed. Final status:\n"; - validator.printStatus(); - - // Cleanup - cbSdkClose(INST); - return 0; -} diff --git a/tools/loop_tester/spikes_lsl.py b/tools/loop_tester/spikes_lsl.py deleted file mode 100644 index 13e4600c..00000000 --- a/tools/loop_tester/spikes_lsl.py +++ /dev/null @@ -1,189 +0,0 @@ -import numpy as np -import pylsl -import time - - -wf_orig = [ - [ - 0, 34, 68, 96, 123, 191, 258, 291, 325, 291, 258, - 112, -33, -397, -760, -777, -794, -688, -581, -380, -179, 6, - 191, 275, 358, 370, 381, 330, 280, 230, 179, 118, 56, - 28, 0, -11, -22, -28, -33, -16, 0 - ], - [ - 0, -16, -33, -67, -100, -296, -492, -447, -402, 34, 470, - 549, 627, 470, 314, 90, -134, -167, -201, -190, -179, -162, - -145, -100, -56, -22, 12, 40, 68, 73, 79, 45, 12, - -5, -22, -22, -22, -16, -11, -5, 0 - ], - [ - 0, -22, -44, -100, -156, -380, -604, -604, -604, -397, -190, - 34, 258, 426, 593, 700, 806, 677, 549, 358, 168, 90, - 12, -22, -56, -67, -78, -84, -89, -89, -89, -89, -89, - -84, -78, -61, -44, -39, -33, -16, 0 - ] -] -wf_orig = np.array(wf_orig, dtype=np.int16) - - -def main(num_channels: int = 128, chunk_size: int = 100): - """ - The pattern during single spikes: - - ``` - ** Ch 1 1-----------2-----------3-----------1-----------2- ... and - ** Ch 2 ---2-----------3-----------1-----------2---------- ... on - ** Ch 3 ------3-----------1-----------2-----------3------- ... and - ** Ch 4 ---------1-----------2-----------3-----------1---- ... on - ** Ch 5 1-----------2-----------3-----------1-----------2- ... and - ``` - Waveforms are inserted 7500 samples (0.25 seconds). At each iteration, - the next waveform (% 3 waveforms) is inserted into the next channel (% 4 channels). - This repeats until there are 36 spike + gap periods (36 * 7500), for 9 seconds total. - - During bursting: - - ``` - ** Ch 1 1---2---3---1---2---3---1---2---3---1---2---3---1- ... and - ** Ch 2 1---2---3---1---2---3---1---2---3---1---2---3---1- ... on - ** Ch 3 1---2---3---1---2---3---1---2---3---1---2---3---1- ... and - ** Ch 4 1---2---3---1---2---3---1---2---3---1---2---3---1- ... on - ** Ch 5 1---2---3---1---2---3---1---2---3---1---2---3---1- ... and - ``` - - Bursts begin immediately after the last gap following the 36th spike during the slow period. - During bursting, a spike occurs in all channels simultaneously with the same waveform. - Each spike in a burst comprises a spike waveform + gap for 300 total samples (0.01 s). - Waveforms cycle in order, 1, 2, 3, 1, 2, 3, and so on. - There are 99 total spikes in a burst for 0.99 second total. - - A burst ends with a 300 sample gap (so 600 from the onset of the last spike in the burst) before the slow period begins again. - - All above spikes are additive on a common background pattern. - The background pattern is a sum of 3 sinusoids at frequencies of 1.0, 3.0, and 9.0 Hz. - - :param num_channels: - :param chunk_size: - :return: - """ - # Constants - SAMPLE_RATE = 30_000 - SINGLE_SPIKE_DURATION = 7500 # 0.25 seconds per spike + gap - NUM_SINGLE_SPIKES = 36 - BURST_SPIKE_DURATION = 300 # 0.01 seconds per spike + gap - NUM_BURST_SPIKES = 99 - BURST_END_GAP = 300 # Extra gap after burst - SINE_AMPLITUDE = 894.4 - - # Total samples for each period - single_spike_period_samples = NUM_SINGLE_SPIKES * SINGLE_SPIKE_DURATION # 270,000 samples (9 seconds) - burst_period_samples = NUM_BURST_SPIKES * BURST_SPIKE_DURATION + BURST_END_GAP # 30,000 samples (1 second) - total_samples = single_spike_period_samples + burst_period_samples # 300,000 samples (10 seconds) - - print(f"Generating {total_samples / SAMPLE_RATE:.1f} second signal ({total_samples:,} samples) for {num_channels} channels...") - - # Create background: sum of 3 sinusoids at 1.0, 3.0, and 9.0 Hz - t = np.arange(total_samples) / SAMPLE_RATE - background = ( - SINE_AMPLITUDE * np.sin(2 * np.pi * 1.0 * t) + - SINE_AMPLITUDE * np.sin(2 * np.pi * 3.0 * t) + - SINE_AMPLITUDE * np.sin(2 * np.pi * 9.0 * t) - ).astype(np.int16) - - # Initialize signal array [samples, channels] via broadcast addition - signal = background[:, None] + np.zeros((1, num_channels), dtype=np.int16) - - # Generate single spike period (first 9 seconds) - print("Adding single spikes...") - for spike_idx in range(NUM_SINGLE_SPIKES): - # Determine which channel gets this spike (cycles through 4 channels) - channel = spike_idx % 4 - - # Determine which waveform to use (cycles through 3 waveforms) - waveform_idx = spike_idx % 3 - waveform = wf_orig[waveform_idx] - - # Insert waveform at the start of this spike period - start_sample = spike_idx * SINGLE_SPIKE_DURATION - signal[start_sample:start_sample + len(waveform), channel] += waveform - - # Generate burst period (last 1 second) - print("Adding burst spikes...") - burst_start = single_spike_period_samples - for burst_spike_idx in range(NUM_BURST_SPIKES): - # Determine which waveform to use (cycles through 3 waveforms) - waveform_idx = burst_spike_idx % 3 - waveform = wf_orig[waveform_idx] - - # Insert waveform into ALL channels simultaneously - start_sample = burst_start + burst_spike_idx * BURST_SPIKE_DURATION - signal[start_sample:start_sample + len(waveform), :] += waveform[:, None] - - print(f"Signal prepared. Shape: {signal.shape}") - - outlet = pylsl.StreamOutlet( - pylsl.StreamInfo( - name="SpikeTest", - type="Simulation", - channel_count=num_channels, - nominal_srate=SAMPLE_RATE, - channel_format=pylsl.cf_int16, - source_id="spikes1234" - ) - ) - - print(f"Now sending data in chunks of {chunk_size} samples...") - print("Press Ctrl+C to stop") - - # Stream the signal in chunks, maintaining real-time pacing - sample_idx = 0 - start_time = pylsl.local_clock() - - try: - while True: - # Loop through the 10-second cycle continuously - chunk_start = sample_idx % total_samples - chunk_end = min(chunk_start + chunk_size, total_samples) - - # Extract chunk - chunk = signal[chunk_start:chunk_end, :] - - # If we wrapped around, handle the wrap - if (chunk_start + chunk_size) > total_samples and sample_idx > 0: - # Need to wrap to beginning - remainder = (chunk_start + chunk_size) - total_samples - wrap_chunk = signal[:, :remainder] - chunk = np.vstack([chunk, wrap_chunk]) - - # Send chunk to LSL outlet - outlet.push_chunk(chunk) - - # Update sample counter - sample_idx += chunk_size - - # Calculate how long to sleep to maintain real-time pacing - expected_time = start_time + (sample_idx / SAMPLE_RATE) - current_time = pylsl.local_clock() - sleep_time = expected_time - current_time - - if sleep_time > 0: - time.sleep(sleep_time) - elif sleep_time < -0.1: - # Warn if we're falling behind by more than 100ms - print(f"Warning: Running {-sleep_time:.3f}s behind real-time") - - except KeyboardInterrupt: - print("\nStopped by user") - - -if __name__ == "__main__": - b_try_no_args = False - try: - import typer - - typer.run(main) - except ModuleNotFoundError: - print("Please install typer to use CLI args; using defaults.") - b_try_no_args = True - if b_try_no_args: - main() diff --git a/tools/n2h5/.cproject b/tools/n2h5/.cproject deleted file mode 100755 index 67291f4a..00000000 --- a/tools/n2h5/.cproject +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/n2h5/.project b/tools/n2h5/.project deleted file mode 100755 index ac00604b..00000000 --- a/tools/n2h5/.project +++ /dev/null @@ -1,87 +0,0 @@ - - - n2h5 - - - - - - org.eclipse.cdt.managedbuilder.core.genmakebuilder - clean,full,incremental, - - - ?children? - ?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|\|| - - - ?name? - - - - org.eclipse.cdt.make.core.append_environment - true - - - org.eclipse.cdt.make.core.autoBuildTarget - all - - - org.eclipse.cdt.make.core.buildArguments - - - - org.eclipse.cdt.make.core.buildCommand - mingw32-make - - - org.eclipse.cdt.make.core.buildLocation - ${workspace_loc:/n2h5} - - - org.eclipse.cdt.make.core.cleanBuildTarget - clean - - - org.eclipse.cdt.make.core.contents - org.eclipse.cdt.make.core.activeConfigSettings - - - org.eclipse.cdt.make.core.enableAutoBuild - false - - - org.eclipse.cdt.make.core.enableCleanBuild - true - - - org.eclipse.cdt.make.core.enableFullBuild - true - - - org.eclipse.cdt.make.core.fullBuildTarget - all - - - org.eclipse.cdt.make.core.stopOnError - true - - - org.eclipse.cdt.make.core.useDefaultBuildCmd - false - - - - - org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder - full,incremental, - - - - - - org.eclipse.cdt.core.cnature - org.eclipse.cdt.core.ccnature - org.eclipse.cdt.managedbuilder.core.managedBuildNature - org.eclipse.cdt.managedbuilder.core.ScannerConfigNature - - diff --git a/tools/n2h5/CMakeLists.txt b/tools/n2h5/CMakeLists.txt deleted file mode 100644 index b2347341..00000000 --- a/tools/n2h5/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -## -# n2h5 -find_package( HDF5 COMPONENTS C HL) -if(HDF5_FOUND) - set(N2H5_SOURCE - ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/n2h5.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/n2h5.h - ${CMAKE_CURRENT_SOURCE_DIR}/NevNsx.h - ) - message(STATUS "Add n2h5 utility build target") - if(MSVC) - set(N2H5_SOURCE ${N2H5_SOURCE} ${CMAKE_CURRENT_SOURCE_DIR}/res/n2h5_res.rc) - endif(MSVC) - add_executable( n2h5 ${N2H5_SOURCE} ) - target_include_directories( n2h5 PRIVATE ${LIB_INCL_DIRS} ${HDF5_INCLUDE_DIRS}) - target_link_libraries(n2h5 ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES}) - list(APPEND INSTALL_TARGET_LIST n2h5) -endif(HDF5_FOUND ) diff --git a/tools/n2h5/NevNsx.h b/tools/n2h5/NevNsx.h deleted file mode 100755 index 6db7ab7a..00000000 --- a/tools/n2h5/NevNsx.h +++ /dev/null @@ -1,231 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2012 Blackrock Microsystems -// -// $Workfile: NevNsx.h $ -// $Archive: /n2h5/NevNsx.h $ -// $Revision: 1 $ -// $Date: 11/1/12 1:00p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// - -#ifndef NEVNSX_H_ -#define NEVNSX_H_ - -#include "cbhwlib.h" - -#pragma pack(push, 1) - -#ifndef WIN32 -typedef struct _SYSTEMTIME -{ - uint16_t wYear; - uint16_t wMonth; - uint16_t wDayOfWeek; - uint16_t wDay; - uint16_t wHour; - uint16_t wMinute; - uint16_t wSecond; - uint16_t wMilliseconds; -} SYSTEMTIME; -#endif - -// SQL time format -typedef struct -{ - char chYear[4]; - char ch4; - char chMonth[2]; - char ch7; - char chDay[2]; - char ch10; - char chHour[2]; - char ch13; - char chMinute[2]; - char ch16; - char chSecond[2]; - char ch19; - char chMicroSecond[6]; - char chNull; -} TIMSTM; - -typedef struct -{ - char achFileID[8]; // Always set to NEURALSG - char szGroup[16]; // Label of the group - uint32_t nPeriod; // How many ticks per sample (30,000 = rate) - uint32_t cnChannels; // How many channels - - // Next comes - // uint32_t // Which channels? - // then - // int16_t wData; // Data from each channel -} Nsx21Hdr; - -typedef struct -{ - char achFileID[8]; // Always set to NEURALCD - unsigned char nMajor; // Major version number - unsigned char nMinor; // Minor version number - uint32_t nBytesInHdrs; // Bytes in all headers also pointer to first data pkt - char szGroup[16]; // Label of the group - char szComment[256]; // File comment - uint32_t nPeriod; // How many ticks per sample (30,000 = rate) - uint32_t nResolution; // Time resolution of time stamps - SYSTEMTIME isAcqTime; // Windows time structure - uint32_t cnChannels; // How many channels -} Nsx22Hdr; - -typedef struct -{ - char achExtHdrID[2]; // Always set to CC - uint16_t id; // Which channel - char label[16]; // What is the "name" of this electrode? - uint8_t phys_connector; // Which connector (e.g. bank 1) - uint8_t connector_pin; // Which pin on that collector - int16_t digmin; // Minimum digital value - int16_t digmax; // Maximum digital value - int16_t anamin; // Minimum Analog Value - int16_t anamax; // Maximum Analog Value - char anaunit[16]; // Units for the Analog Value (e.g. "mV) - uint32_t hpfreq; - uint32_t hporder; - int16_t hptype; - uint32_t lpfreq; - uint32_t lporder; - int16_t lptype; -} Nsx22ExtHdr; - -typedef struct -{ - char nHdr; // Always set to 0x01 - uint32_t nTimestamp; // Which channel - uint32_t nNumDatapoints; // Number of datapoints - - // Next comes - // int16_t DataPoint // Data points -} Nsx22DataHdr; - -// This is the basic header data structure as recorded in the NEV file -typedef struct -{ - char achFileType[8]; // should always be "NEURALEV" - uint8_t byFileRevMajor; // Major version of the file - uint8_t byFileRevMinor; // Minor version of the file - uint16_t wFileFlags; // currently set to "0x01" to mean all data is 16 bit - uint32_t dwStartOfData; // how many bytes are are ALL of the headers - uint32_t dwBytesPerPacket; // All "data" is fixed length...what is that length - uint32_t dwTimeStampResolutionHz; // How many counts per second for the global clock (now 30,000) - uint32_t dwSampleResolutionHz; // How many counts per second for the samples (now 30,000 as well) - SYSTEMTIME isAcqTime; // Greenwich Mean Time when data was collected - char szApplication[32]; // Which application created this? NULL terminated - char szComment[256]; // Comments (NULL terminated) - uint32_t dwNumOfExtendedHeaders; // How many extended headers are there? -} NevHdr; - -// This is the extra header data structure as recorded in the NEV file -typedef struct -{ - char achPacketID[8]; // "NEUWAV", "NEULABL", "NEUFLT", ... - union { - struct { - uint16_t id; - union { - struct { - uint8_t phys_connector; - uint8_t connector_pin; - uint16_t digital_factor; - uint16_t energy_thresh; - int16_t high_thresh; - int16_t low_thresh; - uint8_t sorted_count; - uint8_t wave_bytes; - uint16_t wave_samples; - char achReserved[8]; - } neuwav; - struct { - char label[16]; - char achReserved[6]; - } neulabel; - struct { - uint32_t hpfreq; - uint32_t hporder; - int16_t hptype; - uint32_t lpfreq; - uint32_t lporder; - int16_t lptype; - char achReserved[2]; - } neuflt; - struct { - char label[16]; - float fFps; - char achReserved[2]; - } videosyn; - struct { - uint16_t trackID; - uint16_t maxPoints; - char label[16]; - char achReserved[2]; - } trackobj; - }; - }; - struct { - char label[16]; - uint8_t mode; - char achReserved[7]; - } diglabel; - struct { - char label[24]; - } mapfile; - }; -} NevExtHdr; - -// This is the data structure as recorded in the NEV file -// The size is larger than any packet to safely read them from file -typedef struct -{ - uint32_t dwTimestamp; - uint16_t wPacketID; - union { - struct { - uint8_t byInsertionReason; - uint8_t byReserved; - uint16_t wDigitalValue; - char achReserved[260]; - } digital; // digital or serial data - struct { - uint8_t charset; - uint8_t flags; - uint32_t data; - char comment[258]; - } comment; // comment data - struct { - uint16_t split; - uint32_t frame; - uint32_t etime; - uint32_t id; - char achReserved[250]; - } synch; // synchronization data - struct { - uint16_t parentID; - uint16_t nodeID; - uint16_t nodeCount; - uint16_t coordsLength; - uint16_t coords[cbMAX_TRACKCOORDS]; - } track; // tracking data - struct { - uint8_t unit; - uint8_t res; - int16_t wave[cbMAX_PNTS]; - char achReserved[6]; - } spike; // spike data - }; - char achReserved[754]; -} NevData; - -#pragma pack(pop) - -#endif /* NEVNSX_H_ */ diff --git a/tools/n2h5/main.cpp b/tools/n2h5/main.cpp deleted file mode 100755 index b7d4a3fa..00000000 --- a/tools/n2h5/main.cpp +++ /dev/null @@ -1,1582 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2012 Blackrock Microsystems -// -// $Workfile: main.c $ -// $Archive: /n2h5/main.c $ -// $Revision: 1 $ -// $Date: 11/1/12 1:00p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// -// -// PURPOSE: -// -// Convert nsx and nev files to hdf5 format -// -// Multiple data groups can be merged in one file -// For example to combine different experiments. -// Main data group populates the root itself, -// the next roots at "/group00001" and so forth -// The field GroupCount in the root attributes contains total data groups count -// While first level group names are part of the spec (/channel, /comment, /group00001, ...) -// the sub-group names should not be relied upon, instead all children should be iterated -// and attributes consulted to find the information about each sub-group -// To merge channels of the same experiment, it should be added to the same group -// for example the raw recording of /channel/channel00001 can go to /channel/channel00001_1 -// -////////////////////////////////////////////////////////////////////////////// -#include "stdafx.h" -#include - -#include "n2h5.h" -#include "NevNsx.h" - -#ifndef WIN32 -#include -#include -#include -#endif - - -#ifdef WIN32 // Windows needs the different spelling -#define ftello _ftelli64 -#define fseeko _fseeki64 -#endif - - // TODO: optimize these numbers -#define CHUNK_SIZE_CONTINUOUS (1024) -#define CHUNK_SIZE_EVENT (1024) - -uint16_t g_nCombine = 0; // subchannel combine level -bool g_bAppend = false; -bool g_bNoSpikes = false; -bool g_bSkipEmpty = false; - -// Keep last synch packet -BmiSynch_t g_synch = {0}; - -// Author & Date: Ehsan Azar Jan 16, 2013 -// Purpose: Add created header to the hdf file -// Inputs: -// file - the destination file -// header - Root header -// Outputs: -// Returns 0 on success, error code otherwise -int AddRoot(hid_t file, BmiRootAttr_t & header) -{ - herr_t ret; - - // Add file header as attribute of the root group - { - std::string strAttr = "BmiRoot"; - hsize_t dims[1] = {1}; - hid_t space = H5Screate_simple(1, dims, NULL); - hid_t gid_root = H5Gopen(file, "/", H5P_DEFAULT); - if(!H5Aexists(gid_root, strAttr.c_str())) - { - hid_t tid_root_attr = CreateRootAttrType(file); - hid_t aid_root = H5Acreate(gid_root, strAttr.c_str(), tid_root_attr, space, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid_root, tid_root_attr, &header); - ret = H5Aclose(aid_root); - H5Tclose(tid_root_attr); - } - - ret = H5Gclose(gid_root); - ret = H5Sclose(space); - } - - return 0; -} - -// Author & Date: Ehsan Azar Nov 17, 2012 -// Purpose: Create and add root -// Inputs: -// pFile - the source file -// file - the destination file -// isHdr - NEV header -// Outputs: -// Returns 0 on success, error code otherwise -int AddRoot(FILE * pFile, hid_t file, NevHdr & isHdr) -{ - BmiRootAttr_t header; - memset(&header, 0, sizeof(header)); - header.nMajorVersion = 1; - strncpy(header.szApplication, isHdr.szApplication, 32); - strncpy(header.szComment, isHdr.szComment, 256); - header.nGroupCount = 1; - { - TIMSTM ts; - memset(&ts, 0, sizeof(ts)); - SYSTEMTIME & st = isHdr.isAcqTime; - sprintf((char *)&ts, "%04hd-%02hd-%02hd %02hd:%02hd:%02hd.%06d", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, - st.wSecond, st.wMilliseconds * 1000); - ts.chNull = '\0'; - strncpy(header.szDate, (char *)&ts, sizeof(ts)); - } - - return AddRoot(file, header); -} - -// Author & Date: Ehsan Azar Nov 17, 2012 -// Purpose: Create and add root -// Inputs: -// szSrcFile - source file name -// pFile - the source file -// file - the destination file -// isHdr - Nsx 2.1 header -// Outputs: -// Returns 0 on success, error code otherwise -int AddRoot(const char * szSrcFile, FILE * pFile, hid_t file, Nsx21Hdr & isHdr) -{ - BmiRootAttr_t header; - memset(&header, 0, sizeof(header)); - header.nMajorVersion = 1; - strncpy(header.szApplication, isHdr.szGroup, 16); - char szComment[] = ""; // Old format does not have a comment - strncpy(header.szComment, szComment, 1024); - header.nGroupCount = 1; - TIMSTM ts; - memset(&ts, 0, sizeof(ts)); -#ifdef WIN32 - WIN32_FILE_ATTRIBUTE_DATA fattr; - if (!GetFileAttributesEx(szSrcFile, GetFileExInfoStandard, &fattr)) - { - printf("Cannot get file attributes\n"); - return 1; - } - SYSTEMTIME st; - FileTimeToSystemTime(&fattr.ftCreationTime, &st); - sprintf((char *)&ts, "%04hd-%02hd-%02hd %02hd:%02hd:%02hd.%06d", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, - st.wSecond, st.wMilliseconds * 1000); - ts.chNull = '\0'; - header.szDate = (char *)&ts; -#else - struct stat st; - if (stat(szSrcFile, &st)) - { - printf("Cannot get file attributes\n"); - return 1; - } -//unused time_t t = st.st_mtime; - // TODO: use strftime to convert to st -#endif - - return AddRoot(file, header); -} - -// Author & Date: Ehsan Azar Nov 17, 2012 -// Purpose: Create and add root -// Inputs: -// pFile - the source file -// file - the destination file -// isHdr - NSx 2.2 header -// Outputs: -// Returns 0 on success, error code otherwise -int AddRoot(FILE * pFile, hid_t file, Nsx22Hdr & isHdr) -{ - BmiRootAttr_t header; - memset(&header, 0, sizeof(header)); - header.nMajorVersion = 1; - strncpy(header.szApplication, isHdr.szGroup, 16); - strncpy(header.szComment, isHdr.szComment, 256); - header.nGroupCount = 1; - { - TIMSTM ts; - memset(&ts, 0, sizeof(ts)); - SYSTEMTIME & st = isHdr.isAcqTime; - sprintf((char *)&ts, "%04hd-%02hd-%02hd %02hd:%02hd:%02hd.%06d", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, - st.wSecond, st.wMilliseconds * 1000); - ts.chNull = '\0'; - strncpy(header.szDate, (char *)&ts, sizeof(ts)); - } - - return AddRoot(file, header); -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Convert NEV -// Inputs: -// pFile - the source file -// file - the destination file -// Outputs: -// Returns 0 on success, error code otherwise -int ConvertNev(FILE * pFile, hid_t file) -{ - herr_t ret; - - NevHdr isHdr; - fseeko(pFile, 0, SEEK_SET); // read header from beginning of file - if (fread(&isHdr, sizeof(isHdr), 1, pFile) != 1) - { - printf("Cannot read source file header\n"); - return 1; - } - - // Add root attribute - if (AddRoot(pFile, file, isHdr)) - return 1; - - uint16_t nSpikeLength = 48; - hid_t tid_spike = -1; - hid_t tid_dig = -1; - hid_t tid_comment = -1; - hid_t tid_tracking[cbMAXTRACKOBJ]; - int size_tracking[cbMAXTRACKOBJ] = {0}; - hid_t tid_synch = -1; - hid_t tid_sampling_attr = -1; - hid_t tid_filt_attr = -1; - - // 21, 22, 23 flat file verion number - uint32_t nVer = isHdr.byFileRevMajor * 10 + isHdr.byFileRevMinor; - - nSpikeLength = (isHdr.dwBytesPerPacket - 8) / 2; - - char * szMapFile = NULL; - BmiTrackingAttr_t trackingAttr[cbMAXTRACKOBJ]; - memset(trackingAttr, 0, sizeof(trackingAttr)); - BmiSynchAttr_t synchAttr; - memset(&synchAttr, 0, sizeof(synchAttr)); - BmiSamplingAttr_t samplingAttr[cbNUM_ANALOG_CHANS]; - memset(samplingAttr, 0, sizeof(samplingAttr)); - BmiChanAttr_t chanAttr[cbMAXCHANS]; - memset(chanAttr, 0, sizeof(chanAttr)); - BmiChanExtAttr_t chanExtAttr[cbNUM_ANALOG_CHANS]; - memset(chanExtAttr, 0, sizeof(chanExtAttr)); - BmiFiltAttr_t filtAttr[cbNUM_ANALOG_CHANS]; - memset(filtAttr, 0, sizeof(filtAttr)); - // NEV provides Ext1 additional header - BmiChanExt1Attr_t chanExt1Attr[cbNUM_ANALOG_CHANS]; - memset(chanExt1Attr, 0, sizeof(chanExt1Attr)); - // Read the header to fill channel attributes - for (uint32_t i = 0; i < isHdr.dwNumOfExtendedHeaders; ++i) - { - NevExtHdr isExtHdr; - if (fread(&isExtHdr, sizeof(isExtHdr), 1, pFile) != 1) - { - printf("Invalid source file header\n"); - return 1; - } - if (0 == strncmp(isExtHdr.achPacketID, "NEUEVWAV", sizeof(isExtHdr.achPacketID))) - { - if (isExtHdr.neuwav.wave_samples != 0) - nSpikeLength = isExtHdr.neuwav.wave_samples; - int id = isExtHdr.id; - if (id == 0 || id > cbNUM_ANALOG_CHANS) - { - printf("Invalid channel ID in source file header\n"); - return 1; - } - id--; // make it zero-based - chanAttr[id].id = isExtHdr.id; - // Currently all spikes are sampled at clock rate - samplingAttr[id].fClock = float(isHdr.dwTimeStampResolutionHz); - samplingAttr[id].fSampleRate = float(isHdr.dwSampleResolutionHz); - samplingAttr[id].nSampleBits = isExtHdr.neuwav.wave_bytes * 8; - - chanExtAttr[id].phys_connector = isExtHdr.neuwav.phys_connector; - chanExtAttr[id].connector_pin = isExtHdr.neuwav.connector_pin; - chanExtAttr[id].dFactor = isExtHdr.neuwav.digital_factor; - - chanExt1Attr[id].energy_thresh = isExtHdr.neuwav.energy_thresh; - chanExt1Attr[id].high_thresh = isExtHdr.neuwav.high_thresh; - chanExt1Attr[id].low_thresh = isExtHdr.neuwav.low_thresh; - chanExt1Attr[id].sortCount = isExtHdr.neuwav.sorted_count; - - } - else if (0 == strncmp(isExtHdr.achPacketID, "NEUEVLBL", sizeof(isExtHdr.achPacketID))) - { - int id = isExtHdr.id; - if (id == 0 || id > cbNUM_ANALOG_CHANS) - { - printf("Invalid channel ID in source file header\n"); - return 1; - } - id--; - strncpy(chanAttr[id].szLabel, isExtHdr.neulabel.label, 16); - } - else if (0 == strncmp(isExtHdr.achPacketID, "NEUEVFLT", sizeof(isExtHdr.achPacketID))) - { - int id = isExtHdr.id; - if (id == 0 || id > cbNUM_ANALOG_CHANS) - { - printf("Invalid channel ID in source file header\n"); - return 1; - } - id--; - filtAttr[id].hpfreq = isExtHdr.neuflt.hpfreq; - filtAttr[id].hporder = isExtHdr.neuflt.hporder; - filtAttr[id].hptype = isExtHdr.neuflt.hptype; - filtAttr[id].lpfreq = isExtHdr.neuflt.lpfreq; - filtAttr[id].lporder = isExtHdr.neuflt.lporder; - filtAttr[id].lptype = isExtHdr.neuflt.lptype; - } - else if (0 == strncmp(isExtHdr.achPacketID, "VIDEOSYN", sizeof(isExtHdr.achPacketID))) - { - synchAttr.id = isExtHdr.id; - synchAttr.fFps = isExtHdr.videosyn.fFps; - strncpy(synchAttr.szLabel, isExtHdr.videosyn.label, 16); - } - else if (0 == strncmp(isExtHdr.achPacketID, "TRACKOBJ", sizeof(isExtHdr.achPacketID))) - { - int id = isExtHdr.trackobj.trackID; // 1-based - if (id == 0 || id > cbMAXTRACKOBJ) - { - printf("Invalid trackable ID in source file header\n"); - return 1; - } - id--; - trackingAttr[id].type = isExtHdr.id; // 0-based type - trackingAttr[id].trackID = isExtHdr.trackobj.trackID; - trackingAttr[id].maxPoints = isExtHdr.trackobj.maxPoints; - strncpy(trackingAttr[id].szLabel, isExtHdr.trackobj.label, 16); - } - else if (0 == strncmp(isExtHdr.achPacketID, "MAPFILE", sizeof(isExtHdr.achPacketID))) - { - szMapFile = _strdup(isExtHdr.mapfile.label); - } - else if (0 == strncmp(isExtHdr.achPacketID, "DIGLABEL", sizeof(isExtHdr.achPacketID))) - { - int id = 0; - if (isExtHdr.diglabel.mode == 0) - { - id = cbFIRST_SERIAL_CHAN; // Serial - chanAttr[id].id = cbFIRST_SERIAL_CHAN + 1; - } - else if (isExtHdr.diglabel.mode == 1) - { - id = cbFIRST_DIGIN_CHAN; // Digital - chanAttr[id].id = cbFIRST_DIGIN_CHAN + 1; - } else { - printf("Invalid digital input mode in source file header\n"); - return 1; - } - strncpy(chanAttr[id].szLabel, isExtHdr.diglabel.label, 16); - } else { - printf("Unknown header (%7s) in the source file\n", isExtHdr.achPacketID); - } - } // end for (uint32_t i = 0 - - uint32_t nChannelOffset = 0; - uint32_t nDigChannelOffset = 0; - uint32_t nSerChannelOffset = 0; - - hsize_t dims[1] = {1}; - hid_t space_attr = H5Screate_simple(1, dims, NULL); - - // Add channel group - { - hid_t gid_channel = -1; - if (H5Lexists(file, "channel", H5P_DEFAULT)) - gid_channel = H5Gopen(file, "channel", H5P_DEFAULT); - else - gid_channel = H5Gcreate(file, "channel", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - if (szMapFile != NULL) - { - hid_t tid_attr_map_str = H5Tcopy(H5T_C_S1); - ret = H5Tset_size(tid_attr_map_str, 1024); - hid_t aid = H5Acreate(gid_channel, "MapFile", tid_attr_map_str, space_attr, H5P_DEFAULT, H5P_DEFAULT); - char szMapFileRecord[1024] = {0}; - strcpy(szMapFileRecord, szMapFile); - ret = H5Awrite(aid, tid_attr_map_str, szMapFileRecord); - ret = H5Aclose(aid); - H5Tclose(tid_attr_map_str); - } - if (g_bAppend) - { - bool bExists = false; - // Find the last place to append to - do { - nChannelOffset++; - std::string strLabel = "channel"; - char szNum[7]; - sprintf(szNum, "%05u", nChannelOffset); - strLabel += szNum; - bExists = (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT) != 0); - } while(bExists); - nChannelOffset--; - do { - nDigChannelOffset++; - std::string strLabel = "digital"; - char szNum[7]; - sprintf(szNum, "%05u", nDigChannelOffset); - strLabel += szNum; - bExists = (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT) != 0); - } while(bExists); - nDigChannelOffset--; - do { - nSerChannelOffset++; - std::string strLabel = "serial"; - char szNum[7]; - sprintf(szNum, "%05u", nSerChannelOffset); - strLabel += szNum; - bExists = (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT) != 0); - } while(bExists); - nSerChannelOffset--; - } - tid_spike = CreateSpike16Type(gid_channel, nSpikeLength); - hid_t tid_chan_attr = CreateChanAttrType(gid_channel); - hid_t tid_chanext_attr = CreateChanExtAttrType(gid_channel); - hid_t tid_chanext1_attr = CreateChanExt1AttrType(gid_channel); - tid_sampling_attr = CreateSamplingAttrType(gid_channel); - tid_filt_attr = CreateFiltAttrType(gid_channel); - for (int i = 0; i < cbNUM_ANALOG_CHANS; ++i) - { - if (chanAttr[i].id == 0) - continue; - char szNum[7]; - std::string strLabel = "channel"; - sprintf(szNum, "%05u", chanAttr[i].id + nChannelOffset); - strLabel += szNum; - hid_t gid = -1; - if (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gid_channel, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gid_channel, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - // Basic channel attributes - if (!H5Aexists(gid, "BmiChan")) - { - hid_t aid = H5Acreate(gid, "BmiChan", tid_chan_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chan_attr, &chanAttr[i]); - ret = H5Aclose(aid); - } - - // Extra channel attributes - if (!H5Aexists(gid, "BmiChanExt")) - { - hid_t aid = H5Acreate(gid, "BmiChanExt", tid_chanext_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chanext_attr, &chanExtAttr[i]); - ret = H5Aclose(aid); - } - - // Additional extra channel attributes - if (!H5Aexists(gid, "BmiChanExt1")) - { - hid_t aid = H5Acreate(gid, "BmiChanExt1", tid_chanext1_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chanext1_attr, &chanExt1Attr[i]); - ret = H5Aclose(aid); - } - - ret = H5Gclose(gid); - } - ret = H5Tclose(tid_chanext1_attr); - ret = H5Tclose(tid_chanext_attr); - - // Add digital and serial channel and their attributes - { - uint16_t id = 1 + 0; - char szNum[7]; - std::string strLabel = "digital"; - sprintf(szNum, "%05u", id + nDigChannelOffset); - strLabel += szNum; - tid_dig = CreateDig16Type(gid_channel); - hid_t gid = -1; - if (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gid_channel, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gid_channel, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - if (chanAttr[cbFIRST_DIGIN_CHAN].id) - { - if (!H5Aexists(gid, "BmiChan")) - { - hid_t aid = H5Acreate(gid, "BmiChan", tid_chan_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chan_attr, &chanAttr[cbFIRST_DIGIN_CHAN]); - ret = H5Aclose(aid); - } - } - ret = H5Gclose(gid); - } - - // Add digital and serial channel and their attributes - { - uint16_t id = 1 + 0; - char szNum[7]; - std::string strLabel = "serial"; - sprintf(szNum, "%05u", id + nSerChannelOffset); - strLabel += szNum; - hid_t gid = -1; - if (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gid_channel, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gid_channel, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - if (chanAttr[cbFIRST_SERIAL_CHAN].id) - { - if (!H5Aexists(gid, "BmiChan")) - { - hid_t aid = H5Acreate(gid, "BmiChan", tid_chan_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chan_attr, &chanAttr[cbFIRST_SERIAL_CHAN]); - ret = H5Aclose(aid); - } - } - ret = H5Gclose(gid); - } - ret = H5Tclose(tid_chan_attr); - - ret = H5Gclose(gid_channel); - } - - bool bHasVideo = false; - // Add video group - if (nVer >= 23) - { - hid_t gid_video = -1; - if (H5Lexists(file, "video", H5P_DEFAULT)) - gid_video = H5Gopen(file, "video", H5P_DEFAULT); - else - gid_video = H5Gcreate(file, "video", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - hid_t tid_tracking_attr = CreateTrackingAttrType(gid_video); - hid_t tid_synch_attr = CreateSynchAttrType(gid_video); - tid_synch = CreateSynchType(gid_video); - // Add synchronization groups - if (synchAttr.szLabel != NULL) // Warning: Always true! - { - bHasVideo = true; - char szNum[7]; - std::string strLabel = "synch"; - sprintf(szNum, "%05u", synchAttr.id); - strLabel += szNum; - hid_t gid = -1; - if (H5Lexists(gid_video, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gid_video, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gid_video, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - if (!H5Aexists(gid, "BmiSynch")) - { - hid_t aid = H5Acreate(gid, "BmiSynch", tid_synch_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_synch_attr, &synchAttr); - ret = H5Aclose(aid); - ret = H5Gclose(gid); - } - } - // Add tracking groups - for (int i = 0; i < cbMAXTRACKOBJ; ++i) - { - tid_tracking[i] = -1; - if (trackingAttr[i].szLabel != NULL) // Warning: Always true! - { - bHasVideo = true; - int dim = 2, width = 2; - switch (trackingAttr[i].type) - { - case cbTRACKOBJ_TYPE_2DMARKERS: - case cbTRACKOBJ_TYPE_2DBLOB: - case cbTRACKOBJ_TYPE_2DBOUNDARY: - dim = 2; - width = 2; - break; - case cbTRACKOBJ_TYPE_3DMARKERS: - dim = 3; - width = 2; - break; - case cbTRACKOBJ_TYPE_1DSIZE: - dim = 1; - width = 4; - break; - default: - // The defualt is already set - break; - } - // The only fixed length now is the case for single point tracking - tid_tracking[i] = CreateTrackingType(gid_video, dim, width); - size_tracking[i] = dim * width; // Size of each tracking element in bytes - char szNum[7]; - std::string strLabel = "tracking"; - sprintf(szNum, "%05u", trackingAttr[i].trackID); - strLabel += szNum; - - hid_t gid = -1; - if (H5Lexists(gid_video, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gid_video, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gid_video, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - if (!H5Aexists(gid, "BmiTracking")) - { - hid_t aid = H5Acreate(gid, "BmiTracking", tid_tracking_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_tracking_attr, &trackingAttr[i]); - ret = H5Aclose(aid); - ret = H5Gclose(gid); - } - } // end if (trackingAttr[i].szLabel - } // end for (int i = 0 - ret = H5Tclose(tid_tracking_attr); - ret = H5Tclose(tid_synch_attr); - ret = H5Gclose(gid_video); - } - // Comment group - if (nVer >= 23) - { - hid_t gid_comment = H5Gcreate(file, "comment", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - tid_comment = CreateCommentType(gid_comment); - { - // NeuroMotive charset is fixed - hid_t aid = H5Acreate(gid_comment, "NeuroMotiveCharset", H5T_NATIVE_UINT8, space_attr, H5P_DEFAULT, H5P_DEFAULT); - uint8_t charset = 255; - ret = H5Awrite(aid, H5T_NATIVE_UINT8, &charset); - ret = H5Aclose(aid); - - hid_t gid; - if (bHasVideo) - { - gid = H5Gcreate(gid_comment, "comment00256", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - aid = H5Acreate(gid, "Charset", H5T_NATIVE_UINT8, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, H5T_NATIVE_UINT8, &charset); - ret = H5Aclose(aid); - ret = H5Gclose(gid); - } - charset = 0; // Add group for normal comments right here - gid = H5Gcreate(gid_comment, "comment00001", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - aid = H5Acreate(gid, "Charset", H5T_NATIVE_UINT8, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, H5T_NATIVE_UINT8, &charset); - ret = H5Aclose(aid); - ret = H5Gclose(gid); - } - ret = H5Gclose(gid_comment); - } - ret = H5Sclose(space_attr); - - // --------------------------------------------------------------------------------------------- - // Done with reading headers - // Add packet tables - { - size_t chunk_size = CHUNK_SIZE_EVENT; - int compression = -1; // TODO: use options to add compression - hid_t ptid_spike[cbNUM_ANALOG_CHANS]; - for (int i = 0; i < cbNUM_ANALOG_CHANS; ++i) - ptid_spike[i] = -1; - hid_t ptid_serial = -1, ptid_digital = -1, ptid_synch = -1; - hid_t ptid_comment[256]; - for (int i = 0; i < 256; ++i) - ptid_comment[i] = -1; - hid_t ptid_tracking[cbMAXTRACKOBJ]; - for (int i = 0; i < cbMAXTRACKOBJ; ++i) - ptid_tracking[i] = -1; - NevData nevData; - fseeko(pFile, isHdr.dwStartOfData, SEEK_SET); - size_t nGot = fread(&nevData, isHdr.dwBytesPerPacket, 1, pFile); - if (nGot != 1) - { - perror("Source file is empty or invalid\n"); - return 1; - } - do { - - if (nevData.wPacketID >= 1 && nevData.wPacketID <= cbNUM_ANALOG_CHANS) // found spike data - { - if (!g_bNoSpikes) - { - int id = nevData.wPacketID; // 1-based - if (ptid_spike[id - 1] < 0) - { - char szNum[7]; - std::string strLabel = "/channel/channel"; - sprintf(szNum, "%05u", id + nChannelOffset); - strLabel += szNum; - hid_t gid; - if(H5Lexists(file, strLabel.c_str(), H5P_DEFAULT)) - { - gid = H5Gopen(file, strLabel.c_str(), H5P_DEFAULT); - } else { - printf("Creating %s without attributes\n", strLabel.c_str()); - gid = H5Gcreate(file, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - } - ptid_spike[id - 1] = H5PTcreate_fl(gid, "spike_set", tid_spike, chunk_size, compression); - hid_t dsid = H5Dopen(gid, "spike_set", H5P_DEFAULT); - ret = H5Gclose(gid); - hid_t space_attr = H5Screate_simple(1, dims, NULL); - - // Add data sampling attribute - hid_t aid = H5Acreate(dsid, "Sampling", tid_sampling_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_sampling_attr, &samplingAttr[id - 1]); - ret = H5Aclose(aid); - // Add data filtering attribute - aid = H5Acreate(dsid, "Filter", tid_filt_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_filt_attr, &filtAttr[id - 1]); - ret = H5Aclose(aid); - - ret = H5Sclose(space_attr); - } - BmiSpike16_t spk; - spk.dwTimestamp = nevData.dwTimestamp; - spk.res = nevData.spike.res; - spk.unit = nevData.spike.unit; - for (int i = 0; i < nSpikeLength; ++i) - spk.wave[i] = nevData.spike.wave[i]; - ret = H5PTappend(ptid_spike[id - 1], 1, &spk); - } // end if (!g_bNoSpikes - } else { - switch (nevData.wPacketID) - { - case 0: // found a digital or serial event - if (!(nevData.digital.byInsertionReason & 1)) - { - // Other digital events are not implemented in NSP yet - printf("Unkown digital event (%u) dropped\n", nevData.digital.byInsertionReason); - break; - } - if (nevData.digital.byInsertionReason & 128) // If bit 7 is set it is serial - { - if (ptid_serial < 0) - { - uint16_t id = 1 + 0; - char szNum[7]; - std::string strLabel = "/channel/serial"; - sprintf(szNum, "%05u", id + nSerChannelOffset); - strLabel += szNum; - hid_t gid = H5Gopen(file, strLabel.c_str(), H5P_DEFAULT); - ptid_serial = H5PTcreate_fl(gid, "serial_set", tid_dig, chunk_size, compression); - H5Gclose(gid); - } - BmiDig16_t dig; - dig.dwTimestamp = nevData.dwTimestamp; - dig.value = nevData.digital.wDigitalValue; - ret = H5PTappend(ptid_serial, 1, &dig); - } else { - if (ptid_digital < 0) - { - uint16_t id = 1 + 0; - char szNum[7]; - std::string strLabel = "/channel/digital"; - sprintf(szNum, "%05u", id + nDigChannelOffset); - strLabel += szNum; - hid_t gid = H5Gopen(file, strLabel.c_str(), H5P_DEFAULT); - ptid_digital = H5PTcreate_fl(gid, "digital_set", tid_dig, chunk_size, compression); - H5Gclose(gid); - } - BmiDig16_t dig; - dig.dwTimestamp = nevData.dwTimestamp; - dig.value = nevData.digital.wDigitalValue; - ret = H5PTappend(ptid_digital, 1, &dig); - } - break; - case 0xFFFF: // found a comment event - { - int id = nevData.comment.charset; // 0-based - if (ptid_comment[id] < 0) - { - char szNum[7]; - std::string strLabel = "/comment/comment"; - sprintf(szNum, "%05u", id + 1); - strLabel += szNum; - hid_t gid; - if(H5Lexists(file, strLabel.c_str(), H5P_DEFAULT)) - { - gid = H5Gopen(file, strLabel.c_str(), H5P_DEFAULT); - } else { - gid = H5Gcreate(file, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - { - hsize_t dims[1] = {1}; - hid_t space_attr = H5Screate_simple(1, dims, NULL); - hid_t aid = H5Acreate(gid, "Charset", H5T_NATIVE_UINT8, space_attr, H5P_DEFAULT, H5P_DEFAULT); - uint8_t charset = nevData.comment.charset; - ret = H5Awrite(aid, H5T_NATIVE_UINT8, &charset); - ret = H5Aclose(aid); - ret = H5Sclose(space_attr); - } - } - - ptid_comment[id] = H5PTcreate_fl(gid, "comment_set", tid_comment, chunk_size, compression); - H5Gclose(gid); - } - BmiComment_t cmt; - cmt.dwTimestamp = nevData.dwTimestamp; - cmt.data = nevData.comment.data; - cmt.flags = nevData.comment.flags; - strncpy(cmt.szComment, nevData.comment.comment, std::min((std::size_t)BMI_COMMENT_LEN, sizeof(nevData.comment.comment))); - ret = H5PTappend(ptid_comment[id], 1, &cmt); - } - break; - case 0xFFFE: // found a synchronization event - { - int id = nevData.synch.id; // 0-based - if (id != 0) - { - printf("Unsupported synchronization source dropped\n"); - break; - } - if (ptid_synch < 0) - { - char szNum[7]; - std::string strLabel = "/video/synch"; - sprintf(szNum, "%05u", id + 1); - strLabel += szNum; - hid_t gid; - if(H5Lexists(file, strLabel.c_str(), H5P_DEFAULT)) - { - gid = H5Gopen(file, strLabel.c_str(), H5P_DEFAULT); - } else { - printf("Creating %s without attributes\n", strLabel.c_str()); - gid = H5Gcreate(file, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - } - ptid_synch = H5PTcreate_fl(gid, "synch_set", tid_synch, chunk_size, compression); - H5Gclose(gid); - } - g_synch.dwTimestamp = nevData.dwTimestamp; - g_synch.etime = nevData.synch.etime; - g_synch.frame = nevData.synch.frame; - g_synch.split = nevData.synch.split; - ret = H5PTappend(ptid_synch, 1, &g_synch); - } - break; - case 0xFFFD: // found a video tracking event - { - int id = nevData.track.nodeID; // 0-based - if (id >= cbMAXTRACKOBJ) - { - printf("Invalid tracking packet dropped\n"); - break; - } - if (ptid_tracking[id] < 0) - { - char szNum[7]; - std::string strLabel = "/video/tracking"; - sprintf(szNum, "%05u", id + 1); - strLabel += szNum; - hid_t gid; - if(H5Lexists(file, strLabel.c_str(), H5P_DEFAULT)) - { - gid = H5Gopen(file, strLabel.c_str(), H5P_DEFAULT); - } else { - printf("Creating %s without attributes\n", strLabel.c_str()); - gid = H5Gcreate(file, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - } - if (tid_tracking[id] < 0) - { - printf("Creating tracking set with undefined type\n"); - hid_t gid_video = H5Gopen(file, "/video", H5P_DEFAULT); - tid_tracking[id] = CreateTrackingType(gid_video, 2, 2); - ret = H5Gclose(gid_video); - } - ptid_tracking[id] = H5PTcreate_fl(gid, "tracking_set", tid_tracking[id], chunk_size, compression); - H5Gclose(gid); - } - - // Flatten tracking array - BmiTracking_fl_t tr; - tr.dwTimestamp = nevData.dwTimestamp; - tr.nodeCount = nevData.track.nodeCount; - tr.parentID = nevData.track.parentID; - // Use last synch packet to augment the data - tr.etime = g_synch.etime; - if (size_tracking[id] > 0) - { - for (int i = 0; i < nevData.track.coordsLength; ++i) - { - memcpy(tr.coords, (char *)&nevData.track.coords[0] + i * size_tracking[id], size_tracking[id]); - ret = H5PTappend(ptid_tracking[id], 1, &tr); - } - } - } - break; - default: - if (nevData.wPacketID <= 2048) - printf("Unexpected spike channel (%u) dropped\n", nevData.wPacketID); - else - printf("Unknown packet type (%u) dropped\n", nevData.wPacketID); - break; - } - } - // Read more packets - nGot = fread(&nevData, isHdr.dwBytesPerPacket, 1, pFile); - } while (nGot == 1); - - // H5Close SIGSEVs if I do not close the PT manually! - for (int i = 0; i < cbNUM_ANALOG_CHANS; ++i) { - if (ptid_spike[i] >= 0) { - H5PTclose(ptid_spike[i]); - } - } - } - - // - // We are going to call H5Close so no need to close what is open at this stage - // - return 0; -} - -// Author & Date: Ehsan Azar Nov 17, 2012 -// Purpose: Convert NSx2.1 -// Inputs: -// szSrcFile - source file name -// pFile - the source file -// file - the destination file -// Outputs: -// Returns 0 on success, error code otherwise -int ConvertNSx21(const char * szSrcFile, FILE * pFile, hid_t file) -{ - herr_t ret; - Nsx21Hdr isHdr; - // Read the header - fseeko(pFile, 0, SEEK_SET); // read header from beginning of file - fread(&isHdr, sizeof(isHdr), 1, pFile); - - if (isHdr.cnChannels > cbNUM_ANALOG_CHANS) - { - printf("Invalid number of channels in source file header\n"); - return 1; - } - - BmiChanAttr_t chanAttr[cbNUM_ANALOG_CHANS]; - memset(chanAttr, 0, sizeof(chanAttr)); - BmiSamplingAttr_t samplingAttr[cbNUM_ANALOG_CHANS]; - memset(samplingAttr, 0, sizeof(samplingAttr)); - - // Add root attribute - if (AddRoot(szSrcFile, pFile, file, isHdr)) - return 1; - - hid_t ptid_chan[cbNUM_ANALOG_CHANS]; - { - size_t chunk_size = CHUNK_SIZE_CONTINUOUS; - int compression = -1; // TODO: use options to add compression - - hsize_t dims[1] = {1}; - hid_t space_attr = H5Screate_simple(1, dims, NULL); - - hid_t gid_channel = -1; - if (H5Lexists(file, "channel", H5P_DEFAULT)) - gid_channel = H5Gopen(file, "channel", H5P_DEFAULT); - else - gid_channel = H5Gcreate(file, "channel", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - hid_t tid_chan_attr = CreateChanAttrType(gid_channel); - hid_t tid_sampling_attr = CreateSamplingAttrType(gid_channel); - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - char szNum[7]; - uint32_t id; // 1-based - if (fread(&id, sizeof(uint32_t), 1, pFile) != 1) - { - printf("Invalid header in source file\n"); - return 1; - } - chanAttr[i].id = id; - std::string strLabel = "chan"; - sprintf(szNum, "%u", id); - strLabel += szNum; - strncpy(chanAttr[i].szLabel, strLabel.c_str(), 64); - samplingAttr[i].fClock = 30000; - // FIXME: This might be incorrect for really old file recordings - // TODO: search the file to see if 14 is more accurate - samplingAttr[i].nSampleBits = 16; - samplingAttr[i].fSampleRate = float(30000.0) / isHdr.nPeriod; - - uint32_t nChannelOffset = 0; - if (g_bAppend) - { - bool bExists = false; - // Find the last place to append to - do { - nChannelOffset++; - std::string strLabel = "channel"; - char szNum[7]; - sprintf(szNum, "%05u", nChannelOffset); - strLabel += szNum; - bExists = (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT) != 0); - } while(bExists); - nChannelOffset--; - } - - strLabel = "channel"; - sprintf(szNum, "%05u", id + nChannelOffset); - strLabel += szNum; - hid_t gid = -1; - if (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gid_channel, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gid_channel, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - // Basic channel attributes - hid_t aid = H5Acreate(gid, "BmiChan", tid_chan_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chan_attr, &chanAttr[i]); - ret = H5Aclose(aid); - - // If need to go one level deeper - if (g_nCombine > 0) - { - hid_t gidParent = gid; - sprintf(szNum, "%05u", g_nCombine); - strLabel = szNum; - if (H5Lexists(gidParent, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gidParent, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gidParent, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Gclose(gidParent); - } - - ptid_chan[i] = H5PTcreate_fl(gid, "continuous_set", H5T_NATIVE_INT16, chunk_size, compression); - - hid_t dsid = H5Dopen(gid, "continuous_set", H5P_DEFAULT); - ret = H5Gclose(gid); - // Add data start clock attribute - aid = H5Acreate(dsid, "StartClock", H5T_NATIVE_UINT32, space_attr, H5P_DEFAULT, H5P_DEFAULT); - uint32_t nStartTime = 0; // 2.1 does not have paused headers - ret = H5Awrite(aid, H5T_NATIVE_UINT32, &nStartTime); - ret = H5Aclose(aid); - // Add data sampling attribute - aid = H5Acreate(dsid, "Sampling", tid_sampling_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_sampling_attr, &samplingAttr[i]); - ret = H5Aclose(aid); - - ret = H5Dclose(dsid); - } // end for (uint32_t i = 0 - ret = H5Tclose(tid_sampling_attr); - ret = H5Tclose(tid_chan_attr); - ret = H5Gclose(gid_channel); - ret = H5Sclose(space_attr); - } - - int count = 0; - int16_t anDataBufferCache[cbNUM_ANALOG_CHANS][CHUNK_SIZE_CONTINUOUS]; - int16_t anDataBuffer[cbNUM_ANALOG_CHANS]; - size_t nGot = fread(anDataBuffer, sizeof(int16_t), isHdr.cnChannels, pFile); - if (nGot != isHdr.cnChannels) - { - perror("Source file is empty or invalid\n"); - return 1; - } - do { - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - anDataBufferCache[i][count] = anDataBuffer[i]; - } - count++; - if (count == CHUNK_SIZE_CONTINUOUS) - { - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - ret = H5PTappend(ptid_chan[i], count, &anDataBufferCache[i][0]); - } - count = 0; - } - nGot = fread(anDataBuffer, sizeof(int16_t), isHdr.cnChannels, pFile); - } while (nGot == isHdr.cnChannels); - - // Write out the remaining chunk - if (count > 0) - { - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - ret = H5PTappend(ptid_chan[i], count, &anDataBufferCache[i][0]); - } - } - - // - // We are going to call H5Close so no need to close what is open at this stage - // - return 0; -} - -// Author & Date: Ehsan Azar Nov 17, 2012 -// Purpose: Convert NSx2.2 -// Inputs: -// pFile - the source file -// file - the destination file -// Outputs: -// Returns 0 on success, error code otherwise -int ConvertNSx22(FILE * pFile, hid_t file) -{ - herr_t ret; - Nsx22Hdr isHdr; - // Read the header - fseeko(pFile, 0, SEEK_SET); // read header from beginning of file - fread(&isHdr, sizeof(isHdr), 1, pFile); - //UINT64 dataStart = isHdr.nBytesInHdrs + sizeof(Nsx22DataHdr); - - if (isHdr.cnChannels > cbNUM_ANALOG_CHANS) - { - printf("Invalid number of channels in source file header\n"); - return 1; - } - - BmiFiltAttr_t filtAttr[cbNUM_ANALOG_CHANS]; - memset(filtAttr, 0, sizeof(filtAttr)); - BmiSamplingAttr_t samplingAttr[cbNUM_ANALOG_CHANS]; - memset(samplingAttr, 0, sizeof(samplingAttr)); - BmiChanAttr_t chanAttr[cbNUM_ANALOG_CHANS]; - memset(chanAttr, 0, sizeof(chanAttr)); - BmiChanExtAttr_t chanExtAttr[cbNUM_ANALOG_CHANS]; - memset(chanExtAttr, 0, sizeof(chanExtAttr)); - BmiChanExt2Attr_t chanExt2Attr[cbNUM_ANALOG_CHANS]; - memset(chanExt2Attr, 0, sizeof(chanExt2Attr)); - - // Add root attribute - if (AddRoot(pFile, file, isHdr)) - return 1; - - // Read extra headers - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - Nsx22ExtHdr isExtHdr; - if (fread(&isExtHdr, sizeof(isExtHdr), 1, pFile) != 1) - { - printf("Invalid source file header\n"); - return 1; - } - if (0 != strncmp(isExtHdr.achExtHdrID, "CC", sizeof(isExtHdr.achExtHdrID))) - { - printf("Invalid source file extended header\n"); - return 1; - } - int id = isExtHdr.id; - if (id == 0 || id > cbNUM_ANALOG_CHANS) - { - printf("Invalid channel ID in source file header\n"); - return 1; - } - chanAttr[i].id = isExtHdr.id; - samplingAttr[i].fClock = float(isHdr.nResolution); - samplingAttr[i].fSampleRate = float(isHdr.nResolution) / float(isHdr.nPeriod); - samplingAttr[i].nSampleBits = 16; - - chanExtAttr[i].phys_connector = isExtHdr.phys_connector; - chanExtAttr[i].connector_pin = isExtHdr.connector_pin; - uint64_t anarange = int64_t(isExtHdr.anamax) - int64_t(isExtHdr.anamin); - uint64_t digrange = int64_t(isExtHdr.digmax) - int64_t(isExtHdr.digmin); - if (strncmp(isExtHdr.anaunit, "uV", 2) == 0) - { - chanExtAttr[i].dFactor = uint32_t((anarange * int64_t(1E3)) / digrange); - } - else if (strncmp(isExtHdr.anaunit, "mV", 2) == 0) - { - chanExtAttr[i].dFactor = uint32_t((anarange * int64_t(1E6)) / digrange); - } - else if (strncmp(isExtHdr.anaunit, "V", 2) == 0) - { - chanExtAttr[i].dFactor = uint32_t((anarange * int64_t(1E9)) / digrange); - } else { - printf("Unknown analog unit for channel %u, uV used\n", isExtHdr.id); - chanExtAttr[i].dFactor = uint32_t((anarange * int64_t(1E3)) / digrange); - } - filtAttr[i].hpfreq = isExtHdr.hpfreq; - filtAttr[i].hporder = isExtHdr.hporder; - filtAttr[i].hptype = isExtHdr.hptype; - filtAttr[i].lpfreq = isExtHdr.lpfreq; - filtAttr[i].lporder = isExtHdr.lporder; - filtAttr[i].lptype = isExtHdr.lptype; - strncpy(chanAttr[i].szLabel, isExtHdr.label, 16); - - chanExt2Attr[i].anamax = isExtHdr.anamax; - chanExt2Attr[i].anamin = isExtHdr.anamin; - chanExt2Attr[i].digmax = isExtHdr.digmax; - chanExt2Attr[i].digmin = isExtHdr.digmin; - strncpy(chanExt2Attr[i].anaunit, isExtHdr.anaunit, 16); - } - - hsize_t dims[1] = {1}; - hid_t space_attr = H5Screate_simple(1, dims, NULL); - hid_t tid_sampling_attr = -1; - hid_t tid_filt_attr = -1; - - uint32_t nChannelOffset = 0; - - hid_t ptid_chan[cbNUM_ANALOG_CHANS]; - { - hid_t gid_channel = -1; - if (H5Lexists(file, "channel", H5P_DEFAULT)) - gid_channel = H5Gopen(file, "channel", H5P_DEFAULT); - else - gid_channel = H5Gcreate(file, "channel", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - if (g_bAppend) - { - bool bExists = false; - // Find the last place to append to - do { - nChannelOffset++; - std::string strLabel = "channel"; - char szNum[7]; - sprintf(szNum, "%05u", nChannelOffset); - strLabel += szNum; - bExists = (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT) != 0); - } while(bExists); - nChannelOffset--; - } - - tid_sampling_attr = CreateSamplingAttrType(gid_channel); - tid_filt_attr = CreateFiltAttrType(gid_channel); - hid_t tid_chan_attr = CreateChanAttrType(gid_channel); - hid_t tid_chanext_attr = CreateChanExtAttrType(gid_channel); - hid_t tid_chanext2_attr = CreateChanExt2AttrType(gid_channel); - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - std::string strLabel = "channel"; - { - uint32_t id = chanAttr[i].id + nChannelOffset; - char szNum[7]; - sprintf(szNum, "%05u", id); - strLabel += szNum; - } - hid_t gid = -1; - if (H5Lexists(gid_channel, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gid_channel, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gid_channel, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - // Basic channel attributes - if (!H5Aexists(gid, "BmiChan")) - { - hid_t aid = H5Acreate(gid, "BmiChan", tid_chan_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chan_attr, &chanAttr[i]); - ret = H5Aclose(aid); - } - - // Extra header attribute - if (!H5Aexists(gid, "BmiChanExt")) - { - hid_t aid = H5Acreate(gid, "BmiChanExt", tid_chanext_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chanext_attr, &chanExtAttr[i]); - ret = H5Aclose(aid); - } - - // Additional extra channel attributes - if (!H5Aexists(gid, "BmiChanExt2")) - { - hid_t aid = H5Acreate(gid, "BmiChanExt2", tid_chanext2_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_chanext2_attr, &chanExt2Attr[i]); - ret = H5Aclose(aid); - } - - ret = H5Gclose(gid); - } // end for (uint32_t i = 0 - ret = H5Tclose(tid_chanext2_attr); - ret = H5Tclose(tid_chanext_attr); - ret = H5Tclose(tid_chan_attr); - ret = H5Gclose(gid_channel); - } - - // Now read data - fseeko(pFile, isHdr.nBytesInHdrs, SEEK_SET); - Nsx22DataHdr isDataHdr; - size_t nGot = fread(&isDataHdr, sizeof(Nsx22DataHdr), 1, pFile); - int setCount = 0; - if (nGot != 1) - { - printf("Invalid source file (cannot read data header)\n"); - return 1; - } - do { - if (isDataHdr.nHdr != 1) - { - printf("Invalid data header in source file\n"); - break; - } - if (isDataHdr.nNumDatapoints == 0) - { - printf("Data section %d with zero points detected!\n", setCount); - if (g_bSkipEmpty) - { - printf(" Skip this section and assume next in file is new data header\n"); - nGot = fread(&isDataHdr, sizeof(Nsx22DataHdr), 1, pFile); - if (nGot != 1) - break; - continue; - } else { - printf(" Retrieve the rest of the file as one chunk\n" - " Last section may have unaligned trailing data points\n" - " Use --skipempty if instead you want to skip empty headers\n"); - } - } - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - size_t chunk_size = CHUNK_SIZE_CONTINUOUS; - int compression = -1; // TODO: use options to add compression - char szNum[7]; - std::string strLabel = "/channel/channel"; - sprintf(szNum, "%05u", chanAttr[i].id + nChannelOffset); - strLabel += szNum; - hid_t gid = H5Gopen(file, strLabel.c_str(), H5P_DEFAULT); - // If need to go one level deeper - if (g_nCombine > 0) - { - hid_t gidParent = gid; - sprintf(szNum, "%05u", g_nCombine); - strLabel = szNum; - if (H5Lexists(gidParent, strLabel.c_str(), H5P_DEFAULT)) - gid = H5Gopen(gidParent, strLabel.c_str(), H5P_DEFAULT); - else - gid = H5Gcreate(gidParent, strLabel.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Gclose(gidParent); - } - strLabel = "continuous_set"; - if (setCount > 0) - { - sprintf(szNum, "%05u", setCount); - strLabel += szNum; - } - // We want to keep all data sets - while (H5Lexists(gid, strLabel.c_str(), H5P_DEFAULT)) - { - setCount++; - strLabel = "continuous_set"; - sprintf(szNum, "%05u", setCount); - strLabel += szNum; - } - ptid_chan[i] = H5PTcreate_fl(gid, strLabel.c_str(), H5T_NATIVE_INT16, chunk_size, compression); - - hid_t dsid = H5Dopen(gid, strLabel.c_str(), H5P_DEFAULT); - ret = H5Gclose(gid); - // Add data start clock attribute - hid_t aid = H5Acreate(dsid, "StartClock", H5T_NATIVE_UINT32, space_attr, H5P_DEFAULT, H5P_DEFAULT); - uint32_t nStartTime = isDataHdr.nTimestamp; - ret = H5Awrite(aid, H5T_NATIVE_UINT32, &nStartTime); - ret = H5Aclose(aid); - // Add data sampling attribute - aid = H5Acreate(dsid, "Sampling", tid_sampling_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_sampling_attr, &samplingAttr[i]); - ret = H5Aclose(aid); - // Add data filtering attribute - aid = H5Acreate(dsid, "Filter", tid_filt_attr, space_attr, H5P_DEFAULT, H5P_DEFAULT); - ret = H5Awrite(aid, tid_filt_attr, &filtAttr[i]); - ret = H5Aclose(aid); - - ret = H5Dclose(dsid); - } - int count = 0; - int16_t anDataBufferCache[cbNUM_ANALOG_CHANS][CHUNK_SIZE_CONTINUOUS]; - for (uint32_t i = 0; i < isDataHdr.nNumDatapoints || isDataHdr.nNumDatapoints == 0; ++i) - { - int16_t anDataBuffer[cbNUM_ANALOG_CHANS]; - size_t nGot = fread(anDataBuffer, sizeof(int16_t), isHdr.cnChannels, pFile); - if (nGot != isHdr.cnChannels) - { - if (isDataHdr.nNumDatapoints == 0) - printf("Data section %d may be unaligned\n", setCount); - else - printf("Fewer data points (%u) than specified in data header (%u) at the source file!\n", i + 1, isDataHdr.nNumDatapoints); - break; - } - for (uint32_t j = 0; j < isHdr.cnChannels; ++j) - { - anDataBufferCache[j][count] = anDataBuffer[j]; - } - count++; - if (count == CHUNK_SIZE_CONTINUOUS) - { - for (uint32_t j = 0; j < isHdr.cnChannels; ++j) - { - ret = H5PTappend(ptid_chan[j], count, &anDataBufferCache[j][0]); - } - count = 0; - } - } // end for (uint32_t i = 0 - - // Write out the remaining chunk - if (count > 0) - { - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - ret = H5PTappend(ptid_chan[i], count, &anDataBufferCache[i][0]); - } - } - // Close packet tables as we may open them again for paused files - for (uint32_t i = 0; i < isHdr.cnChannels; ++i) - { - ret = H5PTclose(ptid_chan[i]); - } - // Read possiblly more data streams - nGot = fread(&isDataHdr, sizeof(Nsx22DataHdr), 1, pFile); - setCount++; - } while (nGot == 1); - - // - // We are going to call H5Close so no need to close what is open at this stage - // - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////////////// -// Function main() -///////////////////////////////////////////////////////////////////////////////////////////////// -int main(int argc, char * const argv[]) -{ - herr_t ret; - int idxSrcFile = 1; - bool bForce = false; - bool bCache = true; - bool bCombine = false; - for (int i = 1; i < argc; ++i) - { - if (strcmp(argv[i], "--force") == 0) - { - bForce = true; - idxSrcFile++; - } - else if (strcmp(argv[i], "--nocache") == 0) - { - bCache = false; - idxSrcFile++; - } - else if (strcmp(argv[i], "--nospikes") == 0) - { - g_bNoSpikes = true; - idxSrcFile++; - } - else if (strcmp(argv[i], "--skipempty") == 0) - { - g_bSkipEmpty = true; - idxSrcFile++; - } - else if (strcmp(argv[i], "--append") == 0) - { - g_bAppend = true; - idxSrcFile++; - } - else if (strcmp(argv[i], "--combine") == 0) - { - if (i + 1 >= argc || !isdigit(argv[i + 1][0])) - { - printf("Combine level not specified or is invalid\n"); - idxSrcFile = argc; // Just to show the usage - break; - } - g_nCombine = atoi(argv[i + 1]); - bCombine = true; - idxSrcFile += 2; - i++; - } - else if (strcmp(argv[i], "--group") == 0) - { - // TODO: implement - printf("Group addition is not implemented in this version!\n"); - return 0; - } - } - if (idxSrcFile >= argc) - { - printf("Blackrock file conversion utility (version 1.0)\n" - "Usage: n2h5 [options] []\n" - "Purpose: Converts srcfile to destfile\n" - "Inputs:\n" - " - the file to convert from (nev or nsx format)\n" - " - the converted file to create (hdf5 format)\n" - " default is .bh5\n" - "Options:\n" - " --force : overwrites the destination if it exists, create if not\n" - " --nocache : slower but results in smaller file size\n" - " --nospikes : ignore spikes\n" - " --skipempty: skip 0-sized headers (instead of ignoring them)\n" - " --combine : combine to the existing channels at given subchannel level (level 0 means no subchannel)\n" - " same experiment, same channels, different data sets (e.g. different sampling rates or filters)\n" - " --append : append channels to the end of current channels\n" - " same experiment, different channel (e.g. sync systems recording)\n" - " --group : add as new group\n" - " different experiments\n"); - return 0; - } - - // TODO: implement --append for video and comment data - // TODO: implement --group - - const char * szSrcFile = argv[idxSrcFile]; - std::string strDest; - if ((idxSrcFile + 1) >= argc) - { - strDest = szSrcFile; - strDest += ".bh5"; - } else { - strDest = argv[idxSrcFile + 1]; - } - const char * szDstFile = strDest.c_str(); - - char achFileID[8]; - - FILE * pFile = fopen(szSrcFile, "rb"); - if (pFile == NULL) - { - perror("Unable to open source file for reading"); - return 0; - } - - if (H5open()) - { - fclose(pFile); - printf("cannot open hdf5 library\n"); - return 0; - } - - hid_t file; - hid_t facpl = H5P_DEFAULT; - - if (g_bAppend || bCombine) - { - // Open read-only just to validate destination file - file = H5Fopen(szDstFile, H5F_ACC_RDONLY, H5P_DEFAULT); - if (file < 0 && !bForce) - { - printf("Cannot append to the destination file or destiantion file does not exist\n" - "Use --force to to ignore this error\n"); - goto ErrHandle; - } - H5Fclose(file); - } - - if (bCache) - { - double rdcc_w0 = 1; // We only write so this should work - facpl = H5Pcreate(H5P_FILE_ACCESS); - // Useful primes: 401 4049 404819 - ret = H5Pset_cache(facpl, 0, 404819, 4 * 1024 * CHUNK_SIZE_CONTINUOUS, rdcc_w0); - } - if (g_bAppend || bCombine) - { - file = H5Fopen(szDstFile, H5F_ACC_RDWR, H5P_DEFAULT); - } else { - file = H5Fcreate(szDstFile, bForce ? H5F_ACC_TRUNC : H5F_ACC_EXCL, H5P_DEFAULT, facpl); - } - if (facpl != H5P_DEFAULT) - ret = H5Pclose(facpl); - - if (file < 0) - { - if (g_bAppend || bCombine) - printf("Cannot open the destination file or destination file does not exist\n" - "Use --force to create new file\n"); - else - printf("Cannot create destination file or destiantion file exists\n" - "Use --force to overwite the file\n"); - goto ErrHandle; - } - fread(&achFileID, sizeof(achFileID), 1, pFile); - // NEV file - if (0 == strncmp(achFileID, "NEURALEV", sizeof(achFileID))) - { - if (ConvertNev(pFile, file)) - { - printf("Error in ConvertNev()\n"); - goto ErrHandle; - } - } - // 2.1 filespec - else if (0 == strncmp(achFileID, "NEURALSG", sizeof(achFileID))) - { - if (ConvertNSx21(szSrcFile, pFile, file)) - { - printf("Error in ConvertNSx21()\n"); - goto ErrHandle; - } - } - // 2.2 filespec - else if (0 == strncmp(achFileID, "NEURALCD", sizeof(achFileID))) - { - if (ConvertNSx22(pFile, file)) - { - printf("Error in ConvertNSx22()\n"); - goto ErrHandle; - } - } else { - printf("Invalid source file format\n"); - } -ErrHandle: - if (pFile) - fclose(pFile); - if (file > 0) - H5Fclose(file); - H5close(); - return 0; -} diff --git a/tools/n2h5/n2h5.cpp b/tools/n2h5/n2h5.cpp deleted file mode 100755 index e967b3f2..00000000 --- a/tools/n2h5/n2h5.cpp +++ /dev/null @@ -1,493 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2012 Blackrock Microsystems -// -// $Workfile: n2h5.c $ -// $Archive: /n2h5/n2h5.c $ -// $Revision: 1 $ -// $Date: 11/1/12 1:00p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// -// -// PURPOSE: -// -// Implementation of our types in terms of hdf5 types -// -// Note: all the types are committed to the most immediate parent group -// while we try to keep the types in different version the same, -// one should try to base reading on the field names rather than types -// -////////////////////////////////////////////////////////////////////////////// - -#include "stdafx.h" - -#include "n2h5.h" - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiFiltAttr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateFiltAttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiFiltAttr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiFiltAttr_t)); - ret = H5Tinsert(tid, "HighPassFreq", offsetof(BmiFiltAttr_t, hpfreq), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "HighPassOrder", offsetof(BmiFiltAttr_t, hporder), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "HighPassType", offsetof(BmiFiltAttr_t, hptype), H5T_NATIVE_UINT16); - ret = H5Tinsert(tid, "LowPassFreq", offsetof(BmiFiltAttr_t, lpfreq), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "LowPassOrder", offsetof(BmiFiltAttr_t, lporder), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "LowPassType", offsetof(BmiFiltAttr_t, lptype), H5T_NATIVE_UINT16); - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiChanExtAttr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateChanExtAttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiChanExtAttr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiChanExtAttr_t)); - ret = H5Tinsert(tid, "NanoVoltsPerLSB", offsetof(BmiChanExtAttr_t, dFactor), H5T_NATIVE_DOUBLE); - ret = H5Tinsert(tid, "PhysicalConnector", offsetof(BmiChanExtAttr_t, phys_connector), H5T_NATIVE_UINT8); - ret = H5Tinsert(tid, "ConnectorPin", offsetof(BmiChanExtAttr_t, connector_pin), H5T_NATIVE_UINT8); - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiChanExt1Attr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateChanExt1AttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiChanExt1Attr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiChanExt1Attr_t)); - ret = H5Tinsert(tid, "SortCount", offsetof(BmiChanExt1Attr_t, sortCount), H5T_NATIVE_UINT8); - ret = H5Tinsert(tid, "EnergyThreshold", offsetof(BmiChanExt1Attr_t, energy_thresh), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "HighThreshold", offsetof(BmiChanExt1Attr_t, high_thresh), H5T_NATIVE_INT32); - ret = H5Tinsert(tid, "LowThreshold", offsetof(BmiChanExt1Attr_t, low_thresh), H5T_NATIVE_INT32); - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiChanExt2Attr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateChanExt2AttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiChanExt2Attr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - hid_t tid_attr_unit_str = H5Tcopy(H5T_C_S1); - ret = H5Tset_size(tid_attr_unit_str, 16); - - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiChanExt2Attr_t)); - ret = H5Tinsert(tid, "DigitalMin", offsetof(BmiChanExt2Attr_t, digmin), H5T_NATIVE_INT32); - ret = H5Tinsert(tid, "DigitalMax", offsetof(BmiChanExt2Attr_t, digmax), H5T_NATIVE_INT32); - ret = H5Tinsert(tid, "AnalogMin", offsetof(BmiChanExt2Attr_t, anamin), H5T_NATIVE_INT32); - ret = H5Tinsert(tid, "AnalogMax", offsetof(BmiChanExt2Attr_t, anamax), H5T_NATIVE_INT32); - ret = H5Tinsert(tid, "AnalogUnit", offsetof(BmiChanExt2Attr_t, anaunit), tid_attr_unit_str); - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - H5Tclose(tid_attr_unit_str); - - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiChanAttr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateChanAttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiChanAttr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - hid_t tid_attr_label_str = H5Tcopy(H5T_C_S1); - ret = H5Tset_size(tid_attr_label_str, 64); - - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiChanAttr_t)); - ret = H5Tinsert(tid, "ID", offsetof(BmiChanAttr_t, id), H5T_NATIVE_UINT16); - ret = H5Tinsert(tid, "Label", offsetof(BmiChanAttr_t, szLabel), tid_attr_label_str); - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - H5Tclose(tid_attr_label_str); - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiSamplingAttr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateSamplingAttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiSamplingAttr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiSamplingAttr_t)); - ret = H5Tinsert(tid, "Clock", offsetof(BmiSamplingAttr_t, fClock), H5T_NATIVE_FLOAT); - ret = H5Tinsert(tid, "SampleRate", offsetof(BmiSamplingAttr_t, fSampleRate), H5T_NATIVE_FLOAT); - ret = H5Tinsert(tid, "SampleBits", offsetof(BmiSamplingAttr_t, nSampleBits), H5T_NATIVE_UINT8); - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiRootAttr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateRootAttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiRootAttr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - hid_t tid_attr_str = H5Tcopy(H5T_C_S1); - ret = H5Tset_size(tid_attr_str, 64); - hid_t tid_attr_comment_str = H5Tcopy(H5T_C_S1); - ret = H5Tset_size(tid_attr_comment_str, 1024); - - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiRootAttr_t)); - ret = H5Tinsert(tid, "MajorVersion", offsetof(BmiRootAttr_t, nMajorVersion), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "MinorVersion", offsetof(BmiRootAttr_t, nMinorVersion), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "Flags", offsetof(BmiRootAttr_t, nFlags), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "GroupCount", offsetof(BmiRootAttr_t, nGroupCount), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "Date", offsetof(BmiRootAttr_t, szDate), tid_attr_str); // date of the file creation - ret = H5Tinsert(tid, "Application", offsetof(BmiRootAttr_t, szApplication), tid_attr_str); // application that created the file - ret = H5Tinsert(tid, "Comment", offsetof(BmiRootAttr_t, szComment), tid_attr_comment_str); // file comments - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - H5Tclose(tid_attr_str); - H5Tclose(tid_attr_comment_str); - return tid; -} - -// Author & Date: Ehsan Azar Nov 17, 2012 -// Purpose: Create type for BmiSynchAttr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateSynchAttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiSynchAttr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - hid_t tid_attr_label_str = H5Tcopy(H5T_C_S1); - ret = H5Tset_size(tid_attr_label_str, 64); - - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiSynchAttr_t)); - ret = H5Tinsert(tid, "ID", offsetof(BmiSynchAttr_t, id), H5T_NATIVE_UINT16); - ret = H5Tinsert(tid, "Label", offsetof(BmiSynchAttr_t, szLabel), tid_attr_label_str); - ret = H5Tinsert(tid, "FPS", offsetof(BmiSynchAttr_t, fFps), H5T_NATIVE_FLOAT); - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - H5Tclose(tid_attr_label_str); - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiTrackingAttr_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateTrackingAttrType(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiTrackingAttr_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - hid_t tid_attr_label_str = H5Tcopy(H5T_C_S1); - ret = H5Tset_size(tid_attr_label_str, 128); - - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiTrackingAttr_t)); - ret = H5Tinsert(tid, "Label", offsetof(BmiTrackingAttr_t, szLabel), tid_attr_label_str); - ret = H5Tinsert(tid, "Type", offsetof(BmiTrackingAttr_t, type), H5T_NATIVE_UINT16); - ret = H5Tinsert(tid, "TrackID", offsetof(BmiTrackingAttr_t, trackID), H5T_NATIVE_UINT16); - ret = H5Tinsert(tid, "MaxPoints", offsetof(BmiTrackingAttr_t, maxPoints), H5T_NATIVE_UINT16); - - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - H5Tclose(tid_attr_label_str); - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiSpike16_t and commit -// Note: For performance reasons and because spike length is constant during -// and experiment we use fixed-length array here instead of varible-length -// Inputs: -// loc - where to add the type -// spikeLength - the spike length to use -// Outputs: -// Returns the type id -hid_t CreateSpike16Type(hid_t loc, uint16_t spikeLength) -{ - herr_t ret; - hid_t tid = -1; - - // e.g. for spike length of 48 type name will be "BmiSpike16_48_t" - char szNum[4] = {'\0'}; - sprintf(szNum, "%u", spikeLength); - std::string strLink = "BmiSpike16_"; - strLink += szNum; - strLink += "_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - - hsize_t dims[1] = {spikeLength}; - hid_t tid_arr_wave = H5Tarray_create(H5T_NATIVE_INT16, 1, dims); - - tid = H5Tcreate(H5T_COMPOUND, offsetof(BmiSpike16_t, wave) + sizeof(int16_t) * spikeLength); - ret = H5Tinsert(tid, "TimeStamp", offsetof(BmiSpike16_t, dwTimestamp), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "Unit", offsetof(BmiSpike16_t, unit), H5T_NATIVE_UINT8); - ret = H5Tinsert(tid, "Wave", offsetof(BmiSpike16_t, wave), tid_arr_wave); - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - H5Tclose(tid_arr_wave); - return tid; -} - -// Author & Date: Ehsan Azar Nov 23, 2012 -// Purpose: Create type for BmiDig16_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateDig16Type(hid_t loc) -{ - herr_t ret; - hid_t tid = -1; - std::string strLink = "BmiDig16_t"; - // If already there return it - if(H5Lexists(loc, strLink.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLink.c_str(), H5P_DEFAULT); - return tid; - } - - tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiDig16_t)); - ret = H5Tinsert(tid, "TimeStamp", offsetof(BmiDig16_t, dwTimestamp), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "Value", offsetof(BmiDig16_t, value), H5T_NATIVE_UINT16); - ret = H5Tcommit(loc, strLink.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - return tid; -} - -// Author & Date: Ehsan Azar Nov 13, 2012 -// Purpose: Create type for BmiSynch_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateSynchType(hid_t loc) -{ - herr_t ret; - - hid_t tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiSynch_t)); - ret = H5Tinsert(tid, "TimeStamp", offsetof(BmiSynch_t, dwTimestamp), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "Split", offsetof(BmiSynch_t, split), H5T_NATIVE_UINT16); - ret = H5Tinsert(tid, "Frame", offsetof(BmiSynch_t, frame), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "ElapsedTime", offsetof(BmiSynch_t, etime), H5T_NATIVE_UINT32); - - ret = H5Tcommit(loc, "BmiSynch_t", tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - return tid; -} - -// Author & Date: Ehsan Azar Nov 20, 2012 -// Purpose: Create type for BmiTracking_t and commit -// Inputs: -// loc - where to add the type -// dim - dimension (1D, 2D or 3D) -// width - datapoint width in bytes -// Outputs: -// Returns the type id -hid_t CreateTrackingType(hid_t loc, int dim, int width) -{ - herr_t ret; - hid_t tid_coords; - std::string strLabel = "BmiTracking"; - // e.g. for 1D (32-bit) fixed-length, type name will be "BmiTracking32_1D_t" - // for 2D (16-bit) fixed-length, type name will be "BmiTracking16_2D_t" - - switch (width) - { - case 1: - strLabel += "8"; - tid_coords = H5Tcopy(H5T_NATIVE_UINT8); - break; - case 2: - strLabel += "16"; - tid_coords = H5Tcopy(H5T_NATIVE_UINT16); - break; - case 4: - strLabel += "32"; - tid_coords = H5Tcopy(H5T_NATIVE_UINT32); - break; - default: - return 0; - // should not happen - break; - } - switch (dim) - { - case 1: - strLabel += "_1D"; - break; - case 2: - strLabel += "_2D"; - break; - case 3: - strLabel += "_3D"; - break; - default: - return 0; - // should not happen - break; - } - strLabel += "_t"; - - hid_t tid = -1; - if(H5Lexists(loc, strLabel.c_str(), H5P_DEFAULT)) - { - tid = H5Topen(loc, strLabel.c_str(), H5P_DEFAULT); - } else { - tid = H5Tcreate(H5T_COMPOUND, offsetof(BmiTracking_fl_t, coords) + dim * width * 1); - ret = H5Tinsert(tid, "TimeStamp", offsetof(BmiTracking_fl_t, dwTimestamp), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "ParentID", offsetof(BmiTracking_fl_t, parentID), H5T_NATIVE_UINT16); - ret = H5Tinsert(tid, "NodeCount", offsetof(BmiTracking_fl_t, nodeCount), H5T_NATIVE_UINT16); - ret = H5Tinsert(tid, "ElapsedTime", offsetof(BmiTracking_fl_t, etime), H5T_NATIVE_UINT32); - char corrd_labels[3][2] = {"x", "y", "z"}; - for (int i = 0; i < dim; ++i) - ret = H5Tinsert(tid, corrd_labels[i], offsetof(BmiTracking_fl_t, coords) + i * width, tid_coords); - ret = H5Tcommit(loc, strLabel.c_str(), tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - } - - H5Tclose(tid_coords); - - return tid; -} - -// Author & Date: Ehsan Azar Nov 19, 2012 -// Purpose: Create type for BmiComment_t and commit -// Inputs: -// loc - where to add the type -// Outputs: -// Returns the type id -hid_t CreateCommentType(hid_t loc) -{ - herr_t ret; - // Use fixed-size comments because - // it is faster, more tools support it, and also allows compression - hid_t tid_str_array = H5Tcopy(H5T_C_S1); - ret = H5Tset_size(tid_str_array, BMI_COMMENT_LEN); - - hid_t tid = H5Tcreate(H5T_COMPOUND, sizeof(BmiComment_t)); - ret = H5Tinsert(tid, "TimeStamp", offsetof(BmiComment_t, dwTimestamp), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "Data", offsetof(BmiComment_t, data), H5T_NATIVE_UINT32); - ret = H5Tinsert(tid, "Flags", offsetof(BmiComment_t, flags), H5T_NATIVE_UINT8); - ret = H5Tinsert(tid, "Comment", offsetof(BmiComment_t, szComment), tid_str_array); - - ret = H5Tcommit(loc, "BmiComment_t", tid, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - H5Tclose(tid_str_array); - - return tid; -} diff --git a/tools/n2h5/n2h5.h b/tools/n2h5/n2h5.h deleted file mode 100755 index 38b28198..00000000 --- a/tools/n2h5/n2h5.h +++ /dev/null @@ -1,204 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -// -// (c) Copyright 2012 Blackrock Microsystems -// -// $Workfile: n2h5.h $ -// $Archive: /n2h5/n2h5.h $ -// $Revision: 1 $ -// $Date: 11/1/12 1:00p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// -// -// Note: -// for simple tools to better understand the format keep variable length types to the end -// - -#ifndef N2H5_H_ -#define N2H5_H_ - -#include "cbhwlib.h" -#include "hdf5.h" -#include "hdf5_hl.h" - -#define cbFIRST_DIGIN_CHAN (cbNUM_FE_CHANS + cbNUM_ANAIN_CHANS + cbNUM_ANAOUT_CHANS + cbNUM_AUDOUT_CHANS) -#define cbFIRST_SERIAL_CHAN (cbFIRST_DIGIN_CHAN + cbNUM_DIGIN_CHANS) - -// -// Basic channel attributes -// -typedef struct { - uint16_t id; // channel id - char szLabel[64]; // Channel label -} BmiChanAttr_t; - -hid_t CreateChanAttrType(hid_t loc); - -// -// Sample rate attributes -// -typedef struct { - float fClock; // global clock used for this data set - float fSampleRate; // sampling done for this channel - uint8_t nSampleBits; // Number of bits in each sample -} BmiSamplingAttr_t; - -hid_t CreateSamplingAttrType(hid_t loc); - -// -// Channel filter attributes -// -typedef struct { - // High pass filter info - uint32_t hpfreq; // Filter frequency in mHz - uint32_t hporder; // Filter order - uint16_t hptype; // Filter type - // Low pass filter info - uint32_t lpfreq; // Filter frequency in mHz - uint32_t lporder; // Filter order - uint16_t lptype; // Filter type -} BmiFiltAttr_t; - -hid_t CreateFiltAttrType(hid_t loc); - -// -// Channel extra attributes addition 1 -// -typedef struct { - // These may only appear in NEV extra headers - uint8_t sortCount; // Number of sorted units - uint32_t energy_thresh; - int32_t high_thresh; - int32_t low_thresh; -} BmiChanExt1Attr_t; - -hid_t CreateChanExt1AttrType(hid_t loc); - -// -// Channel extra attributes addition 2 -// -typedef struct { - // These may only appear in NSx extra headers - int32_t digmin; // Minimum digital value - int32_t digmax; // Maximum digital value - int32_t anamin; // Minimum analog Value - int32_t anamax; // Maximum analog Value - char anaunit[16]; // Units for the Analog Value (e.g. "mV) -} BmiChanExt2Attr_t; - -hid_t CreateChanExt2AttrType(hid_t loc); - -// -// Channel extra attributes -// -typedef struct { - double dFactor; // nano volts per LSB (used in conversion between digital and analog values) - uint8_t phys_connector; - uint8_t connector_pin; -} BmiChanExtAttr_t; - -hid_t CreateChanExtAttrType(hid_t loc); - -// -// Header may not change with each experiment -// and thus root-group attribute -// -typedef struct { - uint32_t nMajorVersion; - uint32_t nMinorVersion; - uint32_t nFlags; - uint32_t nGroupCount; // Number of data groups withing this file - char szDate[64]; // File creation date-time in SQL format - char szApplication[64]; // Which application created this file - char szComment[1024]; // File Comment -} BmiRootAttr_t; - -hid_t CreateRootAttrType(hid_t loc); - -// -// Synch general information -// -typedef struct { - uint16_t id; // video source ID - float fFps; - char szLabel[64]; // Name of the video source -} BmiSynchAttr_t; - -hid_t CreateSynchAttrType(hid_t loc); - -// -// Video source general information -// -typedef struct { - uint16_t type; // trackable type - uint16_t trackID; // trackable ID - uint16_t maxPoints; - char szLabel[128]; // Name of the trackable -} BmiTrackingAttr_t; - -hid_t CreateTrackingAttrType(hid_t loc); - -// Spike data (of int16_t samples) -typedef struct { - uint32_t dwTimestamp; - uint8_t unit; - uint8_t res; - // This must be the last - int16_t wave[cbMAX_PNTS]; // Currently up to cbMAX_PNTS -} BmiSpike16_t; - -hid_t CreateSpike16Type(hid_t loc, uint16_t spikeLength); - -// Digital/serial data (of int16_t samples) -typedef struct { - uint32_t dwTimestamp; - uint16_t value; -} BmiDig16_t; - -hid_t CreateDig16Type(hid_t loc); - -// Video synchronization -typedef struct { - uint32_t dwTimestamp; - uint16_t split; - uint32_t frame; - uint32_t etime; // Elapsed time in milli-seconds -} BmiSynch_t; - -hid_t CreateSynchType(hid_t loc); - -// Video tracking -typedef struct { - uint32_t dwTimestamp; - uint16_t parentID; - uint16_t nodeCount; - // This must be the last - hvl_t coords; -} BmiTracking_t; - -// Video tracking -typedef struct { - uint32_t dwTimestamp; - uint16_t parentID; - uint16_t nodeCount; - uint32_t etime; - // This must be the last - uint16_t coords[cbMAX_TRACKCOORDS]; -} BmiTracking_fl_t; - -hid_t CreateTrackingType(hid_t loc, int dim, int width); - -#define BMI_COMMENT_LEN 256 -// Comment or user event -typedef struct { - uint32_t dwTimestamp; - uint8_t flags; - uint32_t data; - char szComment[BMI_COMMENT_LEN]; -} BmiComment_t; - -hid_t CreateCommentType(hid_t loc); - -#endif /* N2H5_H_ */ diff --git a/tools/n2h5/n2h5.vcproj b/tools/n2h5/n2h5.vcproj deleted file mode 100755 index 57760475..00000000 --- a/tools/n2h5/n2h5.vcproj +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/n2h5/res/n2h5.ico b/tools/n2h5/res/n2h5.ico deleted file mode 100755 index a8e94d86..00000000 Binary files a/tools/n2h5/res/n2h5.ico and /dev/null differ diff --git a/tools/n2h5/res/n2h5_res.rc b/tools/n2h5/res/n2h5_res.rc deleted file mode 100755 index 2b278f21..00000000 --- a/tools/n2h5/res/n2h5_res.rc +++ /dev/null @@ -1,60 +0,0 @@ -#include - -#define VER_FILEVERSION 1,0,0,0 -#define VER_FILEVERSION_STR "1.0" - -#define VER_PRODUCTVERSION 1,0,0,0 -#define VER_PRODUCTVERSION_STR "1.0" - -#define VER_COMPANYNAME_STR "Blackrock Microsystems" -#define VER_FILEDESCRIPTION_STR "Blackrock hdf5 conversion utility" -#define VER_INTERNALNAME_STR "n2h5.exe" -#define VER_LEGALCOPYRIGHT_STR "Copyright(C) 2012-2013 Blackrock Microsystems" -#define VER_ORIGINALFILENAME_STR "n2h5.exe" -#define VER_PRODUCTNAME_STR "Blackrock hdf5 conversion utility" - -#ifndef DEBUG -#define VER_DEBUG 0 -#else -#define VER_DEBUG VS_FF_DEBUG -#endif - -IDI_ICON1 ICON DISCARDABLE "n2h5.ico" -VS_VERSION_INFO VERSIONINFO -FILEVERSION VER_FILEVERSION -PRODUCTVERSION VER_PRODUCTVERSION -FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -FILEFLAGS (VER_DEBUG) -FILEOS VOS__WINDOWS32 -FILETYPE VFT_APP -FILESUBTYPE VFT2_UNKNOWN -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904E4" - BEGIN - VALUE "CompanyName", VER_COMPANYNAME_STR - VALUE "FileDescription", VER_FILEDESCRIPTION_STR - VALUE "FileVersion", VER_FILEVERSION_STR - VALUE "InternalName", VER_INTERNALNAME_STR - VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR - VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR - VALUE "ProductName", VER_PRODUCTNAME_STR - VALUE "ProductVersion", VER_PRODUCTVERSION_STR - END - END - - BLOCK "VarFileInfo" - BEGIN - /* The following line should only be modified for localized versions. */ - /* It consists of any number of WORD,WORD pairs, with each pair */ - /* describing a language,codepage combination supported by the file. */ - /* */ - /* For example, a file might have values "0x409,1252" indicating that it */ - /* supports English language (0x409) in the Windows ANSI codepage (1252). */ - - VALUE "Translation", 0x409, 1252 - - END -END - diff --git a/tools/n2h5/stdafx.h b/tools/n2h5/stdafx.h deleted file mode 100755 index ef137495..00000000 --- a/tools/n2h5/stdafx.h +++ /dev/null @@ -1,25 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -// -// -// $Workfile: stdafx.h $ -// $Archive: /n2h5/stdafx.h $ -// $Revision: 1 $ -// $Date: 11/1/12 1:00p $ -// $Author: Ehsan $ -// -// $NoKeywords: $ -// -////////////////////////////////////////////////////////////////////////////// - -// MS compatibility header file -#ifndef STDAFX_H_ -#define STDAFX_H_ - -#define _CRT_SECURE_NO_DEPRECATE -#include -#include -#include -#ifndef WIN32 -#define _strdup strdup -#endif -#endif /* STDAFX_H_ */ diff --git a/tools/validate_clock_sync/CMakeLists.txt b/tools/validate_clock_sync/CMakeLists.txt new file mode 100644 index 00000000..97798767 --- /dev/null +++ b/tools/validate_clock_sync/CMakeLists.txt @@ -0,0 +1,18 @@ +# validate_clock_sync - PTP-based validation of ClockSync accuracy +# Linux-only: requires PTP hardware clock (PHC) access via clock_gettime() + +if(UNIX AND NOT APPLE) + add_executable(validate_clock_sync validate_clock_sync.cpp) + target_link_libraries(validate_clock_sync PRIVATE cbsdk cbdev cbproto) + target_include_directories(validate_clock_sync PRIVATE + ${PROJECT_SOURCE_DIR}/src/cbsdk/include + ${PROJECT_SOURCE_DIR}/src/cbdev/include + ${PROJECT_SOURCE_DIR}/src/cbproto/include) + + # probe_timing - Measure nPlay probe forward/reverse delays using PTP timestamps + # Standalone raw UDP tool (no cbsdk/cbdev dependency) for precise timing measurements + add_executable(probe_timing probe_timing.cpp) + target_link_libraries(probe_timing PRIVATE cbproto) + target_include_directories(probe_timing PRIVATE + ${PROJECT_SOURCE_DIR}/src/cbproto/include) +endif() diff --git a/tools/validate_clock_sync/probe_timing.cpp b/tools/validate_clock_sync/probe_timing.cpp new file mode 100644 index 00000000..e4fe8f79 --- /dev/null +++ b/tools/validate_clock_sync/probe_timing.cpp @@ -0,0 +1,350 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file probe_timing.cpp +/// @brief Measure nPlay clock probe forward/reverse delays using PTP timestamps +/// +/// Sends nPlay probe packets to a Gemini Hub and measures the one-way delays +/// using PTP hardware clock timestamps on both sides. Requires: +/// - RPi5 with PTP slave synced to Hub (ptp4l -i eth0 -s -m --priority1=255) +/// - Hub firmware with fresh PTP timestamp in .etime (ProcessPktNplaySet mod) +/// +/// All timestamps are in the PTP clock domain, so the deltas are true network delays +/// (assuming PTP sync error is small relative to network delay). +/// +/// Usage: +/// ./probe_timing --ptp-device /dev/ptp0 [OPTIONS] +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifdef __linux__ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef FD_TO_CLOCKID +#define FD_TO_CLOCKID(fd) ((~(clockid_t)(fd) << 3) | 3) +#endif + +static volatile sig_atomic_t g_running = 1; + +static void signal_handler(int) { + g_running = 0; +} + +static inline int64_t timespec_to_ns(const struct timespec& ts) { + return static_cast(ts.tv_sec) * 1'000'000'000LL + ts.tv_nsec; +} + +static inline int64_t read_phc_ns(clockid_t phc_clockid) { + struct timespec ts; + clock_gettime(phc_clockid, &ts); + return timespec_to_ns(ts); +} + +struct Options { + const char* ptp_device = nullptr; + const char* device_addr = "192.168.137.200"; + int port = 51002; + int count = 100; + int interval_ms = 1000; + int recv_timeout_ms = 500; +}; + +struct ProbeResult { + int64_t t1_ptp; // PHC time at send + int64_t t3_etime; // Fresh device PTP time (.etime) + int64_t t3_stale; // Stale device time (header->time) + int64_t t4_ptp; // PHC time at receive + int64_t d1; // Forward delay: t3_etime - t1_ptp + int64_t d2; // Reverse delay: t4_ptp - t3_etime + int64_t rtt; // Round trip: t4_ptp - t1_ptp + int64_t staleness; // t3_etime - t3_stale +}; + +static void print_stats(const char* label, const std::vector& values) { + if (values.empty()) { + fprintf(stderr, " %s: no data\n", label); + return; + } + + std::vector sorted = values; + std::sort(sorted.begin(), sorted.end()); + + double sum = 0, sum_sq = 0; + for (auto v : sorted) { + sum += v; + sum_sq += static_cast(v) * v; + } + double mean = sum / sorted.size(); + double variance = (sum_sq / sorted.size()) - (mean * mean); + double stddev = std::sqrt(std::max(0.0, variance)); + + int64_t min = sorted.front(); + int64_t max = sorted.back(); + int64_t median = sorted[sorted.size() / 2]; + int64_t p5 = sorted[sorted.size() * 5 / 100]; + int64_t p95 = sorted[sorted.size() * 95 / 100]; + + fprintf(stderr, " %-20s n=%-4zu min=%6ld p5=%6ld median=%6ld mean=%8.1f p95=%6ld max=%6ld stddev=%7.1f (ns)\n", + label, sorted.size(), min, p5, median, mean, p95, max, stddev); +} + +static void print_usage(const char* prog) { + fprintf(stderr, + "Usage: %s --ptp-device /dev/ptpN [OPTIONS]\n" + "\n" + "Measures nPlay clock probe forward/reverse delays using PTP timestamps.\n" + "\n" + "Options:\n" + " --ptp-device PATH PTP hardware clock device (required)\n" + " --device-addr ADDR Hub IP address (default: 192.168.137.200)\n" + " --port PORT Hub UDP port (default: 51002)\n" + " --count N Number of probes to send (default: 100)\n" + " --interval MS Interval between probes in ms (default: 1000)\n" + " --timeout MS Receive timeout per probe in ms (default: 500)\n", + prog); +} + +int main(int argc, char* argv[]) { + Options opts; + + static struct option long_options[] = { + {"ptp-device", required_argument, nullptr, 'p'}, + {"device-addr", required_argument, nullptr, 'a'}, + {"port", required_argument, nullptr, 'P'}, + {"count", required_argument, nullptr, 'n'}, + {"interval", required_argument, nullptr, 'i'}, + {"timeout", required_argument, nullptr, 't'}, + {"help", no_argument, nullptr, 'h'}, + {nullptr, 0, nullptr, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "p:a:P:n:i:t:h", long_options, nullptr)) != -1) { + switch (opt) { + case 'p': opts.ptp_device = optarg; break; + case 'a': opts.device_addr = optarg; break; + case 'P': opts.port = atoi(optarg); break; + case 'n': opts.count = atoi(optarg); break; + case 'i': opts.interval_ms = atoi(optarg); break; + case 't': opts.recv_timeout_ms = atoi(optarg); break; + case 'h': print_usage(argv[0]); return 0; + default: print_usage(argv[0]); return 1; + } + } + + if (!opts.ptp_device) { + fprintf(stderr, "Error: --ptp-device is required\n\n"); + print_usage(argv[0]); + return 1; + } + + // Open PTP hardware clock + int phc_fd = open(opts.ptp_device, O_RDONLY); + if (phc_fd < 0) { + perror("Failed to open PTP device"); + return 1; + } + clockid_t phc_clockid = FD_TO_CLOCKID(phc_fd); + + // Verify PHC is readable + { + int64_t test_ns = read_phc_ns(phc_clockid); + if (test_ns == 0) { + fprintf(stderr, "Failed to read PTP clock\n"); + close(phc_fd); + return 1; + } + fprintf(stderr, "PHC opened: %s (current time: %ld ns)\n", opts.ptp_device, test_ns); + } + + // Create UDP socket + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sockfd < 0) { + perror("Failed to create socket"); + close(phc_fd); + return 1; + } + + // Set SO_REUSEADDR + int opt_one = 1; + setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt_one, sizeof(opt_one)); + + // Set receive timeout + struct timeval tv; + tv.tv_sec = opts.recv_timeout_ms / 1000; + tv.tv_usec = (opts.recv_timeout_ms % 1000) * 1000; + setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + // Set receive buffer size + int rcvbuf = 6000000; + setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + + // Bind to receive port + struct sockaddr_in bind_addr{}; + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = htons(opts.port); + bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); + + if (bind(sockfd, reinterpret_cast(&bind_addr), sizeof(bind_addr)) != 0) { + perror("Failed to bind socket"); + fprintf(stderr, " Port %d may be in use — ensure no other CereLink client is running\n", opts.port); + close(sockfd); + close(phc_fd); + return 1; + } + + // Set up send address + struct sockaddr_in send_addr{}; + send_addr.sin_family = AF_INET; + send_addr.sin_port = htons(opts.port); + send_addr.sin_addr.s_addr = inet_addr(opts.device_addr); + + fprintf(stderr, "Connected to %s:%d\n", opts.device_addr, opts.port); + + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + // Collect results + std::vector results; + results.reserve(opts.count); + + // Print TSV header + printf("#\tt1_ptp\t\t\t\tt3_etime\t\t\tt3_stale\t\t\tt4_ptp\t\t\t\td1_ns\td2_ns\t\trtt_ns\tstaleness_ns\n"); + fflush(stdout); + + uint8_t recv_buf[65536]; + + for (int i = 0; i < opts.count && g_running; i++) { + // Build nPlay probe packet + cbPKT_NPLAY pkt{}; + pkt.cbpkt_header.chid = cbPKTCHAN_CONFIGURATION; + pkt.cbpkt_header.type = cbPKTTYPE_NPLAYSET; + pkt.cbpkt_header.dlen = cbPKTDLEN_NPLAY; + + // Read PHC just before sending → T1 + int64_t t1_ptp = read_phc_ns(phc_clockid); + pkt.stime = static_cast(t1_ptp); + + // Send + size_t pkt_size = cbPKT_HEADER_SIZE + (pkt.cbpkt_header.dlen * 4); + ssize_t sent = sendto(sockfd, &pkt, pkt_size, 0, + reinterpret_cast(&send_addr), sizeof(send_addr)); + if (sent < 0) { + fprintf(stderr, "Probe %d: sendto failed\n", i); + continue; + } + + // Receive loop — wait for NPLAYREP, skip other packets + bool got_reply = false; + auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(opts.recv_timeout_ms); + + while (std::chrono::steady_clock::now() < deadline) { + ssize_t n = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, nullptr, nullptr); + if (n < 0) { + break; // timeout or error + } + + // Read PHC immediately after recvfrom → T4 candidate + int64_t t4_ptp = read_phc_ns(phc_clockid); + + // Parse — may contain multiple packets in one datagram + size_t offset = 0; + while (offset + cbPKT_HEADER_SIZE <= static_cast(n)) { + const auto* hdr = reinterpret_cast(recv_buf + offset); + size_t pkt_len = cbPKT_HEADER_SIZE + (hdr->dlen * 4); + if (offset + pkt_len > static_cast(n)) break; + + if (hdr->type == cbPKTTYPE_NPLAYREP && + (hdr->chid & cbPKTCHAN_CONFIGURATION) == cbPKTCHAN_CONFIGURATION) { + + const auto* nplay = reinterpret_cast(recv_buf + offset); + + ProbeResult r; + r.t1_ptp = t1_ptp; + r.t3_etime = static_cast(nplay->etime); + r.t3_stale = static_cast(hdr->time); + r.t4_ptp = t4_ptp; + r.d1 = r.t3_etime - r.t1_ptp; + r.d2 = r.t4_ptp - r.t3_etime; + r.rtt = r.t4_ptp - r.t1_ptp; + r.staleness = r.t3_etime - r.t3_stale; + + printf("%d\t%ld\t%ld\t%ld\t%ld\t%ld\t%ld\t\t%ld\t%ld\n", + i, r.t1_ptp, r.t3_etime, r.t3_stale, r.t4_ptp, + r.d1, r.d2, r.rtt, r.staleness); + fflush(stdout); + + results.push_back(r); + got_reply = true; + break; + } + + offset += pkt_len; + } + + if (got_reply) break; + } + + if (!got_reply) { + fprintf(stderr, "Probe %d: no NPLAYREP received (timeout)\n", i); + } + + // Wait for next probe + if (i + 1 < opts.count && g_running) { + std::this_thread::sleep_for(std::chrono::milliseconds(opts.interval_ms)); + } + } + + // Print statistics + fprintf(stderr, "\n=== Probe Timing Statistics (%zu/%d probes received) ===\n", + results.size(), opts.count); + + if (!results.empty()) { + std::vector d1_vals, d2_vals, rtt_vals, stale_vals; + for (const auto& r : results) { + d1_vals.push_back(r.d1); + d2_vals.push_back(r.d2); + rtt_vals.push_back(r.rtt); + stale_vals.push_back(r.staleness); + } + + print_stats("D1 (forward)", d1_vals); + print_stats("D2 (reverse)", d2_vals); + print_stats("RTT", rtt_vals); + print_stats("Staleness", stale_vals); + } + + close(sockfd); + close(phc_fd); + return 0; +} + +#else // !__linux__ + +#include + +int main() { + fprintf(stderr, "probe_timing requires Linux (PTP hardware clock support)\n"); + return 1; +} + +#endif // __linux__ diff --git a/tools/validate_clock_sync/validate_clock_sync.cpp b/tools/validate_clock_sync/validate_clock_sync.cpp new file mode 100644 index 00000000..656be3cf --- /dev/null +++ b/tools/validate_clock_sync/validate_clock_sync.cpp @@ -0,0 +1,347 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +/// @file validate_clock_sync.cpp +/// @brief PTP-based validation of CereLink's ClockSync accuracy +/// +/// Compares ClockSync's estimated device-to-host offset against a PTP ground truth. +/// +/// Prerequisites (Raspberry Pi 5 connected to Hub1 via Ethernet): +/// +/// # Install linuxptp +/// sudo apt install linuxptp +/// +/// # Find the Ethernet interface connected to Hub1 (192.168.137.x subnet) +/// ip addr show # look for the interface with 192.168.137.x +/// +/// # Check PTP/timestamping capability and find PHC index +/// ethtool -T eth0 # replace eth0 with actual interface +/// # Look for "PTP Hardware Clock: N" → device is /dev/ptpN +/// +/// # Start ptp4l as PTP slave to Hub1 (grandmaster) +/// # Hub1 must be running as PTP master: ptp4l -i eth0 -H -m --priority1 0 +/// sudo ptp4l -i eth0 -s -m --ptp_minor_version 0 +/// # -i: interface, -s: slave-only, -m: log to stdout +/// # --ptp_minor_version 0: required if RPi5 defaults to PTPv2.1 (Hub1 uses PTPv2.0) +/// # Wait until "rms" in log output drops below 1000 ns +/// +/// Usage: +/// ./validate_clock_sync --ptp-device /dev/ptp0 [OPTIONS] +/// +/// Options: +/// --ptp-device PATH PTP hardware clock device (required) +/// --device-type TYPE Device type: hub1 (default), hub2, hub3, nsp, legacy_nsp, nplay +/// --duration SECS Duration in seconds (default: 60) +/// --probe-interval MS Clock probe interval in ms (default: 2000) +/// --sample-interval MS Sampling interval in ms (default: 100) +/// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifdef __linux__ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/// Welford's online algorithm for running mean/variance/min/max. +struct RunningStats { + int64_t n = 0; + double mean = 0.0; + double m2 = 0.0; // sum of squared deviations + int64_t min_val = std::numeric_limits::max(); + int64_t max_val = std::numeric_limits::min(); + + void update(int64_t x) { + ++n; + double d = static_cast(x); + double delta = d - mean; + mean += delta / n; + m2 += delta * (d - mean); + if (x < min_val) min_val = x; + if (x > max_val) max_val = x; + } + + double stddev() const { + return (n > 1) ? std::sqrt(m2 / (n - 1)) : 0.0; + } + + void reset() { *this = RunningStats{}; } + + void print(FILE* f, const char* label) const { + if (n == 0) { + fprintf(f, "%s: (no samples)\n", label); + return; + } + fprintf(f, "%s: n=%ld mean=%.0f ns stddev=%.0f ns min=%ld ns max=%ld ns\n", + label, n, mean, stddev(), min_val, max_val); + } +}; + +// Linux-specific: derive clockid_t from PTP device fd +// See kernel docs: Documentation/ptp/ptp.txt +// FD_TO_CLOCKID is defined in linux/posix-timers.h but not always available, +// so we define the standard formula here. +#ifndef FD_TO_CLOCKID +#define FD_TO_CLOCKID(fd) ((~(clockid_t)(fd) << 3) | 3) +#endif + +static volatile sig_atomic_t g_running = 1; + +static void signal_handler(int) { + g_running = 0; +} + +static inline int64_t timespec_to_ns(const struct timespec& ts) { + return static_cast(ts.tv_sec) * 1'000'000'000LL + ts.tv_nsec; +} + +struct Options { + const char* ptp_device = nullptr; + const char* device_type_str = "hub1"; + int duration_s = 60; + int probe_interval_ms = 2000; + int sample_interval_ms = 100; +}; + +static cbsdk::DeviceType parse_device_type(const char* str) { + if (strcmp(str, "hub1") == 0) return cbsdk::DeviceType::HUB1; + if (strcmp(str, "hub2") == 0) return cbsdk::DeviceType::HUB2; + if (strcmp(str, "hub3") == 0) return cbsdk::DeviceType::HUB3; + if (strcmp(str, "nsp") == 0) return cbsdk::DeviceType::NSP; + if (strcmp(str, "legacy_nsp") == 0) return cbsdk::DeviceType::LEGACY_NSP; + if (strcmp(str, "nplay") == 0) return cbsdk::DeviceType::NPLAY; + fprintf(stderr, "Unknown device type: %s\n", str); + exit(1); +} + +static void print_usage(const char* prog) { + fprintf(stderr, + "Usage: %s --ptp-device /dev/ptpN [OPTIONS]\n" + "\n" + "Options:\n" + " --ptp-device PATH PTP hardware clock device (required)\n" + " --device-type TYPE hub1 (default), hub2, hub3, nsp, legacy_nsp, nplay\n" + " --duration SECS Duration in seconds (default: 60)\n" + " --probe-interval MS Clock probe interval in ms (default: 2000)\n" + " --sample-interval MS Sampling interval in ms (default: 100)\n", + prog); +} + +/// Read PTP-vs-CLOCK_MONOTONIC offset using paired clock_gettime reads. +/// Returns the offset (phc_ns - mono_midpoint_ns) and the read uncertainty. +static bool read_ptp_offset(clockid_t phc_clockid, + int64_t& offset_ns, int64_t& uncertainty_ns) { + struct timespec mono1, phc, mono2; + + if (clock_gettime(CLOCK_MONOTONIC, &mono1) != 0) return false; + if (clock_gettime(phc_clockid, &phc) != 0) return false; + if (clock_gettime(CLOCK_MONOTONIC, &mono2) != 0) return false; + + int64_t mono1_ns = timespec_to_ns(mono1); + int64_t mono2_ns = timespec_to_ns(mono2); + int64_t phc_ns = timespec_to_ns(phc); + int64_t mono_mid = mono1_ns + (mono2_ns - mono1_ns) / 2; + + offset_ns = phc_ns - mono_mid; + uncertainty_ns = (mono2_ns - mono1_ns) / 2; + return true; +} + +int main(int argc, char* argv[]) { + Options opts; + + static struct option long_options[] = { + {"ptp-device", required_argument, nullptr, 'p'}, + {"device-type", required_argument, nullptr, 't'}, + {"duration", required_argument, nullptr, 'd'}, + {"probe-interval", required_argument, nullptr, 'P'}, + {"sample-interval", required_argument, nullptr, 's'}, + {"help", no_argument, nullptr, 'h'}, + {nullptr, 0, nullptr, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "p:t:d:P:s:h", long_options, nullptr)) != -1) { + switch (opt) { + case 'p': opts.ptp_device = optarg; break; + case 't': opts.device_type_str = optarg; break; + case 'd': opts.duration_s = atoi(optarg); break; + case 'P': opts.probe_interval_ms = atoi(optarg); break; + case 's': opts.sample_interval_ms = atoi(optarg); break; + case 'h': + print_usage(argv[0]); + return 0; + default: + print_usage(argv[0]); + return 1; + } + } + + if (!opts.ptp_device) { + fprintf(stderr, "Error: --ptp-device is required\n\n"); + print_usage(argv[0]); + return 1; + } + + // Open PTP hardware clock + int phc_fd = open(opts.ptp_device, O_RDONLY); + if (phc_fd < 0) { + perror("Failed to open PTP device"); + fprintf(stderr, " Device: %s\n", opts.ptp_device); + fprintf(stderr, " Hint: run with sudo, or add user to 'ptp' group\n"); + return 1; + } + clockid_t phc_clockid = FD_TO_CLOCKID(phc_fd); + + // Verify PHC is readable + { + int64_t test_offset, test_unc; + if (!read_ptp_offset(phc_clockid, test_offset, test_unc)) { + fprintf(stderr, "Failed to read PTP clock. Is ptp4l running?\n"); + close(phc_fd); + return 1; + } + fprintf(stderr, "PHC opened: %s (initial offset: %ld ns, read uncertainty: %ld ns)\n", + opts.ptp_device, test_offset, test_unc); + } + + // Connect to device via SdkSession + cbsdk::SdkConfig config; + config.device_type = parse_device_type(opts.device_type_str); + + fprintf(stderr, "Connecting to device (type: %s)...\n", opts.device_type_str); + auto result = cbsdk::SdkSession::create(config); + if (result.isError()) { + fprintf(stderr, "Failed to create SDK session: %s\n", result.error().c_str()); + close(phc_fd); + return 1; + } + auto& session = result.value(); + fprintf(stderr, "Connected.\n"); + + fprintf(stderr, "Waiting for initial clock probe response...\n"); + + // Brief wait for first probe response + std::this_thread::sleep_for(std::chrono::seconds(2)); + + // Install signal handlers for clean shutdown + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + // Print TSV header + printf("# elapsed_s\tptp_offset_ns\tcs_offset_ns\tcs_uncertainty_ns" + "\terror_ns\tphc_read_unc_ns\tepoch_mean_ns\tepoch_stddev_ns\n"); + fflush(stdout); + + auto start_time = std::chrono::steady_clock::now(); + auto last_probe_time = start_time; + auto end_time = start_time + std::chrono::seconds(opts.duration_s); + auto sample_interval = std::chrono::milliseconds(opts.sample_interval_ms); + auto probe_interval = std::chrono::milliseconds(opts.probe_interval_ms); + + RunningStats overall; // all error samples + RunningStats epoch; // error samples since last cs_offset change + int64_t prev_cs_offset = 0; // track when cs_offset changes (new probe selected) + bool have_prev_offset = false; + + // Send initial clock probe + session.sendClockProbe(); + + while (g_running && std::chrono::steady_clock::now() < end_time) { + auto now = std::chrono::steady_clock::now(); + + // Send periodic clock probes + if (now - last_probe_time >= probe_interval) { + auto probe_result = session.sendClockProbe(); + if (probe_result.isError()) { + fprintf(stderr, "Warning: sendClockProbe failed: %s\n", + probe_result.error().c_str()); + } + last_probe_time = now; + } + + // Read PTP offset (ground truth) + int64_t ptp_offset_ns = 0; + int64_t phc_read_uncertainty_ns = 0; + if (!read_ptp_offset(phc_clockid, ptp_offset_ns, phc_read_uncertainty_ns)) { + fprintf(stderr, "Warning: failed to read PHC\n"); + std::this_thread::sleep_for(sample_interval); + continue; + } + + // Read ClockSync offset + auto cs_offset = session.getClockOffsetNs(); + auto cs_uncertainty = session.getClockUncertaintyNs(); + + if (cs_offset.has_value()) { + int64_t error_ns = cs_offset.value() - ptp_offset_ns; + double elapsed_s = std::chrono::duration(now - start_time).count(); + + // Detect probe epoch change (cs_offset changed → new best probe selected) + if (have_prev_offset && cs_offset.value() != prev_cs_offset) { + if (epoch.n > 0) { + fprintf(stderr, " epoch ended: n=%ld mean=%.0f ns stddev=%.0f ns " + "min=%ld ns max=%ld ns\n", + epoch.n, epoch.mean, epoch.stddev(), + epoch.min_val, epoch.max_val); + } + epoch.reset(); + } + prev_cs_offset = cs_offset.value(); + have_prev_offset = true; + + overall.update(error_ns); + epoch.update(error_ns); + + printf("%.3f\t%ld\t%ld\t%ld\t%ld\t%ld\t%.0f\t%.0f\n", + elapsed_s, + ptp_offset_ns, + cs_offset.value(), + cs_uncertainty.value_or(0), + error_ns, + phc_read_uncertainty_ns, + epoch.mean, + epoch.stddev()); + fflush(stdout); + } else { + double elapsed_s = std::chrono::duration(now - start_time).count(); + fprintf(stderr, "%.3f: no clock sync data yet\n", elapsed_s); + } + + // Sleep until next sample + std::this_thread::sleep_for(sample_interval); + } + + // Print final summaries + fprintf(stderr, "\n"); + if (epoch.n > 0) { + epoch.print(stderr, "Last epoch"); + } + overall.print(stderr, "Overall"); + fprintf(stderr, "Done. Shutting down...\n"); + + session.stop(); + close(phc_fd); + return 0; +} + +#else // !__linux__ + +#include + +int main() { + fprintf(stderr, "validate_clock_sync requires Linux (PTP hardware clock support)\n"); + return 1; +} + +#endif // __linux__