diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index 13a2a8bfb..557001dae 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -82,6 +82,91 @@ jobs: ninja install env: BRAINFLOW_VERSION: ${{ steps.version.outputs.version }} + - name: Build Swift Package MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/swift_package + swift --version + BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift build + - name: Test Swift Package MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/swift_package + BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift test + - name: Swift CLI Synthetic Board MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/swift_package + BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift run brainflow-swift-cli + - name: Swift Examples MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/swift_package + for example in \ + swift-brainflow-get-data \ + swift-markers \ + swift-read-write-file \ + swift-downsampling \ + swift-transforms \ + swift-signal-filtering \ + swift-denoising \ + swift-band-power \ + swift-eeg-metrics \ + swift-ica + do + BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift run "$example" + done + - name: Build Apple XCFramework Artifacts + if: (matrix.os == 'macos-14') + run: | + $GITHUB_WORKSPACE/tools/apple/build_xcframeworks.sh \ + --output $GITHUB_WORKSPACE/build/apple_xcframeworks + env: + BRAINFLOW_VERSION: ${{ steps.version.outputs.version }} + - name: Verify Apple XCFramework Artifacts + if: (matrix.os == 'macos-14') + run: | + $GITHUB_WORKSPACE/tools/apple/verify_xcframeworks.sh $GITHUB_WORKSPACE/build/apple_xcframeworks + - name: Build Generated Swift Binary Package MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/build/apple_xcframeworks/BrainFlowSwiftBinaryPackage + swift build + - name: Test Generated Swift Binary Package MacOS + if: (matrix.os == 'macos-14') + run: | + $GITHUB_WORKSPACE/tools/apple/test_swift_binary_package.sh $GITHUB_WORKSPACE/build/apple_xcframeworks + - name: Build iOS Demo With Generated XCFrameworks + if: (matrix.os == 'macos-14') + run: | + BRAINFLOW_APPLE_XCFRAMEWORKS_DIR=$GITHUB_WORKSPACE/build/apple_xcframeworks/XCFrameworks \ + xcodebuild -quiet \ + -project $GITHUB_WORKSPACE/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj \ + -scheme BrainFlowiOSDemo \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath $GITHUB_WORKSPACE/build/ios-demo-derived \ + CODE_SIGNING_ALLOWED=NO \ + build + - name: Package macOS Demo With Generated XCFrameworks + if: (matrix.os == 'macos-14') + run: | + BRAINFLOW_APPLE_XCFRAMEWORKS_DIR=$GITHUB_WORKSPACE/build/apple_xcframeworks/XCFrameworks \ + $GITHUB_WORKSPACE/tools/apple/package_macos_demo_app.sh $GITHUB_WORKSPACE/build/apple_xcframeworks/BrainFlowMacDemo.app + - name: Upload Apple XCFramework Artifacts + if: (matrix.os == 'macos-14') + uses: actions/upload-artifact@v4 + with: + name: brainflow-apple-xcframeworks + path: | + ${{ github.workspace }}/build/apple_xcframeworks/BrainFlowAppleXCFrameworks.zip + ${{ github.workspace }}/build/apple_xcframeworks/BrainFlowAppleXCFrameworks.zip.sha256 + ${{ github.workspace }}/build/apple_xcframeworks/checksums.sha256 + ${{ github.workspace }}/build/apple_xcframeworks/swiftpm-checksums.txt + ${{ github.workspace }}/build/apple_xcframeworks/swiftpm-checksums.json + ${{ github.workspace }}/build/apple_xcframeworks/manifest.json + ${{ github.workspace }}/build/apple_xcframeworks/SwiftPMArtifacts/*.xcframework.zip + ${{ github.workspace }}/build/apple_xcframeworks/BrainFlowSwiftPackageRemote - name: Compile BrainFlow Ubuntu if: (matrix.os == 'ubuntu-latest') run: | diff --git a/.gitignore b/.gitignore index d12a18464..cce2a8d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ +swift_package/Artifacts/Apple/ # StyleCop StyleCopReport.xml @@ -96,6 +97,10 @@ ipch/ *.opensdf *.sdf *.cachefile +.swiftpm/ +.build/ +xcuserdata/ +DerivedData/ *.VC.db *.VC.VC.opendb @@ -343,6 +348,7 @@ ASALocalRun/ .vscode/ installed* +build_ios_sim/ compiled/ python/flowcat.egg-info/ .Rproj.user @@ -369,6 +375,7 @@ src/ml/train/data/ src/ml/train/data/*.onnx tools/brainflow-android.aar build_android_aar/ +build_apple/ tools/simpleble-bridge.jar tools/simpleble-bridge-classes/ tools/simpleble-bridge-sources.txt diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c805cb347 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,47 @@ +# Agent Notes + +## Apple XCFramework Artifacts + +Apple binary artifacts are generated build outputs. They contain framework-wrapped XCFrameworks +for iOS device, iOS simulator, and macOS app integration, plus a generated +`BrainFlowSwiftBinaryPackage` for app developers. + +Regenerate the artifacts from the repository root: + +```bash +tools/apple/regenerate_artifacts.sh +``` + +Verify an existing artifact tree: + +```bash +tools/apple/verify_xcframeworks.sh build/apple_xcframeworks +``` + +The regeneration script builds native BrainFlow Apple slices, packages the XCFrameworks, verifies +the required core frameworks, and creates: + +- `BrainFlowAppleXCFrameworks.zip` for complete archive downloads. +- `SwiftPMArtifacts/*.xcframework.zip` with each `.xcframework` at the ZIP root. +- `BrainFlowSwiftPackageRemote`, a generated URL-based SwiftPM package manifest. +- `swiftpm-checksums.txt` and `swiftpm-checksums.json` from `swift package compute-checksum`. + +By default, `tools/apple/regenerate_artifacts.sh` and `tools/apple/build_xcframeworks.sh` write to +`build/apple_xcframeworks`. Do not commit `build/`, `build_apple/`, `swift_package/Artifacts/Apple`, +or local `installed/` outputs. + +The iOS demo and macOS packaging script default to `build/apple_xcframeworks/XCFrameworks`. +CI may override artifact paths with +`BRAINFLOW_APPLE_XCFRAMEWORKS_DIR`. + +When changing Apple artifact generation, regenerate locally and run verification. CI uploads the +aggregate archive, the individual SwiftPM `.xcframework.zip` assets, checksums, generated remote +Swift package, and `manifest.json` as the distributable artifact set. Generated framework headers +and binaries should come from the scripts, not from files copied into the repository. + +For a BrainFlow release or Apple-library refresh, build from the release commit/tag with +`BRAINFLOW_VERSION` set. Set `BRAINFLOW_APPLE_RELEASE_BASE_URL` when release assets are hosted +outside the default GitHub Release tag URL. Regenerate artifacts, verify the generated tree, +smoke-test the generated Swift binary package, validate the iOS/macOS sample apps against the +regenerated XCFrameworks, and publish the individual SwiftPM ZIPs next to their checksum files. +Do not manually patch release frameworks after generation; update source and rerun the script. diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e6510dfb..b6689052d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ option (BUILD_ONNX "BUILD_ONNX" OFF) option (BUILD_TESTS "BUILD_TESTS" OFF) option (BUILD_PERIPHERY "BUILD_PERIPHERY" OFF) option (BRAINFLOW_COPY_TO_PACKAGE_DIRS "Copy built artifacts into language package folders" ON) +option (BRAINFLOW_APPLE_DYNAMIC_FRAMEWORKS "Build Apple iOS native libraries as dynamic libraries for framework/XCFramework packaging" OFF) set (BRAINFLOW_IOS OFF) if (CMAKE_SYSTEM_NAME STREQUAL "iOS") @@ -38,6 +39,11 @@ set (BRAINFLOW_CORE_LIBRARY_TYPE SHARED) if (BRAINFLOW_IOS) set (BRAINFLOW_CORE_LIBRARY_TYPE STATIC) + if (BRAINFLOW_APPLE_DYNAMIC_FRAMEWORKS) + message (STATUS "Building iOS BrainFlow native libraries as dynamic libraries for XCFramework packaging.") + set (BRAINFLOW_CORE_LIBRARY_TYPE SHARED) + endif () + if (BRAINFLOW_COPY_TO_PACKAGE_DIRS) message (STATUS "Disabling BRAINFLOW_COPY_TO_PACKAGE_DIRS for iOS builds.") set (BRAINFLOW_COPY_TO_PACKAGE_DIRS OFF CACHE BOOL "Copy built artifacts into language package folders" FORCE) diff --git a/docs/BuildBrainFlow.rst b/docs/BuildBrainFlow.rst index 2832c68f3..4a8a14e2e 100644 --- a/docs/BuildBrainFlow.rst +++ b/docs/BuildBrainFlow.rst @@ -152,7 +152,42 @@ Rust Swift ------- -You can build Swift binding for BrainFlow using xcode. Before that you need to compile C/C++ code :ref:`compilation-label` and ensure that native libraries are properly placed. Keep in mind that currently it supports only MacOS. +You can build Swift bindings for BrainFlow with Swift Package Manager or Xcode. Before running examples or tests you need to compile C/C++ code :ref:`compilation-label` and ensure that native libraries are available to the Swift runtime loader. + +Local build example: + +.. code-block:: bash + + python3 tools/build.py + cd swift_package + BRAINFLOW_LIB_DIR=../installed/lib swift build + BRAINFLOW_LIB_DIR=../installed/lib swift test + BRAINFLOW_LIB_DIR=../installed/lib swift run brainflow-swift-cli + BRAINFLOW_LIB_DIR=../installed/lib swift run swift-brainflow-get-data + +The Swift package intentionally does not vendor BrainFlow native binaries. Like source builds for other bindings, it dynamically loads native libraries built from this repository. The loader searches :code:`BRAINFLOW_LIB_DIR`, system library paths, :code:`installed/lib`, and app bundle resource/framework directories for :code:`libBoardController`, :code:`libDataHandler`, and :code:`libMLModule`. + +For production iOS and macOS applications, use Apple XCFramework artifacts and the generated Swift binary package. Regenerate Apple artifacts with: + +.. code-block:: bash + + tools/apple/regenerate_artifacts.sh + tools/apple/verify_xcframeworks.sh build/apple_xcframeworks + +The default generated artifact directory is :code:`build/apple_xcframeworks`. It contains :code:`XCFrameworks`, :code:`BrainFlowSwiftBinaryPackage`, :code:`BrainFlowSwiftPackageRemote`, :code:`SwiftPMArtifacts/*.xcframework.zip`, :code:`BrainFlowAppleXCFrameworks.zip`, and checksum files. These generated headers and binaries are release/CI artifacts, not source files committed to the repository. + +The generated :code:`BrainFlowSwiftBinaryPackage` contains the Swift API and binary targets for :code:`BoardController.xcframework`, :code:`DataHandler.xcframework`, and :code:`MLModule.xcframework`. Add this package to an app through Xcode or Swift Package Manager so embedded frameworks are handled by standard Apple build, embed, and signing flows. + +For public Swift Package distribution, publish the individual zips from :code:`SwiftPMArtifacts` and use :code:`BrainFlowSwiftPackageRemote`, which declares URL-based binary targets with checksums generated by :code:`swift package compute-checksum`. Set :code:`BRAINFLOW_APPLE_RELEASE_BASE_URL` before regeneration if release assets are hosted outside the default GitHub Release tag URL. + +The macOS demo can be built with: + +.. code-block:: bash + + cd swift_package + BRAINFLOW_LIB_DIR=../installed/lib swift run BrainFlowMacDemo + +iOS and Mac App Store sample source and release-preparation notes are available in :code:`swift_package/examples/apps`, :code:`swift_package/Docs/AppleBinaryDistribution.md`, and :code:`swift_package/Docs/AppStoreReadiness.md`. App runtime support requires matching BrainFlow native frameworks embedded and signed inside the app bundle; App Store builds should not depend on :code:`BRAINFLOW_LIB_DIR` or local development directories. Docker Image -------------- diff --git a/docs/Examples.rst b/docs/Examples.rst index dab898f10..ccdce30d4 100644 --- a/docs/Examples.rst +++ b/docs/Examples.rst @@ -605,6 +605,8 @@ Typescript ICA Swift ------------ +The Swift examples below are also Swift Package Manager executable products. After building native BrainFlow libraries, run them from :code:`swift_package` with :code:`BRAINFLOW_LIB_DIR=../installed/lib swift run `. For example, :code:`swift run swift-brainflow-get-data`. + Swift Get Data from a Board ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/UserAPI.rst b/docs/UserAPI.rst index 642443a6b..735b6f4c1 100644 --- a/docs/UserAPI.rst +++ b/docs/UserAPI.rst @@ -167,7 +167,7 @@ Example: Swift ------ -Swift binding calls C/C++ code as any other binding. Use Swift examples and API reference for other languaes as a starting point. +Swift binding calls C/C++ code as any other binding. The Swift package exposes BoardShim, DataFilter, MLModel, params, errors, and BrainFlow constants using the same public API groups as Python and Java. In-place signal-processing methods use Swift :code:`inout [Double]` arguments. Example: diff --git a/src/utils/inc/runtime_dll_loader.h b/src/utils/inc/runtime_dll_loader.h index 0d4558908..6f9e7b61e 100644 --- a/src/utils/inc/runtime_dll_loader.h +++ b/src/utils/inc/runtime_dll_loader.h @@ -5,6 +5,8 @@ #include #else #include +#include +#include #endif @@ -19,7 +21,8 @@ class DLLLoader public: DLLLoader (const char *dll_path) { - strcpy (this->dll_path, dll_path); + strncpy (this->dll_path, dll_path, sizeof (this->dll_path) - 1); + this->dll_path[sizeof (this->dll_path) - 1] = '\0'; this->lib_instance = NULL; } @@ -68,11 +71,15 @@ class DLLLoader { // RTLD_DEEPBIND will search for symbols in loaded lib first and after that in global // scope - lib_instance = dlopen (this->dll_path, RTLD_LAZY | RTLD_DEEPBIND); - if (!lib_instance) + for (const std::string &candidate : get_dlopen_candidates ()) { - return false; + lib_instance = dlopen (candidate.c_str (), RTLD_LAZY | RTLD_DEEPBIND); + if (lib_instance) + { + return true; + } } + return false; } return true; } @@ -97,6 +104,115 @@ class DLLLoader #endif private: +#ifndef _WIN32 + std::vector get_dlopen_candidates () const + { + std::vector candidates; + append_unique (candidates, this->dll_path); + +#ifdef __APPLE__ + std::string original_path = this->dll_path; + std::string directory = parent_directory (original_path); + std::string file_name = last_path_component (original_path); + std::string framework_name = apple_framework_name (file_name); + + if (!framework_name.empty ()) + { + append_unique (candidates, framework_name + ".framework/" + framework_name); + append_unique (candidates, "@rpath/" + framework_name + ".framework/" + framework_name); + if (!directory.empty ()) + { + append_unique ( + candidates, directory + framework_name + ".framework/" + framework_name); + std::string framework_parent = parent_frameworks_directory (directory); + if (!framework_parent.empty ()) + { + append_unique (candidates, + framework_parent + framework_name + ".framework/" + framework_name); + } + } + } +#endif + return candidates; + } + + static void append_unique (std::vector &values, const std::string &value) + { + if (value.empty ()) + { + return; + } + for (const std::string &existing : values) + { + if (existing == value) + { + return; + } + } + values.push_back (value); + } + +#ifdef __APPLE__ + static std::string last_path_component (const std::string &path) + { + size_t slash = path.find_last_of ('/'); + if (slash == std::string::npos) + { + return path; + } + return path.substr (slash + 1); + } + + static std::string parent_directory (const std::string &path) + { + size_t slash = path.find_last_of ('/'); + if (slash == std::string::npos) + { + return ""; + } + return path.substr (0, slash + 1); + } + + static std::string parent_frameworks_directory (const std::string &directory) + { + std::string normalized = directory; + if (!normalized.empty () && normalized[normalized.size () - 1] == '/') + { + normalized.resize (normalized.size () - 1); + } + + size_t framework_suffix = normalized.rfind (".framework"); + if (framework_suffix == std::string::npos) + { + return ""; + } + + size_t framework_dir_start = normalized.find_last_of ('/', framework_suffix); + if (framework_dir_start == std::string::npos) + { + return ""; + } + return normalized.substr (0, framework_dir_start + 1); + } + + static std::string apple_framework_name (const std::string &file_name) + { + std::string name = file_name; + const std::string dylib_suffix = ".dylib"; + if (name.size () > dylib_suffix.size () && + name.substr (name.size () - dylib_suffix.size ()) == dylib_suffix) + { + name.resize (name.size () - dylib_suffix.size ()); + } + if (name.size () > 3 && name.substr (0, 3) == "lib") + { + name = name.substr (3); + } + return name; + } +#endif +#endif + char dll_path[1024]; #ifdef _WIN32 HINSTANCE lib_instance; diff --git a/swift_package/Docs/APIParity.md b/swift_package/Docs/APIParity.md new file mode 100644 index 000000000..5ce223368 --- /dev/null +++ b/swift_package/Docs/APIParity.md @@ -0,0 +1,40 @@ +# Swift API Parity + +Swift mirrors the public Python and Java API shape, with Swift-native signatures where required by the language. + +## BoardShim + +Implemented: + +- Session lifecycle: `prepare_session`, `start_stream`, `stop_stream`, `release_session`, `release_all_sessions`, `is_prepared`. +- Data access: `get_current_board_data`, `get_board_data`, `get_board_data_count`, `get_board_id`, `get_board_sampling_rate`, `insert_marker`. +- Stream/config: `add_streamer`, `delete_streamer`, `config_board`, `config_board_with_bytes`. +- Metadata: sampling rate, package/timestamp/marker/battery rows, row count, EEG names, board presets, board description, device name, all channel getters exposed by the C ABI. +- Logging/version: board logger controls, log file, log message, version. + +## DataFilter + +Implemented: + +- Filters/noise/detrend: lowpass, highpass, bandpass, bandstop, environmental noise removal, rolling filter, detrend. +- Transforms/features: downsampling, wavelet transform/inverse/denoising, CSP, windowing, FFT/IFFT, PSD/Welch, band powers, ICA. +- Helpers: stddev, railed percentage, oxygen level, heart rate, peak detection, nearest power of two, file IO, reshape helpers, logging, version. + +Swift differs from Java/Python for in-place operations by using `inout [Double]`. + +## MLModel + +Implemented: + +- `BrainFlowModelParams` +- `prepare` +- `release` +- `predict` +- logger controls +- `release_all` +- `get_version` + +## Known Packaging Notes + +- Runtime calls require native BrainFlow libraries to be available through `BRAINFLOW_LIB_DIR`, system loader paths, `installed/lib`, or app bundle resources. +- iOS execution depends on shipping BrainFlow native binaries compiled for iOS. The Swift API compiles for iOS, but native libraries still determine runtime support. diff --git a/swift_package/Docs/AppStoreReadiness.md b/swift_package/Docs/AppStoreReadiness.md new file mode 100644 index 000000000..172b8144b --- /dev/null +++ b/swift_package/Docs/AppStoreReadiness.md @@ -0,0 +1,50 @@ +# App Store Readiness + +This checklist is intentionally separate from the sample source because final App Store submission requires a developer account, bundle IDs, certificates, provisioning profiles, App Store Connect records, screenshots, and final product metadata. + +## Shared + +- Build with the current App Store-required SDK in Xcode. +- Replace placeholder bundle IDs. +- Add production app icons and screenshots. +- Keep the synthetic-board demo path available so App Review can exercise the app without external hardware. +- Embed BrainFlow XCFramework products in the app bundle and sign them with the app. +- Use the generated `BrainFlowSwiftBinaryPackage` for production app integration. Do not rely on `BRAINFLOW_LIB_DIR`, local `installed/lib` folders, or loose development dylibs in App Store builds. +- Confirm final privacy answers reflect real-board connectivity, Bluetooth, networking, files, and any third-party native dependencies actually shipped. +- Run an archive build and install it on a physical device or clean Mac before upload. +- Verify the archive contains only device slices for iOS, with embedded framework install names in the form `@rpath/.framework/`. + +## iOS + +- Use `examples/apps/ios/BrainFlowiOSDemo` as the Xcode app project. +- Use the generated Swift binary package or embed framework slices from `build/apple_xcframeworks/XCFrameworks`. +- Provide iOS-compatible BrainFlow native binaries through XCFrameworks. The high-level Swift package compiles for iOS, but BrainFlow calls can only run when matching native frameworks are embedded and signed. +- For Muse native BLE boards, build BrainFlow native libraries with BLE support enabled for the target platform and keep the Bluetooth privacy string in the app plist. +- Keep permissions minimal. The synthetic-board demo needs no network or file permissions. +- Test via TestFlight before App Store submission. + +## macOS + +- Use `swift_package` product `BrainFlowMacDemo` for local development, or package it with `tools/apple/package_macos_demo_app.sh` for app-bundle smoke testing. +- Add the files from `examples/apps/macos/BrainFlowMacDemo`. +- Enable App Sandbox. +- Embed and sign BrainFlow XCFramework products. +- Verify dynamic loading works inside the app bundle, not only with `BRAINFLOW_LIB_DIR`. + +## Production Gate + +- Swift package builds. +- Swift tests pass with native libraries present. +- CLI smoke test succeeds with the synthetic board. +- `tools/apple/build_xcframeworks.sh` and `tools/apple/verify_xcframeworks.sh` pass. +- `tools/apple/regenerate_artifacts.sh` refreshes `build/apple_xcframeworks`, and `tools/apple/verify_xcframeworks.sh build/apple_xcframeworks` passes. +- The Apple release artifact set includes the individual SwiftPM XCFramework zips, + `swiftpm-checksums.txt`, `swiftpm-checksums.json`, `BrainFlowSwiftPackageRemote`, + `BrainFlowAppleXCFrameworks.zip`, `BrainFlowAppleXCFrameworks.zip.sha256`, `checksums.sha256`, + and `manifest.json` from the same build. +- `manifest.json` records the BrainFlow version, source revision, toolchain versions, deployment + targets, optional native feature flags, SwiftPM release URL base, and binary target checksums. +- A clean app consumes `BrainFlowSwiftBinaryPackage` without building native BrainFlow locally. +- iOS and macOS app targets launch, handle missing native frameworks gracefully, and run the synthetic-board workflow when frameworks are embedded. +- Accessibility labels and dynamic text behavior are reviewed in the sample apps. +- Crash logs are clean after repeated start, stop, read, and release cycles. diff --git a/swift_package/Docs/AppleBinaryDistribution.md b/swift_package/Docs/AppleBinaryDistribution.md new file mode 100644 index 000000000..24c2dfbd4 --- /dev/null +++ b/swift_package/Docs/AppleBinaryDistribution.md @@ -0,0 +1,156 @@ +# Apple Binary Distribution + +BrainFlow Swift source bindings can be built directly from this repository, but production iOS +and macOS apps should consume signed, embedded Apple frameworks instead of loose local dylibs. +The Apple packaging workflow creates framework-wrapped XCFrameworks and a generated SwiftPM +binary package for app developers. + +## Generated Artifacts + +Regenerate the Apple artifacts from source: + +```bash +tools/apple/regenerate_artifacts.sh +``` + +Verify the generated artifact tree: + +```bash +tools/apple/verify_xcframeworks.sh build/apple_xcframeworks +``` + +The default output is: + +- `build/apple_xcframeworks/XCFrameworks/*.xcframework` +- `build/apple_xcframeworks/BrainFlowSwiftBinaryPackage` +- `build/apple_xcframeworks/BrainFlowSwiftPackageRemote` +- `build/apple_xcframeworks/SwiftPMArtifacts/*.xcframework.zip` +- `build/apple_xcframeworks/BrainFlowAppleXCFrameworks.zip` + +The generated package contains the BrainFlow Swift API plus binary targets for: + +- `BoardController.xcframework` +- `DataHandler.xcframework` +- `MLModule.xcframework` + +The artifact directory also includes `checksums.sha256` and +`BrainFlowAppleXCFrameworks.zip.sha256`; `tools/apple/verify_xcframeworks.sh` validates both. +It also includes `swiftpm-checksums.txt` and `swiftpm-checksums.json`, generated with +`swift package compute-checksum` for the SwiftPM release ZIPs. + +Generated artifacts are not committed to the source repository. CI uploads +`BrainFlowAppleXCFrameworks.zip` as a workflow artifact for complete archive downloads. Releases +should also publish the individual files in `SwiftPMArtifacts` because SwiftPM URL-based binary +targets expect a ZIP archive with the `.xcframework` at the archive root. + +Add `BrainFlowSwiftBinaryPackage` to an app in Xcode or with Swift Package Manager. Xcode embeds +and signs the binary frameworks during app builds. + +Use `BrainFlowSwiftPackageRemote` as the release-facing Swift package template when publishing +from GitHub Releases, a CDN, or another public HTTPS host. It declares URL-based binary targets for +`BoardController`, `DataHandler`, and `MLModule` using the checksums from +`swiftpm-checksums.json`. + +The artifact directory may also include optional board vendor XCFrameworks such as Muse, Ganglion, +BrainBit, or NeuroSDK libraries. Those are not dependencies of the core `BrainFlow` Swift product; +embed the optional vendor frameworks explicitly when the app enables boards that require them. + +## Supported Slices + +The packaging script builds and verifies: + +- macOS universal framework slices +- iOS device framework slices +- iOS simulator framework slices + +Core BrainFlow libraries are required for every slice. Optional vendor libraries are packaged only +for platforms where the selected native build options produce valid Apple binaries. + +## Optional Native Features + +The default artifact build keeps optional native SDKs disabled for a small, App Store-friendly +synthetic-board package. Enable optional features with environment variables: + +```bash +BRAINFLOW_APPLE_BUILD_BLE=ON \ +BRAINFLOW_APPLE_BUILD_BLUETOOTH=ON \ +BRAINFLOW_APPLE_BUILD_ONNX=ON \ +tools/apple/build_xcframeworks.sh +``` + +Only ship optional vendor frameworks that are supported on the Apple platform you target and that +match your App Store privacy and permission answers. + +## Release Maintenance + +Apple library releases should be reproducible from the same source revision as the BrainFlow +release. For each BrainFlow release or Apple-library refresh: + +1. Build from a clean checkout of the release commit or tag. +2. Set `BRAINFLOW_VERSION` to the release version. If the binary assets will not be hosted under + `https://github.com/brainflow-dev/brainflow/releases/download/$BRAINFLOW_VERSION`, set + `BRAINFLOW_APPLE_RELEASE_BASE_URL` to the exact public URL prefix for the `.xcframework.zip` + assets. +3. Run `tools/apple/regenerate_artifacts.sh`. +4. Verify `build/apple_xcframeworks` with `tools/apple/verify_xcframeworks.sh`. +5. Run the generated Swift binary package smoke test: + +```bash +tools/apple/test_swift_binary_package.sh build/apple_xcframeworks +``` + +6. Build the iOS demo and package the macOS demo against `build/apple_xcframeworks/XCFrameworks`. +7. Publish `SwiftPMArtifacts/BoardController.xcframework.zip`, + `SwiftPMArtifacts/DataHandler.xcframework.zip`, `SwiftPMArtifacts/MLModule.xcframework.zip`, + `swiftpm-checksums.txt`, `swiftpm-checksums.json`, `BrainFlowAppleXCFrameworks.zip`, + `BrainFlowAppleXCFrameworks.zip.sha256`, `checksums.sha256`, and `manifest.json` from the same + CI run or GitHub Release. + +The generated `manifest.json` records the BrainFlow version, source revision, Xcode, CMake, Ninja, +deployment targets, optional native feature flags, SwiftPM asset URL base, and SwiftPM binary +target checksums. Use it as the compatibility record for supporting downstream developers and for +reproducing a release later. + +When BrainFlow native headers or libraries change, do not edit framework contents by hand. Update +the source, rerun the Apple artifact script, and release the newly generated zip plus checksums. +The packaging script copies public headers from the current source/install tree into framework +slices as part of generation. + +For a remote Swift Package distribution, publish each XCFramework archive from a release URL and +use URL-based `binaryTarget` declarations with checksums generated by +`swift package compute-checksum`. The archive must contain the `.xcframework` at the ZIP root, +which `tools/apple/build_xcframeworks.sh` enforces for files in `SwiftPMArtifacts`. For local +development or downloaded release archives, the generated `BrainFlowSwiftBinaryPackage` uses +path-based binary targets. Apple documents both distribution modes in +https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages. + +Apple's XCFramework guidance is the baseline for this workflow: +https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle. + +## App Store Practice + +Production app builds should follow normal Apple SDK distribution rules: + +- Use XCFrameworks for multi-platform binary distribution. +- Embed dynamic frameworks in the app bundle under `Frameworks`. +- Let Xcode sign embedded frameworks with the app during build/archive. +- Do not ship simulator slices in iOS device archives. +- Verify `@rpath/.framework/` install names. +- Keep privacy manifests and app privacy answers aligned with the actual native binaries shipped. +- For public SDK-style distribution, sign release XCFrameworks with an Apple Developer Program + identity when the release process has access to one. CI smoke builds may use ad-hoc signing only + for local validation. + +## Sample App Checks + +The iOS demo embeds framework slices from `build/apple_xcframeworks/XCFrameworks` by default. +Override the artifact directory with `BRAINFLOW_APPLE_XCFRAMEWORKS_DIR`. + +The macOS SwiftUI demo can be packaged as an app bundle: + +```bash +tools/apple/package_macos_demo_app.sh +``` + +Run app smoke tests with the synthetic board and without `BRAINFLOW_LIB_DIR` to verify that runtime +loading works from embedded frameworks, not from local development directories. diff --git a/swift_package/Package.swift b/swift_package/Package.swift new file mode 100644 index 000000000..142926dd2 --- /dev/null +++ b/swift_package/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let exampleTargets: [(product: String, target: String, path: String)] = [ + ("swift-brainflow-get-data", "SwiftBrainFlowGetDataExample", "examples/tests/brainflow_get_data"), + ("swift-markers", "SwiftMarkersExample", "examples/tests/markers"), + ("swift-read-write-file", "SwiftReadWriteFileExample", "examples/tests/read_write_file"), + ("swift-downsampling", "SwiftDownsamplingExample", "examples/tests/downsampling"), + ("swift-transforms", "SwiftTransformsExample", "examples/tests/transforms"), + ("swift-signal-filtering", "SwiftSignalFilteringExample", "examples/tests/signal_filtering"), + ("swift-denoising", "SwiftDenoisingExample", "examples/tests/denoising"), + ("swift-band-power", "SwiftBandPowerExample", "examples/tests/band_power"), + ("swift-eeg-metrics", "SwiftEEGMetricsExample", "examples/tests/eeg_metrics"), + ("swift-ica", "SwiftICAExample", "examples/tests/ica") +] + +let package = Package( + name: "BrainFlow", + platforms: [ + .macOS(.v12), + .iOS(.v15) + ], + products: [ + .library(name: "BrainFlow", targets: ["BrainFlow"]), + .executable(name: "brainflow-swift-cli", targets: ["BrainFlowCLI"]), + .executable(name: "BrainFlowMacDemo", targets: ["BrainFlowMacDemo"]) + ] + exampleTargets.map { .executable(name: $0.product, targets: [$0.target]) }, + targets: [ + .target( + name: "BrainFlow" + ), + .target( + name: "BrainFlowExampleSupport", + dependencies: ["BrainFlow"], + path: "examples/tests/support" + ), + .executableTarget( + name: "BrainFlowCLI", + dependencies: ["BrainFlow"] + ), + .executableTarget( + name: "BrainFlowMacDemo", + dependencies: ["BrainFlow"] + ), + .testTarget( + name: "BrainFlowTests", + dependencies: ["BrainFlow"] + ) + ] + exampleTargets.map { + .executableTarget( + name: $0.target, + dependencies: ["BrainFlow", "BrainFlowExampleSupport"], + path: $0.path + ) + } +) diff --git a/swift_package/README.md b/swift_package/README.md new file mode 100644 index 000000000..2f457b5cc --- /dev/null +++ b/swift_package/README.md @@ -0,0 +1,77 @@ +# BrainFlow Swift + +Swift bindings call BrainFlow's native C ABI through runtime dynamic loading. Build the native libraries first, then point Swift at the installed library directory. + +```bash +cd .. +python3 tools/build.py + +cd swift_package +BRAINFLOW_LIB_DIR=../installed/lib swift test +BRAINFLOW_LIB_DIR=../installed/lib swift run brainflow-swift-cli +BRAINFLOW_LIB_DIR=../installed/lib swift run swift-brainflow-get-data +``` + +The Swift package does not vendor native BrainFlow binaries. Build native libraries from this repository and provide them at runtime. The loader searches `BRAINFLOW_LIB_DIR`, `DYLD_LIBRARY_PATH`, `LD_LIBRARY_PATH`, `installed/lib`, app bundle resources, and the current directory for: + +- `libBoardController.dylib` +- `libDataHandler.dylib` +- `libMLModule.dylib` + +For production iOS and macOS apps, use the Apple XCFramework packaging workflow instead of loose +development dylibs: + +```bash +tools/apple/build_xcframeworks.sh +tools/apple/verify_xcframeworks.sh build/apple_xcframeworks +``` + +The generated `build/apple_xcframeworks/BrainFlowSwiftBinaryPackage` is a normal Swift Package +with binary targets for the BrainFlow native frameworks. Add that package to an app in Xcode so +embedded frameworks are handled by the standard Xcode build, embed, and signing flow. + +For public SwiftPM distribution, publish the individual +`build/apple_xcframeworks/SwiftPMArtifacts/*.xcframework.zip` files and use the generated +`build/apple_xcframeworks/BrainFlowSwiftPackageRemote` package. Its URL-based binary targets use +checksums generated by `swift package compute-checksum`, matching Xcode's binary package +validation flow. + +Regenerate and verify the Apple artifacts with: + +```bash +tools/apple/regenerate_artifacts.sh +tools/apple/verify_xcframeworks.sh build/apple_xcframeworks +``` + +See `Docs/AppleBinaryDistribution.md` for artifact details and App Store packaging notes. + +On Linux the equivalent `.so` names are used. + +## API Coverage + +The package exposes Swift equivalents for the public Python/Java binding surface: + +- `BoardShim`: session lifecycle, streamers, data reads, markers, config, board metadata, logging, versions. +- `DataFilter`: filters, denoising, FFT/IFFT, PSD, band powers, CSP, ICA, file IO, statistics, logging, versions. +- `MLModel`: model params, prepare/release, predict, logging, versions. +- `BrainFlowInputParams`, `BrainFlowModelParams`, `BrainFlowError`, and public constants/enums. + +Swift in-place signal-processing methods take `inout [Double]`, for example: + +```swift +var data = Array(0..<256).map { sin(Double($0) / 10.0) } +try DataFilter.perform_lowpass( + data: &data, + sampling_rate: 250, + cutoff: 30.0, + order: 4, + filter_type: .BUTTERWORTH, + ripple: 0.0 +) +``` + +## Apps + +- `swift run BrainFlowMacDemo` builds a simple macOS SwiftUI demo against the synthetic board. +- `examples/apps/ios/BrainFlowiOSDemo` contains an Xcode iOS app project with synthetic-board autorun, Muse/native BLE board selection, and an EEG plot. +- `examples/apps/macos/BrainFlowMacDemo` contains Mac App Store release-prep metadata for an Xcode app bundle. diff --git a/swift_package/Sources/BrainFlow/BoardShim.swift b/swift_package/Sources/BrainFlow/BoardShim.swift new file mode 100644 index 000000000..4aa5d270c --- /dev/null +++ b/swift_package/Sources/BrainFlow/BoardShim.swift @@ -0,0 +1,655 @@ +import Foundation + +public final class BoardShim { + private let board_id: Int + private let input_params: BrainFlowInputParams + private let serialized_params: String + + public init(board_id: Int, input_params: BrainFlowInputParams = BrainFlowInputParams()) throws { + self.board_id = board_id + self.input_params = input_params + serialized_params = try input_params.to_json() + } + + public convenience init(board_id: BoardIds, input_params: BrainFlowInputParams = BrainFlowInputParams()) throws { + try self.init(board_id: board_id.rawValue, input_params: input_params) + } + + deinit { + try? release_session() + } + + public func prepare_session() throws { + try serialized_params.withCString { params in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.prepare_session(CInt(board_id), params), "Error in prepare_session") + } + } + } + + public func start_stream(buffer_size: Int = 450_000, streamer_params: String = "") throws { + guard buffer_size > 0 else { throw invalidArguments("buffer_size must be positive") } + try serialized_params.withCString { params in + try streamer_params.withCString { streamer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.start_stream(CInt(buffer_size), streamer, CInt(board_id), params), + "Error in start_stream" + ) + } + } + } + } + + public func stop_stream() throws { + try serialized_params.withCString { params in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.stop_stream(CInt(board_id), params), "Error in stop_stream") + } + } + } + + public func release_session() throws { + try serialized_params.withCString { params in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.release_session(CInt(board_id), params), "Error in release_session") + } + } + } + + public func add_streamer(_ streamer: String, preset: BrainFlowPresets = .DEFAULT_PRESET) throws { + try serialized_params.withCString { params in + try streamer.withCString { streamerPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.add_streamer(streamerPtr, CInt(preset.rawValue), CInt(board_id), params), + "Error in add_streamer" + ) + } + } + } + } + + public func delete_streamer(_ streamer: String, preset: BrainFlowPresets = .DEFAULT_PRESET) throws { + try serialized_params.withCString { params in + try streamer.withCString { streamerPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.delete_streamer(streamerPtr, CInt(preset.rawValue), CInt(board_id), params), + "Error in delete_streamer" + ) + } + } + } + } + + public func config_board(_ config: String) throws -> String { + try serialized_params.withCString { params in + try config.withCString { configPtr in + var response = [CChar](repeating: 0, count: 16_000) + let responseCapacity = response.count + var responseLen: CInt = 0 + try response.withUnsafeMutableBufferPointer { responsePtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.config_board(configPtr, responsePtr.baseAddress, &responseLen, CInt(responseCapacity), CInt(board_id), params), + "Error in config_board" + ) + } + } + let returnedCount = min(max(Int(responseLen), 0), responseCapacity) + return String(bytes: response.prefix(returnedCount).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } + } + } + + public func config_board_with_bytes(_ bytes: [UInt8]) throws { + guard !bytes.isEmpty else { throw invalidArguments("bytes must be non-empty") } + try serialized_params.withCString { params in + try bytes.withUnsafeBufferPointer { buffer in + try buffer.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buffer.count) { bytesPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.config_board_with_bytes(bytesPtr, CInt(buffer.count), CInt(board_id), params), + "Error in config_board_with_bytes" + ) + } + } + } + } + } + + public func get_current_board_data(num_samples: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [[Double]] { + guard num_samples > 0 else { throw invalidArguments("num_samples must be positive") } + let rows = try Self.get_num_rows(board_id: board_id, preset: preset) + var data = [Double](repeating: 0.0, count: rows * num_samples) + var returnedSamples: CInt = 0 + try serialized_params.withCString { params in + try data.withUnsafeMutableBufferPointer { dataPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.get_current_board_data(CInt(num_samples), CInt(preset.rawValue), dataPtr.baseAddress, &returnedSamples, CInt(board_id), params), + "Error in get_current_board_data" + ) + } + } + } + let count = Int(returnedSamples) + return BrainFlowArray.reshape_data_to_2d(num_rows: rows, num_cols: count, linear_buffer: Array(data.prefix(rows * count))) + } + + public func get_board_data(_ num_datapoints: Int? = nil, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [[Double]] { + let rows = try Self.get_num_rows(board_id: board_id, preset: preset) + let count = try num_datapoints ?? get_board_data_count(preset: preset) + guard count >= 0 else { throw invalidArguments("num_datapoints must be non-negative") } + var data = [Double](repeating: 0.0, count: rows * count) + try serialized_params.withCString { params in + try data.withUnsafeMutableBufferPointer { dataPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.get_board_data(CInt(count), CInt(preset.rawValue), dataPtr.baseAddress, CInt(board_id), params), + "Error in get_board_data" + ) + } + } + } + return BrainFlowArray.reshape_data_to_2d(num_rows: rows, num_cols: count, linear_buffer: data) + } + + public func get_board_data_count(preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try serialized_params.withCString { params in + var result: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.get_board_data_count(CInt(preset.rawValue), &result, CInt(board_id), params), + "Error in get_board_data_count" + ) + } + return Int(result) + } + } + + public func get_board_id() -> Int { + board_id + } + + public func get_board_sampling_rate(preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try serialized_params.withCString { params in + var rate: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.get_board_sampling_rate(CInt(preset.rawValue), &rate, CInt(board_id), params), + "Error in get_board_sampling_rate" + ) + } + return Int(rate) + } + } + + public func insert_marker(_ value: Double, preset: BrainFlowPresets = .DEFAULT_PRESET) throws { + try serialized_params.withCString { params in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.insert_marker(value, CInt(preset.rawValue), CInt(board_id), params), + "Error in insert_marker" + ) + } + } + } + + public func is_prepared() throws -> Bool { + try serialized_params.withCString { params in + var prepared: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.is_prepared(&prepared, CInt(board_id), params), "Error in is_prepared") + } + return prepared != 0 + } + } + + public static func release_all_sessions() throws { + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.release_all_sessions(), "Error in release_all_sessions") + } + } + + public static func set_log_level(_ log_level: Int) throws { + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.set_log_level_board_controller(CInt(log_level)), "Error in set_log_level") + } + } + + public static func set_log_level(_ log_level: LogLevels) throws { + try set_log_level(log_level.rawValue) + } + + public static func enable_board_logger() throws { + try set_log_level(LogLevels.LEVEL_INFO) + } + + public static func enable_dev_board_logger() throws { + try set_log_level(LogLevels.LEVEL_TRACE) + } + + public static func disable_board_logger() throws { + try set_log_level(LogLevels.LEVEL_OFF) + } + + public static func set_log_file(_ log_file: String) throws { + try log_file.withCString { path in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.set_log_file_board_controller(path), "Error in set_log_file") + } + } + } + + public static func log_message(_ log_level: Int, message: String) throws { + var mutableMessage = Array(message.utf8CString) + try mutableMessage.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.log_message_board_controller(CInt(log_level), pointer.baseAddress), "Error in log_message") + } + } + } + + public static func log_message(_ log_level: LogLevels, message: String) throws { + try log_message(log_level.rawValue, message: message) + } + + public static func get_sampling_rate(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_sampling_rate) + } + + public static func get_sampling_rate(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_sampling_rate(board_id: board_id.rawValue, preset: preset) + } + + public static func get_package_num_channel(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_package_num_channel) + } + + public static func get_package_num_channel(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_package_num_channel(board_id: board_id.rawValue, preset: preset) + } + + public static func get_battery_channel(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_battery_channel) + } + + public static func get_battery_channel(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_battery_channel(board_id: board_id.rawValue, preset: preset) + } + + public static func get_num_rows(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_num_rows) + } + + public static func get_num_rows(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_num_rows(board_id: board_id.rawValue, preset: preset) + } + + public static func get_timestamp_channel(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_timestamp_channel) + } + + public static func get_timestamp_channel(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_timestamp_channel(board_id: board_id.rawValue, preset: preset) + } + + public static func get_marker_channel(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_marker_channel) + } + + public static func get_marker_channel(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_marker_channel(board_id: board_id.rawValue, preset: preset) + } + + public static func get_eeg_names(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [String] { + let names = try getStringBoardInfo(board_id: board_id, preset: preset, maxLength: 4096, function: \.get_eeg_names) + return names.isEmpty ? [] : names.split(separator: ",").map(String.init) + } + + public static func get_eeg_names(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [String] { + try get_eeg_names(board_id: board_id.rawValue, preset: preset) + } + + public static func get_board_presets(board_id: Int) throws -> [BrainFlowPresets] { + var values = [CInt](repeating: 0, count: 512) + var length: CInt = 0 + try values.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.get_board_presets(CInt(board_id), pointer.baseAddress, &length), "Error in get_board_presets") + } + } + return values.prefix(Int(length)).compactMap { BrainFlowPresets(rawValue: Int($0)) } + } + + public static func get_board_presets(board_id: BoardIds) throws -> [BrainFlowPresets] { + try get_board_presets(board_id: board_id.rawValue) + } + + public static func get_version() throws -> String { + try getVersion(function: \.get_version_board_controller) + } + + public static func get_board_descr(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> String { + try getStringBoardInfo(board_id: board_id, preset: preset, maxLength: 16_000, function: \.get_board_descr) + } + + public static func get_board_descr(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> String { + try get_board_descr(board_id: board_id.rawValue, preset: preset) + } + + public static func get_device_name(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> String { + try getStringBoardInfo(board_id: board_id, preset: preset, maxLength: 4096, function: \.get_device_name) + } + + public static func get_device_name(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> String { + try get_device_name(board_id: board_id.rawValue, preset: preset) + } + + public static func get_eeg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_eeg_channels) + } + + public static func get_eeg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_eeg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_exg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_exg_channels) + } + + public static func get_exg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_exg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_emg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_emg_channels) + } + + public static func get_emg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_emg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_ecg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_ecg_channels) + } + + public static func get_ecg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_ecg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_eog_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_eog_channels) + } + + public static func get_eog_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_eog_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_ppg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_ppg_channels) + } + + public static func get_ppg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_ppg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_optical_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_optical_channels) + } + + public static func get_optical_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_optical_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_eda_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_eda_channels) + } + + public static func get_eda_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_eda_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_accel_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_accel_channels) + } + + public static func get_accel_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_accel_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_rotation_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_rotation_channels) + } + + public static func get_rotation_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_rotation_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_analog_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_analog_channels) + } + + public static func get_analog_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_analog_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_gyro_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_gyro_channels) + } + + public static func get_gyro_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_gyro_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_other_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_other_channels) + } + + public static func get_other_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_other_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_temperature_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_temperature_channels) + } + + public static func get_temperature_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_temperature_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_resistance_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_resistance_channels) + } + + public static func get_resistance_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_resistance_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_magnetometer_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_magnetometer_channels) + } + + public static func get_magnetometer_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_magnetometer_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func reshape_data_to_2d(num_rows: Int, num_cols: Int, linear_buffer: [Double]) -> [[Double]] { + BrainFlowArray.reshape_data_to_2d(num_rows: num_rows, num_cols: num_cols, linear_buffer: linear_buffer) + } + + private static func getIntBoardInfo( + board_id: Int, + preset: BrainFlowPresets, + function: KeyPath + ) throws -> Int { + var result: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native[keyPath: function](CInt(board_id), CInt(preset.rawValue), &result), "Error in board info getter") + } + return Int(result) + } + + private static func getChannels( + board_id: Int, + preset: BrainFlowPresets, + function: KeyPath + ) throws -> [Int] { + var channels = [CInt](repeating: 0, count: 512) + var length: CInt = 0 + try channels.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native[keyPath: function](CInt(board_id), CInt(preset.rawValue), pointer.baseAddress, &length), "Error in board channel getter") + } + } + return channels.prefix(Int(length)).map(Int.init) + } + + private static func getStringBoardInfo( + board_id: Int, + preset: BrainFlowPresets, + maxLength: Int, + function: KeyPath + ) throws -> String { + var bytes = [CChar](repeating: 0, count: maxLength) + var length: CInt = 0 + try bytes.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native[keyPath: function](CInt(board_id), CInt(preset.rawValue), pointer.baseAddress, &length, CInt(maxLength)), "Error in board string getter") + } + } + return String(bytes: bytes.prefix(Int(length)).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } + + private static func getVersion(function: KeyPath) throws -> String { + let maxLength = 64 + var bytes = [CChar](repeating: 0, count: maxLength) + var length: CInt = 0 + try bytes.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native[keyPath: function](pointer.baseAddress, &length, CInt(maxLength)), "Error in get_version") + } + } + return String(bytes: bytes.prefix(Int(length)).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } +} + +final class BoardShimNative { + typealias BoardInfoIntFunction = @convention(c) (CInt, CInt, UnsafeMutablePointer?) -> CInt + typealias BoardInfoChannelsFunction = @convention(c) (CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + typealias BoardInfoStringFunction = @convention(c) (CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, CInt) -> CInt + typealias VersionFunction = @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt) -> CInt + + let prepare_session: @convention(c) (CInt, UnsafePointer?) -> CInt + let start_stream: @convention(c) (CInt, UnsafePointer?, CInt, UnsafePointer?) -> CInt + let stop_stream: @convention(c) (CInt, UnsafePointer?) -> CInt + let release_session: @convention(c) (CInt, UnsafePointer?) -> CInt + let get_current_board_data: @convention(c) (CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let get_board_data_count: @convention(c) (CInt, UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let get_board_data: @convention(c) (CInt, CInt, UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let get_board_sampling_rate: @convention(c) (CInt, UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let config_board: @convention(c) (UnsafePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, CInt, UnsafePointer?) -> CInt + let config_board_with_bytes: @convention(c) (UnsafePointer?, CInt, CInt, UnsafePointer?) -> CInt + let is_prepared: @convention(c) (UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let insert_marker: @convention(c) (Double, CInt, CInt, UnsafePointer?) -> CInt + let add_streamer: @convention(c) (UnsafePointer?, CInt, CInt, UnsafePointer?) -> CInt + let delete_streamer: @convention(c) (UnsafePointer?, CInt, CInt, UnsafePointer?) -> CInt + let release_all_sessions: @convention(c) () -> CInt + let set_log_level_board_controller: @convention(c) (CInt) -> CInt + let set_log_file_board_controller: @convention(c) (UnsafePointer?) -> CInt + let log_message_board_controller: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt + let get_version_board_controller: VersionFunction + + let get_board_descr: BoardInfoStringFunction + let get_sampling_rate: BoardInfoIntFunction + let get_package_num_channel: BoardInfoIntFunction + let get_timestamp_channel: BoardInfoIntFunction + let get_marker_channel: BoardInfoIntFunction + let get_battery_channel: BoardInfoIntFunction + let get_num_rows: BoardInfoIntFunction + let get_eeg_names: BoardInfoStringFunction + let get_exg_channels: BoardInfoChannelsFunction + let get_eeg_channels: BoardInfoChannelsFunction + let get_emg_channels: BoardInfoChannelsFunction + let get_ecg_channels: BoardInfoChannelsFunction + let get_eog_channels: BoardInfoChannelsFunction + let get_ppg_channels: BoardInfoChannelsFunction + let get_optical_channels: BoardInfoChannelsFunction + let get_eda_channels: BoardInfoChannelsFunction + let get_accel_channels: BoardInfoChannelsFunction + let get_rotation_channels: BoardInfoChannelsFunction + let get_analog_channels: BoardInfoChannelsFunction + let get_gyro_channels: BoardInfoChannelsFunction + let get_other_channels: BoardInfoChannelsFunction + let get_temperature_channels: BoardInfoChannelsFunction + let get_resistance_channels: BoardInfoChannelsFunction + let get_magnetometer_channels: BoardInfoChannelsFunction + let get_device_name: BoardInfoStringFunction + let get_board_presets: @convention(c) (CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + + private static let lock = NSLock() + private static var cached: BoardShimNative? + + static func withBoard(_ body: (BoardShimNative) throws -> T) throws -> T { + try body(load()) + } + + private static func load() throws -> BoardShimNative { + lock.lock() + defer { lock.unlock() } + if let cached { return cached } + let value = try BoardShimNative(library: NativeLibraries.boardController.load()) + cached = value + return value + } + + private init(library: NativeLibrary) throws { + prepare_session = try library.symbol("prepare_session", as: type(of: prepare_session)) + start_stream = try library.symbol("start_stream", as: type(of: start_stream)) + stop_stream = try library.symbol("stop_stream", as: type(of: stop_stream)) + release_session = try library.symbol("release_session", as: type(of: release_session)) + get_current_board_data = try library.symbol("get_current_board_data", as: type(of: get_current_board_data)) + get_board_data_count = try library.symbol("get_board_data_count", as: type(of: get_board_data_count)) + get_board_data = try library.symbol("get_board_data", as: type(of: get_board_data)) + get_board_sampling_rate = try library.symbol("get_board_sampling_rate", as: type(of: get_board_sampling_rate)) + config_board = try library.symbol("config_board", as: type(of: config_board)) + config_board_with_bytes = try library.symbol("config_board_with_bytes", as: type(of: config_board_with_bytes)) + is_prepared = try library.symbol("is_prepared", as: type(of: is_prepared)) + insert_marker = try library.symbol("insert_marker", as: type(of: insert_marker)) + add_streamer = try library.symbol("add_streamer", as: type(of: add_streamer)) + delete_streamer = try library.symbol("delete_streamer", as: type(of: delete_streamer)) + release_all_sessions = try library.symbol("release_all_sessions", as: type(of: release_all_sessions)) + set_log_level_board_controller = try library.symbol("set_log_level_board_controller", as: type(of: set_log_level_board_controller)) + set_log_file_board_controller = try library.symbol("set_log_file_board_controller", as: type(of: set_log_file_board_controller)) + log_message_board_controller = try library.symbol("log_message_board_controller", as: type(of: log_message_board_controller)) + get_version_board_controller = try library.symbol("get_version_board_controller", as: type(of: get_version_board_controller)) + get_board_descr = try library.symbol("get_board_descr", as: type(of: get_board_descr)) + get_sampling_rate = try library.symbol("get_sampling_rate", as: type(of: get_sampling_rate)) + get_package_num_channel = try library.symbol("get_package_num_channel", as: type(of: get_package_num_channel)) + get_timestamp_channel = try library.symbol("get_timestamp_channel", as: type(of: get_timestamp_channel)) + get_marker_channel = try library.symbol("get_marker_channel", as: type(of: get_marker_channel)) + get_battery_channel = try library.symbol("get_battery_channel", as: type(of: get_battery_channel)) + get_num_rows = try library.symbol("get_num_rows", as: type(of: get_num_rows)) + get_eeg_names = try library.symbol("get_eeg_names", as: type(of: get_eeg_names)) + get_exg_channels = try library.symbol("get_exg_channels", as: type(of: get_exg_channels)) + get_eeg_channels = try library.symbol("get_eeg_channels", as: type(of: get_eeg_channels)) + get_emg_channels = try library.symbol("get_emg_channels", as: type(of: get_emg_channels)) + get_ecg_channels = try library.symbol("get_ecg_channels", as: type(of: get_ecg_channels)) + get_eog_channels = try library.symbol("get_eog_channels", as: type(of: get_eog_channels)) + get_ppg_channels = try library.symbol("get_ppg_channels", as: type(of: get_ppg_channels)) + get_optical_channels = try library.symbol("get_optical_channels", as: type(of: get_optical_channels)) + get_eda_channels = try library.symbol("get_eda_channels", as: type(of: get_eda_channels)) + get_accel_channels = try library.symbol("get_accel_channels", as: type(of: get_accel_channels)) + get_rotation_channels = try library.symbol("get_rotation_channels", as: type(of: get_rotation_channels)) + get_analog_channels = try library.symbol("get_analog_channels", as: type(of: get_analog_channels)) + get_gyro_channels = try library.symbol("get_gyro_channels", as: type(of: get_gyro_channels)) + get_other_channels = try library.symbol("get_other_channels", as: type(of: get_other_channels)) + get_temperature_channels = try library.symbol("get_temperature_channels", as: type(of: get_temperature_channels)) + get_resistance_channels = try library.symbol("get_resistance_channels", as: type(of: get_resistance_channels)) + get_magnetometer_channels = try library.symbol("get_magnetometer_channels", as: type(of: get_magnetometer_channels)) + get_device_name = try library.symbol("get_device_name", as: type(of: get_device_name)) + get_board_presets = try library.symbol("get_board_presets", as: type(of: get_board_presets)) + } +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift new file mode 100644 index 000000000..79aa5b0f4 --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift @@ -0,0 +1,236 @@ +public enum BoardIds: Int, CaseIterable, Sendable { + case NO_BOARD = -100 + case PLAYBACK_FILE_BOARD = -3 + case STREAMING_BOARD = -2 + case SYNTHETIC_BOARD = -1 + case CYTON_BOARD = 0 + case GANGLION_BOARD = 1 + case CYTON_DAISY_BOARD = 2 + case GALEA_BOARD = 3 + case GANGLION_WIFI_BOARD = 4 + case CYTON_WIFI_BOARD = 5 + case CYTON_DAISY_WIFI_BOARD = 6 + case BRAINBIT_BOARD = 7 + case UNICORN_BOARD = 8 + case CALLIBRI_EEG_BOARD = 9 + case CALLIBRI_EMG_BOARD = 10 + case CALLIBRI_ECG_BOARD = 11 + case NOTION_1_BOARD = 13 + case NOTION_2_BOARD = 14 + case GFORCE_PRO_BOARD = 16 + case FREEEEG32_BOARD = 17 + case BRAINBIT_BLED_BOARD = 18 + case GFORCE_DUAL_BOARD = 19 + case MUSE_S_BLED_BOARD = 21 + case MUSE_2_BLED_BOARD = 22 + case CROWN_BOARD = 23 + case ANT_NEURO_EE_410_BOARD = 24 + case ANT_NEURO_EE_411_BOARD = 25 + case ANT_NEURO_EE_430_BOARD = 26 + case ANT_NEURO_EE_211_BOARD = 27 + case ANT_NEURO_EE_212_BOARD = 28 + case ANT_NEURO_EE_213_BOARD = 29 + case ANT_NEURO_EE_214_BOARD = 30 + case ANT_NEURO_EE_215_BOARD = 31 + case ANT_NEURO_EE_221_BOARD = 32 + case ANT_NEURO_EE_222_BOARD = 33 + case ANT_NEURO_EE_223_BOARD = 34 + case ANT_NEURO_EE_224_BOARD = 35 + case ANT_NEURO_EE_225_BOARD = 36 + case ENOPHONE_BOARD = 37 + case MUSE_2_BOARD = 38 + case MUSE_S_BOARD = 39 + case BRAINALIVE_BOARD = 40 + case MUSE_2016_BOARD = 41 + case MUSE_2016_BLED_BOARD = 42 + case EXPLORE_4_CHAN_BOARD = 44 + case EXPLORE_8_CHAN_BOARD = 45 + case GANGLION_NATIVE_BOARD = 46 + case EMOTIBIT_BOARD = 47 + case NTL_WIFI_BOARD = 50 + case ANT_NEURO_EE_511_BOARD = 51 + case FREEEEG128_BOARD = 52 + case AAVAA_V3_BOARD = 53 + case EXPLORE_PLUS_8_CHAN_BOARD = 54 + case EXPLORE_PLUS_32_CHAN_BOARD = 55 + case PIEEG_BOARD = 56 + case NEUROPAWN_KNIGHT_BOARD = 57 + case SYNCHRONI_TRIO_3_CHANNELS_BOARD = 58 + case SYNCHRONI_OCTO_8_CHANNELS_BOARD = 59 + case OB5000_8_CHANNELS_BOARD = 60 + case SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61 + case SYNCHRONI_UNO_1_CHANNELS_BOARD = 62 + case OB3000_24_CHANNELS_BOARD = 63 + case BIOLISTENER_BOARD = 64 + case IRONBCI_32_BOARD = 65 + case NEUROPAWN_KNIGHT_BOARD_IMU = 66 + case MUSE_S_ATHENA_BOARD = 67 + + public var code: Int { rawValue } +} + +public enum IpProtocolTypes: Int, CaseIterable, Sendable { + case NO_IP_PROTOCOL = 0 + case UDP = 1 + case TCP = 2 + + public var code: Int { rawValue } +} + +public enum FilterTypes: Int, CaseIterable, Sendable { + case BUTTERWORTH = 0 + case CHEBYSHEV_TYPE_1 = 1 + case BESSEL = 2 + case BUTTERWORTH_ZERO_PHASE = 3 + case CHEBYSHEV_TYPE_1_ZERO_PHASE = 4 + case BESSEL_ZERO_PHASE = 5 + + public var code: Int { rawValue } +} + +public enum AggOperations: Int, CaseIterable, Sendable { + case MEAN = 0 + case MEDIAN = 1 + case EACH = 2 + + public var code: Int { rawValue } +} + +public enum WindowOperations: Int, CaseIterable, Sendable { + case NO_WINDOW = 0 + case HANNING = 1 + case HAMMING = 2 + case BLACKMAN_HARRIS = 3 + + public var code: Int { rawValue } +} + +public enum DetrendOperations: Int, CaseIterable, Sendable { + case NO_DETREND = 0 + case CONSTANT = 1 + case LINEAR = 2 + + public var code: Int { rawValue } +} + +public enum BrainFlowMetrics: Int, CaseIterable, Sendable { + case MINDFULNESS = 0 + case RESTFULNESS = 1 + case USER_DEFINED = 2 + + public var code: Int { rawValue } +} + +public enum BrainFlowClassifiers: Int, CaseIterable, Sendable { + case DEFAULT_CLASSIFIER = 0 + case DYN_LIB_CLASSIFIER = 1 + case ONNX_CLASSIFIER = 2 + + public var code: Int { rawValue } +} + +public enum BrainFlowPresets: Int, CaseIterable, Sendable { + case DEFAULT_PRESET = 0 + case AUXILIARY_PRESET = 1 + case ANCILLARY_PRESET = 2 + + public var code: Int { rawValue } +} + +public enum LogLevels: Int, CaseIterable, Sendable { + case LEVEL_TRACE = 0 + case LEVEL_DEBUG = 1 + case LEVEL_INFO = 2 + case LEVEL_WARN = 3 + case LEVEL_ERROR = 4 + case LEVEL_CRITICAL = 5 + case LEVEL_OFF = 6 + + public var code: Int { rawValue } +} + +public enum NoiseTypes: Int, CaseIterable, Sendable { + case FIFTY = 0 + case SIXTY = 1 + case FIFTY_AND_SIXTY = 2 + + public var code: Int { rawValue } +} + +public enum WaveletDenoisingTypes: Int, CaseIterable, Sendable { + case VISUSHRINK = 0 + case SURESHRINK = 1 + + public var code: Int { rawValue } +} + +public enum ThresholdTypes: Int, CaseIterable, Sendable { + case SOFT = 0 + case HARD = 1 + + public var code: Int { rawValue } +} + +public enum WaveletExtensionTypes: Int, CaseIterable, Sendable { + case SYMMETRIC = 0 + case PERIODIC = 1 + + public var code: Int { rawValue } +} + +public enum NoiseEstimationLevelTypes: Int, CaseIterable, Sendable { + case FIRST_LEVEL = 0 + case ALL_LEVELS = 1 + + public var code: Int { rawValue } +} + +public enum WaveletTypes: Int, CaseIterable, Sendable { + case HAAR = 0 + case DB1 = 1 + case DB2 = 2 + case DB3 = 3 + case DB4 = 4 + case DB5 = 5 + case DB6 = 6 + case DB7 = 7 + case DB8 = 8 + case DB9 = 9 + case DB10 = 10 + case DB11 = 11 + case DB12 = 12 + case DB13 = 13 + case DB14 = 14 + case DB15 = 15 + case BIOR1_1 = 16 + case BIOR1_3 = 17 + case BIOR1_5 = 18 + case BIOR2_2 = 19 + case BIOR2_4 = 20 + case BIOR2_6 = 21 + case BIOR2_8 = 22 + case BIOR3_1 = 23 + case BIOR3_3 = 24 + case BIOR3_5 = 25 + case BIOR3_7 = 26 + case BIOR3_9 = 27 + case BIOR4_4 = 28 + case BIOR5_5 = 29 + case BIOR6_8 = 30 + case COIF1 = 31 + case COIF2 = 32 + case COIF3 = 33 + case COIF4 = 34 + case COIF5 = 35 + case SYM2 = 36 + case SYM3 = 37 + case SYM4 = 38 + case SYM5 = 39 + case SYM6 = 40 + case SYM7 = 41 + case SYM8 = 42 + case SYM9 = 43 + case SYM10 = 44 + + public var code: Int { rawValue } +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowError.swift b/swift_package/Sources/BrainFlow/BrainFlowError.swift new file mode 100644 index 000000000..867eff305 --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowError.swift @@ -0,0 +1,59 @@ +import Foundation + +public enum BrainFlowExitCodes: Int, CaseIterable, Sendable { + case STATUS_OK = 0 + case PORT_ALREADY_OPEN_ERROR = 1 + case UNABLE_TO_OPEN_PORT_ERROR = 2 + case SET_PORT_ERROR = 3 + case BOARD_WRITE_ERROR = 4 + case INCOMMING_MSG_ERROR = 5 + case INITIAL_MSG_ERROR = 6 + case BOARD_NOT_READY_ERROR = 7 + case STREAM_ALREADY_RUN_ERROR = 8 + case INVALID_BUFFER_SIZE_ERROR = 9 + case STREAM_THREAD_ERROR = 10 + case STREAM_THREAD_IS_NOT_RUNNING = 11 + case EMPTY_BUFFER_ERROR = 12 + case INVALID_ARGUMENTS_ERROR = 13 + case UNSUPPORTED_BOARD_ERROR = 14 + case BOARD_NOT_CREATED_ERROR = 15 + case ANOTHER_BOARD_IS_CREATED_ERROR = 16 + case GENERAL_ERROR = 17 + case SYNC_TIMEOUT_ERROR = 18 + case JSON_NOT_FOUND_ERROR = 19 + case NO_SUCH_DATA_IN_JSON_ERROR = 20 + case CLASSIFIER_IS_NOT_PREPARED_ERROR = 21 + case ANOTHER_CLASSIFIER_IS_PREPARED_ERROR = 22 + case UNSUPPORTED_CLASSIFIER_AND_METRIC_COMBINATION_ERROR = 23 + + public var code: Int { rawValue } +} + +public struct BrainFlowError: Error, LocalizedError, CustomStringConvertible, Sendable { + public let message: String + public let exit_code: Int + + public init(_ message: String, _ exit_code: Int) { + self.message = message + self.exit_code = exit_code + } + + public var errorDescription: String? { + "\(message), exit code: \(exit_code)" + } + + public var description: String { + errorDescription ?? message + } +} + +@inline(__always) +func checkBrainFlowExitCode(_ exitCode: CInt, _ message: String) throws { + guard exitCode == CInt(BrainFlowExitCodes.STATUS_OK.rawValue) else { + throw BrainFlowError(message, Int(exitCode)) + } +} + +func invalidArguments(_ message: String) -> BrainFlowError { + BrainFlowError(message, BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.rawValue) +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowNative.swift b/swift_package/Sources/BrainFlow/BrainFlowNative.swift new file mode 100644 index 000000000..f791d8217 --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowNative.swift @@ -0,0 +1,182 @@ +import Foundation + +#if os(Linux) +import Glibc +#else +import Darwin +#endif + +final class NativeLibrary { + private let handle: UnsafeMutableRawPointer + + init(names: [String]) throws { + var errors = [String]() + for path in Self.candidatePaths(for: names) { + if let handle = dlopen(path, Self.openFlags) { + self.handle = handle + return + } + if let error = dlerror().map({ String(cString: $0) }) { + errors.append("\(path): \(error)") + } + } + throw BrainFlowError( + "Unable to load BrainFlow native library. Set BRAINFLOW_LIB_DIR or build native libs into installed/lib. Tried: \(errors.joined(separator: "; "))", + BrainFlowExitCodes.GENERAL_ERROR.rawValue + ) + } + + deinit { + dlclose(handle) + } + + func symbol(_ name: String, as type: T.Type) throws -> T { + guard let pointer = dlsym(handle, name) else { + let message = dlerror().map { String(cString: $0) } ?? "symbol not found" + throw BrainFlowError("Unable to load symbol \(name): \(message)", BrainFlowExitCodes.GENERAL_ERROR.rawValue) + } + return unsafeBitCast(pointer, to: type) + } + + private static var openFlags: Int32 { + return RTLD_NOW | RTLD_GLOBAL + } + + private static func candidatePaths(for names: [String]) -> [String] { + var dirs = [String]() + let env = ProcessInfo.processInfo.environment + + if let explicit = env["BRAINFLOW_LIB_DIR"], !explicit.isEmpty { + dirs.append(explicit) + } + dirs.append(contentsOf: splitPathList(env["DYLD_LIBRARY_PATH"])) + dirs.append(contentsOf: splitPathList(env["LD_LIBRARY_PATH"])) + + let cwd = FileManager.default.currentDirectoryPath + dirs.append(cwd) + dirs.append("\(cwd)/installed/lib") + dirs.append("\(cwd)/../installed/lib") + dirs.append("\(cwd)/../../installed/lib") + dirs.append("\(cwd)/lib") + + #if os(macOS) || os(iOS) + if let privateFrameworksPath = Bundle.main.privateFrameworksPath { + dirs.append(privateFrameworksPath) + } + if let resourcePath = Bundle.main.resourcePath { + dirs.append(resourcePath) + dirs.append("\(resourcePath)/lib") + dirs.append("\(resourcePath)/Frameworks") + } + #endif + + var candidates = [String]() + for dir in unique(dirs) { + for name in names { + candidates.append((dir as NSString).appendingPathComponent(name)) + #if os(macOS) || os(iOS) + for frameworkPath in appleFrameworkPaths(for: name, in: dir) { + candidates.append(frameworkPath) + } + #endif + } + } + candidates.append(contentsOf: names) + #if os(macOS) || os(iOS) + for name in names { + candidates.append(contentsOf: appleFrameworkLoaderNames(for: name)) + } + #endif + return unique(candidates) + } + + #if os(macOS) || os(iOS) + private static func appleFrameworkPaths(for libraryName: String, in directory: String) -> [String] { + let executable = appleFrameworkExecutableName(from: libraryName) + return [ + "\(directory)/\(executable).framework/\(executable)", + "\(directory)/\(libraryName).framework/\(executable)" + ] + } + + private static func appleFrameworkLoaderNames(for libraryName: String) -> [String] { + let executable = appleFrameworkExecutableName(from: libraryName) + return [ + "@rpath/\(executable).framework/\(executable)", + "\(executable).framework/\(executable)" + ] + } + + private static func appleFrameworkExecutableName(from libraryName: String) -> String { + var name = (libraryName as NSString).lastPathComponent + if name.hasPrefix("lib") { + name.removeFirst(3) + } + if name.hasSuffix(".dylib") { + name.removeLast(".dylib".count) + } + return name + } + #endif + + private static func splitPathList(_ value: String?) -> [String] { + guard let value, !value.isEmpty else { return [] } + return value.split(separator: ":").map(String.init) + } + + private static func unique(_ values: [String]) -> [String] { + var seen = Set() + var result = [String]() + for value in values where !value.isEmpty && !seen.contains(value) { + seen.insert(value) + result.append(value) + } + return result + } +} + +enum NativeLibraries { + static let boardController = LazyNativeLibrary(names: [ + platformLibraryName(base: "BoardController"), + "BoardController" + ]) + static let dataHandler = LazyNativeLibrary(names: [ + platformLibraryName(base: "DataHandler"), + "DataHandler" + ]) + static let mlModule = LazyNativeLibrary(names: [ + platformLibraryName(base: "MLModule"), + "MLModule" + ]) + + private static func platformLibraryName(base: String) -> String { + #if os(Windows) + return "\(base).dll" + #elseif os(macOS) || os(iOS) + return "lib\(base).dylib" + #else + return "lib\(base).so" + #endif + } +} + +final class LazyNativeLibrary { + private let names: [String] + private let lock = NSLock() + private var storage: NativeLibrary? + + init(names: [String]) { + self.names = names + } + + func load() throws -> NativeLibrary { + lock.lock() + defer { lock.unlock() } + if let storage { + return storage + } + let library = try NativeLibrary(names: names) + storage = library + return library + } +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowParams.swift b/swift_package/Sources/BrainFlow/BrainFlowParams.swift new file mode 100644 index 000000000..e33082859 --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowParams.swift @@ -0,0 +1,107 @@ +import Foundation + +public struct BrainFlowInputParams: Codable, Equatable, Sendable { + public var serial_port: String + public var mac_address: String + public var ip_address: String + public var ip_address_aux: String + public var ip_address_anc: String + public var ip_port: Int + public var ip_port_aux: Int + public var ip_port_anc: Int + public var ip_protocol: Int + public var other_info: String + public var timeout: Int + public var serial_number: String + public var file: String + public var file_aux: String + public var file_anc: String + public var master_board: Int + + public init() { + serial_port = "" + mac_address = "" + ip_address = "" + ip_address_aux = "" + ip_address_anc = "" + ip_port = 0 + ip_port_aux = 0 + ip_port_anc = 0 + ip_protocol = IpProtocolTypes.NO_IP_PROTOCOL.rawValue + other_info = "" + timeout = 0 + serial_number = "" + file = "" + file_aux = "" + file_anc = "" + master_board = BoardIds.NO_BOARD.rawValue + } + + public mutating func set_ip_protocol(_ ip_protocol: IpProtocolTypes) { + self.ip_protocol = ip_protocol.rawValue + } + + public mutating func set_master_board(_ board: BoardIds) { + master_board = board.rawValue + } + + public func to_json() throws -> String { + try Self.encoder.encodeString(self) + } + + private static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + return encoder + }() +} + +public struct BrainFlowModelParams: Codable, Equatable, Sendable { + public var metric: Int + public var classifier: Int + public var file: String + public var other_info: String + public var output_name: String + public var max_array_size: Int + + public init(metric: Int, classifier: Int) { + self.metric = metric + self.classifier = classifier + file = "" + other_info = "" + output_name = "" + max_array_size = 8192 + } + + public init(metric: BrainFlowMetrics, classifier: BrainFlowClassifiers) { + self.init(metric: metric.rawValue, classifier: classifier.rawValue) + } + + public mutating func set_metric(_ metric: BrainFlowMetrics) { + self.metric = metric.rawValue + } + + public mutating func set_classifier(_ classifier: BrainFlowClassifiers) { + self.classifier = classifier.rawValue + } + + public func to_json() throws -> String { + try Self.encoder.encodeString(self) + } + + private static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + return encoder + }() +} + +private extension JSONEncoder { + func encodeString(_ value: T) throws -> String { + let data = try encode(value) + guard let string = String(data: data, encoding: .utf8) else { + throw invalidArguments("Unable to encode JSON as UTF-8") + } + return string + } +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowTypes.swift b/swift_package/Sources/BrainFlow/BrainFlowTypes.swift new file mode 100644 index 000000000..0ac58943c --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowTypes.swift @@ -0,0 +1,106 @@ +import Foundation + +public struct Complex: Equatable, Sendable { + public var real: Double + public var imag: Double + + public init(real: Double, imag: Double) { + self.real = real + self.imag = imag + } +} + +public struct WaveletTransform: Equatable, Sendable { + public var coefficients: [Double] + public var decomposition_lengths: [Int] + + public init(coefficients: [Double], decomposition_lengths: [Int]) { + self.coefficients = coefficients + self.decomposition_lengths = decomposition_lengths + } +} + +public struct PSD: Equatable, Sendable { + public var ampl: [Double] + public var freq: [Double] + + public init(ampl: [Double], freq: [Double]) { + self.ampl = ampl + self.freq = freq + } +} + +public struct BandPowerResult: Equatable, Sendable { + public var average: [Double] + public var stddev: [Double] + + public init(average: [Double], stddev: [Double]) { + self.average = average + self.stddev = stddev + } +} + +public struct CSPResult: Equatable, Sendable { + public var filters: [[Double]] + public var eigenvalues: [Double] + + public init(filters: [[Double]], eigenvalues: [Double]) { + self.filters = filters + self.eigenvalues = eigenvalues + } +} + +public struct ICAResult: Equatable, Sendable { + public var w: [[Double]] + public var k: [[Double]] + public var a: [[Double]] + public var s: [[Double]] + + public init(w: [[Double]], k: [[Double]], a: [[Double]], s: [[Double]]) { + self.w = w + self.k = k + self.a = a + self.s = s + } +} + +public struct FrequencyBand: Equatable, Sendable { + public var start: Double + public var stop: Double + + public init(start: Double, stop: Double) { + self.start = start + self.stop = stop + } +} + +enum BrainFlowArray { + static func reshape_data_to_1d(num_rows: Int, num_cols: Int, buf: [[Double]]) -> [Double] { + var output = [Double](repeating: 0.0, count: num_rows * num_cols) + for col in 0.. [[Double]] { + guard num_rows > 0, num_cols > 0 else { return [] } + return (0.. (rows: Int, cols: Int) { + guard let first = data.first else { + throw invalidArguments("Data array is empty") + } + let cols = first.count + guard cols > 0, data.allSatisfy({ $0.count == cols }) else { + throw invalidArguments("Data array must be rectangular") + } + return (data.count, cols) + } +} diff --git a/swift_package/Sources/BrainFlow/DataFilter.swift b/swift_package/Sources/BrainFlow/DataFilter.swift new file mode 100644 index 000000000..ec21ef5a9 --- /dev/null +++ b/swift_package/Sources/BrainFlow/DataFilter.swift @@ -0,0 +1,816 @@ +import Foundation + +public enum DataFilter { + public static func set_log_level(_ log_level: Int) throws { + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.set_log_level_data_handler(CInt(log_level)), "Error in set_log_level") + } + } + + public static func set_log_level(_ log_level: LogLevels) throws { + try set_log_level(log_level.rawValue) + } + + public static func enable_data_logger() throws { + try set_log_level(LogLevels.LEVEL_INFO) + } + + public static func enable_dev_data_logger() throws { + try set_log_level(LogLevels.LEVEL_TRACE) + } + + public static func disable_data_logger() throws { + try set_log_level(LogLevels.LEVEL_OFF) + } + + public static func set_log_file(_ log_file: String) throws { + try log_file.withCString { path in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.set_log_file_data_handler(path), "Error in set_log_file") + } + } + } + + public static func log_message(_ log_level: Int, message: String) throws { + var mutableMessage = Array(message.utf8CString) + try mutableMessage.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.log_message_data_handler(CInt(log_level), pointer.baseAddress), "Error in log_message") + } + } + } + + public static func get_version() throws -> String { + try getVersion(function: \.get_version_data_handler) + } + + public static func perform_lowpass( + data: inout [Double], + sampling_rate: Int, + cutoff: Double, + order: Int, + filter_type: Int, + ripple: Double + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_lowpass(pointer, CInt(count), CInt(sampling_rate), cutoff, CInt(order), CInt(filter_type), ripple), "Failed to perform lowpass") + } + } + } + + public static func perform_lowpass( + data: inout [Double], + sampling_rate: Int, + cutoff: Double, + order: Int, + filter_type: FilterTypes, + ripple: Double + ) throws { + try perform_lowpass(data: &data, sampling_rate: sampling_rate, cutoff: cutoff, order: order, filter_type: filter_type.rawValue, ripple: ripple) + } + + public static func perform_highpass( + data: inout [Double], + sampling_rate: Int, + cutoff: Double, + order: Int, + filter_type: Int, + ripple: Double + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_highpass(pointer, CInt(count), CInt(sampling_rate), cutoff, CInt(order), CInt(filter_type), ripple), "Failed to perform highpass") + } + } + } + + public static func perform_highpass( + data: inout [Double], + sampling_rate: Int, + cutoff: Double, + order: Int, + filter_type: FilterTypes, + ripple: Double + ) throws { + try perform_highpass(data: &data, sampling_rate: sampling_rate, cutoff: cutoff, order: order, filter_type: filter_type.rawValue, ripple: ripple) + } + + public static func perform_bandpass( + data: inout [Double], + sampling_rate: Int, + start_freq: Double, + stop_freq: Double, + order: Int, + filter_type: Int, + ripple: Double + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_bandpass(pointer, CInt(count), CInt(sampling_rate), start_freq, stop_freq, CInt(order), CInt(filter_type), ripple), "Failed to perform bandpass") + } + } + } + + public static func perform_bandpass( + data: inout [Double], + sampling_rate: Int, + start_freq: Double, + stop_freq: Double, + order: Int, + filter_type: FilterTypes, + ripple: Double + ) throws { + try perform_bandpass(data: &data, sampling_rate: sampling_rate, start_freq: start_freq, stop_freq: stop_freq, order: order, filter_type: filter_type.rawValue, ripple: ripple) + } + + public static func perform_bandstop( + data: inout [Double], + sampling_rate: Int, + start_freq: Double, + stop_freq: Double, + order: Int, + filter_type: Int, + ripple: Double + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_bandstop(pointer, CInt(count), CInt(sampling_rate), start_freq, stop_freq, CInt(order), CInt(filter_type), ripple), "Failed to perform bandstop") + } + } + } + + public static func perform_bandstop( + data: inout [Double], + sampling_rate: Int, + start_freq: Double, + stop_freq: Double, + order: Int, + filter_type: FilterTypes, + ripple: Double + ) throws { + try perform_bandstop(data: &data, sampling_rate: sampling_rate, start_freq: start_freq, stop_freq: stop_freq, order: order, filter_type: filter_type.rawValue, ripple: ripple) + } + + public static func remove_environmental_noise(data: inout [Double], sampling_rate: Int, noise_type: Int) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.remove_environmental_noise(pointer, CInt(count), CInt(sampling_rate), CInt(noise_type)), "Failed to remove environmental noise") + } + } + } + + public static func remove_environmental_noise(data: inout [Double], sampling_rate: Int, noise_type: NoiseTypes) throws { + try remove_environmental_noise(data: &data, sampling_rate: sampling_rate, noise_type: noise_type.rawValue) + } + + public static func perform_rolling_filter(data: inout [Double], period: Int, operation: Int) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_rolling_filter(pointer, CInt(count), CInt(period), CInt(operation)), "Failed to perform rolling filter") + } + } + } + + public static func perform_rolling_filter(data: inout [Double], period: Int, operation: AggOperations) throws { + try perform_rolling_filter(data: &data, period: period, operation: operation.rawValue) + } + + public static func detrend(data: inout [Double], detrend_operation: Int) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.detrend(pointer, CInt(count), CInt(detrend_operation)), "Failed to detrend data") + } + } + } + + public static func detrend(data: inout [Double], detrend_operation: DetrendOperations) throws { + try detrend(data: &data, detrend_operation: detrend_operation.rawValue) + } + + public static func perform_downsampling(data: [Double], period: Int, operation: Int) throws -> [Double] { + guard period > 0, data.count / period > 0 else { throw invalidArguments("Invalid period or data size") } + var input = data + var output = [Double](repeating: 0.0, count: data.count / period) + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_downsampling(inputPtr.baseAddress, CInt(data.count), CInt(period), CInt(operation), outputPtr.baseAddress), "Failed to perform downsampling") + } + } + } + return output + } + + public static func perform_downsampling(data: [Double], period: Int, operation: AggOperations) throws -> [Double] { + try perform_downsampling(data: data, period: period, operation: operation.rawValue) + } + + public static func perform_wavelet_transform( + data: [Double], + wavelet: Int, + decomposition_level: Int, + extension_type: Int + ) throws -> WaveletTransform { + guard decomposition_level > 0 else { throw invalidArguments("Invalid decomposition level") } + var input = data + var output = [Double](repeating: 0.0, count: data.count + 2 * decomposition_level * 41) + var lengths = [CInt](repeating: 0, count: decomposition_level + 1) + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try lengths.withUnsafeMutableBufferPointer { lengthsPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_wavelet_transform(inputPtr.baseAddress, CInt(data.count), CInt(wavelet), CInt(decomposition_level), CInt(extension_type), outputPtr.baseAddress, lengthsPtr.baseAddress), "Failed to perform wavelet transform") + } + } + } + } + let swiftLengths = lengths.map(Int.init) + return WaveletTransform(coefficients: Array(output.prefix(swiftLengths.reduce(0, +))), decomposition_lengths: swiftLengths) + } + + public static func perform_wavelet_transform( + data: [Double], + wavelet: WaveletTypes, + decomposition_level: Int, + extension_type: WaveletExtensionTypes + ) throws -> WaveletTransform { + try perform_wavelet_transform(data: data, wavelet: wavelet.rawValue, decomposition_level: decomposition_level, extension_type: extension_type.rawValue) + } + + public static func perform_inverse_wavelet_transform( + wavelet_output: WaveletTransform, + original_data_len: Int, + wavelet: Int, + decomposition_level: Int, + extension_type: Int + ) throws -> [Double] { + var coeffs = wavelet_output.coefficients + var lengths = wavelet_output.decomposition_lengths.map(CInt.init) + var output = [Double](repeating: 0.0, count: original_data_len) + try coeffs.withUnsafeMutableBufferPointer { coeffsPtr in + try lengths.withUnsafeMutableBufferPointer { lengthsPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_inverse_wavelet_transform(coeffsPtr.baseAddress, CInt(original_data_len), CInt(wavelet), CInt(decomposition_level), CInt(extension_type), lengthsPtr.baseAddress, outputPtr.baseAddress), "Failed to perform inverse wavelet transform") + } + } + } + } + return output + } + + public static func perform_inverse_wavelet_transform( + wavelet_output: WaveletTransform, + original_data_len: Int, + wavelet: WaveletTypes, + decomposition_level: Int, + extension_type: WaveletExtensionTypes + ) throws -> [Double] { + try perform_inverse_wavelet_transform(wavelet_output: wavelet_output, original_data_len: original_data_len, wavelet: wavelet.rawValue, decomposition_level: decomposition_level, extension_type: extension_type.rawValue) + } + + public static func perform_wavelet_denoising( + data: inout [Double], + wavelet: Int, + decomposition_level: Int, + wavelet_denoising: Int, + threshold: Int, + extension_type: Int, + noise_level: Int + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_wavelet_denoising(pointer, CInt(count), CInt(wavelet), CInt(decomposition_level), CInt(wavelet_denoising), CInt(threshold), CInt(extension_type), CInt(noise_level)), "Failed to perform wavelet denoising") + } + } + } + + public static func perform_wavelet_denoising( + data: inout [Double], + wavelet: WaveletTypes, + decomposition_level: Int, + wavelet_denoising: WaveletDenoisingTypes, + threshold: ThresholdTypes, + extension_type: WaveletExtensionTypes, + noise_level: NoiseEstimationLevelTypes + ) throws { + try perform_wavelet_denoising(data: &data, wavelet: wavelet.rawValue, decomposition_level: decomposition_level, wavelet_denoising: wavelet_denoising.rawValue, threshold: threshold.rawValue, extension_type: extension_type.rawValue, noise_level: noise_level.rawValue) + } + + public static func restore_data_from_wavelet_detailed_coeffs( + data: [Double], + wavelet: Int, + decomposition_level: Int, + level_to_restore: Int + ) throws -> [Double] { + var input = data + var output = [Double](repeating: 0.0, count: data.count) + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.restore_data_from_wavelet_detailed_coeffs(inputPtr.baseAddress, CInt(data.count), CInt(wavelet), CInt(decomposition_level), CInt(level_to_restore), outputPtr.baseAddress), "Failed to restore wavelet detailed coeffs") + } + } + } + return output + } + + public static func restore_data_from_wavelet_detailed_coeffs( + data: [Double], + wavelet: WaveletTypes, + decomposition_level: Int, + level_to_restore: Int + ) throws -> [Double] { + try restore_data_from_wavelet_detailed_coeffs(data: data, wavelet: wavelet.rawValue, decomposition_level: decomposition_level, level_to_restore: level_to_restore) + } + + public static func detect_peaks_z_score(data: [Double], lag: Int = 5, threshold: Double = 3.5, influence: Double = 0.1) throws -> [Double] { + var input = data + var output = [Double](repeating: 0.0, count: data.count) + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.detect_peaks_z_score(inputPtr.baseAddress, CInt(data.count), CInt(lag), threshold, influence, outputPtr.baseAddress), "Failed to detect peaks") + } + } + } + return output + } + + public static func get_csp(data: [[[Double]]], labels: [Double]) throws -> CSPResult { + guard let firstEpoch = data.first, let firstChannel = firstEpoch.first, !firstChannel.isEmpty else { throw invalidArguments("Invalid CSP data") } + let nEpochs = data.count + let nChannels = firstEpoch.count + let nTimes = firstChannel.count + guard labels.count == nEpochs else { throw invalidArguments("labels count must match epoch count") } + guard data.allSatisfy({ epoch in + epoch.count == nChannels && epoch.allSatisfy { $0.count == nTimes } + }) else { + throw invalidArguments("CSP data must be rectangular") + } + var flattened = [Double]() + flattened.reserveCapacity(nEpochs * nChannels * nTimes) + for epoch in data { + for channel in epoch { + flattened.append(contentsOf: channel) + } + } + var mutableLabels = labels + var filters = [Double](repeating: 0.0, count: nChannels * nChannels) + var eigenvalues = [Double](repeating: 0.0, count: nChannels) + try flattened.withUnsafeMutableBufferPointer { dataPtr in + try mutableLabels.withUnsafeMutableBufferPointer { labelsPtr in + try filters.withUnsafeMutableBufferPointer { filtersPtr in + try eigenvalues.withUnsafeMutableBufferPointer { eigenPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_csp(dataPtr.baseAddress, labelsPtr.baseAddress, CInt(nEpochs), CInt(nChannels), CInt(nTimes), filtersPtr.baseAddress, eigenPtr.baseAddress), "Failed to get CSP") + } + } + } + } + } + return CSPResult(filters: BrainFlowArray.reshape_data_to_2d(num_rows: nChannels, num_cols: nChannels, linear_buffer: filters), eigenvalues: eigenvalues) + } + + public static func get_window(window_function: Int, window_len: Int) throws -> [Double] { + guard window_len > 0 else { throw invalidArguments("window_len must be positive") } + var output = [Double](repeating: 0.0, count: window_len) + try output.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_window(CInt(window_function), CInt(window_len), pointer.baseAddress), "Failed to get window") + } + } + return output + } + + public static func get_window(window_function: WindowOperations, window_len: Int) throws -> [Double] { + try get_window(window_function: window_function.rawValue, window_len: window_len) + } + + public static func perform_fft(data: [Double], start_pos: Int, end_pos: Int, window: Int) throws -> [Complex] { + guard start_pos >= 0, end_pos <= data.count, start_pos < end_pos else { throw invalidArguments("Invalid position arguments") } + var input = Array(data[start_pos.. [Complex] { + try perform_fft(data: data, start_pos: start_pos, end_pos: end_pos, window: window.rawValue) + } + + public static func perform_fft(data: [Double], window: Int) throws -> [Complex] { + try perform_fft(data: data, start_pos: 0, end_pos: data.count, window: window) + } + + public static func perform_fft(data: [Double], window: WindowOperations) throws -> [Complex] { + try perform_fft(data: data, start_pos: 0, end_pos: data.count, window: window.rawValue) + } + + public static func perform_ifft(data: [Complex]) throws -> [Double] { + guard data.count >= 2 else { throw invalidArguments("FFT data must contain at least two bins") } + var real = data.map(\.real) + var imag = data.map(\.imag) + let restoredLength = (data.count - 1) * 2 + var output = [Double](repeating: 0.0, count: restoredLength) + try real.withUnsafeMutableBufferPointer { realPtr in + try imag.withUnsafeMutableBufferPointer { imagPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_ifft(realPtr.baseAddress, imagPtr.baseAddress, CInt(restoredLength), outputPtr.baseAddress), "Failed to perform IFFT") + } + } + } + } + return output + } + + public static func get_psd(data: [Double], start_pos: Int, end_pos: Int, sampling_rate: Int, window: Int) throws -> PSD { + guard start_pos >= 0, end_pos <= data.count, start_pos < end_pos else { throw invalidArguments("Invalid position arguments") } + var input = Array(data[start_pos.. PSD { + try get_psd(data: data, start_pos: start_pos, end_pos: end_pos, sampling_rate: sampling_rate, window: window.rawValue) + } + + public static func get_psd(data: [Double], sampling_rate: Int, window: Int) throws -> PSD { + try get_psd(data: data, start_pos: 0, end_pos: data.count, sampling_rate: sampling_rate, window: window) + } + + public static func get_psd(data: [Double], sampling_rate: Int, window: WindowOperations) throws -> PSD { + try get_psd(data: data, start_pos: 0, end_pos: data.count, sampling_rate: sampling_rate, window: window.rawValue) + } + + public static func get_psd_welch(data: [Double], nfft: Int, overlap: Int, sampling_rate: Int, window: Int) throws -> PSD { + guard nfft > 0, nfft & (nfft - 1) == 0 else { throw invalidArguments("nfft must be a positive power of two") } + guard data.count >= nfft else { throw invalidArguments("nfft must be less than or equal to data count") } + guard overlap >= 0, overlap < nfft else { throw invalidArguments("overlap must be non-negative and less than nfft") } + guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") } + var input = data + var ampl = [Double](repeating: 0.0, count: nfft / 2 + 1) + var freq = [Double](repeating: 0.0, count: nfft / 2 + 1) + try input.withUnsafeMutableBufferPointer { inputPtr in + try ampl.withUnsafeMutableBufferPointer { amplPtr in + try freq.withUnsafeMutableBufferPointer { freqPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_psd_welch(inputPtr.baseAddress, CInt(data.count), CInt(nfft), CInt(overlap), CInt(sampling_rate), CInt(window), amplPtr.baseAddress, freqPtr.baseAddress), "Failed to get PSD Welch") + } + } + } + } + return PSD(ampl: ampl, freq: freq) + } + + public static func get_psd_welch(data: [Double], nfft: Int, overlap: Int, sampling_rate: Int, window: WindowOperations) throws -> PSD { + try get_psd_welch(data: data, nfft: nfft, overlap: overlap, sampling_rate: sampling_rate, window: window.rawValue) + } + + public static func get_band_power(psd: PSD, freq_start: Double, freq_end: Double) throws -> Double { + guard !psd.ampl.isEmpty, psd.ampl.count == psd.freq.count else { throw invalidArguments("PSD arrays must be non-empty and have equal lengths") } + guard freq_start < freq_end else { throw invalidArguments("freq_start must be less than freq_end") } + var ampl = psd.ampl + var freq = psd.freq + var output = 0.0 + try ampl.withUnsafeMutableBufferPointer { amplPtr in + try freq.withUnsafeMutableBufferPointer { freqPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_band_power(amplPtr.baseAddress, freqPtr.baseAddress, CInt(psd.ampl.count), freq_start, freq_end, &output), "Failed to get band power") + } + } + } + return output + } + + public static func get_avg_band_powers(data: [[Double]], channels: [Int], sampling_rate: Int, apply_filter: Bool) throws -> BandPowerResult { + let defaultBands = [ + FrequencyBand(start: 2.0, stop: 4.0), + FrequencyBand(start: 4.0, stop: 8.0), + FrequencyBand(start: 8.0, stop: 13.0), + FrequencyBand(start: 13.0, stop: 30.0), + FrequencyBand(start: 30.0, stop: 45.0) + ] + return try get_custom_band_powers(data: data, bands: defaultBands, channels: channels, sampling_rate: sampling_rate, apply_filter: apply_filter) + } + + public static func get_custom_band_powers( + data: [[Double]], + bands: [FrequencyBand], + channels: [Int], + sampling_rate: Int, + apply_filter: Bool + ) throws -> BandPowerResult { + let (rows, cols) = try BrainFlowArray.validateRectangular(data) + guard !channels.isEmpty, !bands.isEmpty else { throw invalidArguments("Channels and bands must be non-empty") } + guard channels.allSatisfy({ $0 >= 0 && $0 < rows }) else { throw invalidArguments("Channel index is out of range") } + guard bands.allSatisfy({ $0.start < $0.stop }) else { throw invalidArguments("Band start frequency must be less than stop frequency") } + guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") } + var selected = [Double]() + selected.reserveCapacity(channels.count * cols) + for channel in channels { + selected.append(contentsOf: data[channel]) + } + var starts = bands.map(\.start) + var stops = bands.map(\.stop) + var avg = [Double](repeating: 0.0, count: bands.count) + var stddev = [Double](repeating: 0.0, count: bands.count) + try selected.withUnsafeMutableBufferPointer { dataPtr in + try starts.withUnsafeMutableBufferPointer { startsPtr in + try stops.withUnsafeMutableBufferPointer { stopsPtr in + try avg.withUnsafeMutableBufferPointer { avgPtr in + try stddev.withUnsafeMutableBufferPointer { stddevPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_custom_band_powers(dataPtr.baseAddress, CInt(channels.count), CInt(cols), startsPtr.baseAddress, stopsPtr.baseAddress, CInt(bands.count), CInt(sampling_rate), apply_filter ? 1 : 0, avgPtr.baseAddress, stddevPtr.baseAddress), "Failed to get custom band powers") + } + } + } + } + } + } + return BandPowerResult(average: avg, stddev: stddev) + } + + public static func perform_ica(data: [[Double]], num_components: Int, channels: [Int]? = nil) throws -> ICAResult { + let (rows, cols) = try BrainFlowArray.validateRectangular(data) + let selectedChannels = channels ?? Array(0..= 0 && $0 < rows }) else { throw invalidArguments("Channel index is out of range") } + guard cols >= 2 else { throw invalidArguments("ICA data must contain at least two samples") } + guard selectedChannels.count >= 2 else { throw invalidArguments("ICA requires at least two channels") } + guard num_components >= 2, num_components <= selectedChannels.count else { throw invalidArguments("num_components must be between 2 and the selected channel count") } + var selected = [Double]() + selected.reserveCapacity(selectedChannels.count * cols) + for channel in selectedChannels { + selected.append(contentsOf: data[channel]) + } + var w = [Double](repeating: 0.0, count: num_components * num_components) + var k = [Double](repeating: 0.0, count: selectedChannels.count * num_components) + var a = [Double](repeating: 0.0, count: num_components * selectedChannels.count) + var s = [Double](repeating: 0.0, count: cols * num_components) + try selected.withUnsafeMutableBufferPointer { dataPtr in + try w.withUnsafeMutableBufferPointer { wPtr in + try k.withUnsafeMutableBufferPointer { kPtr in + try a.withUnsafeMutableBufferPointer { aPtr in + try s.withUnsafeMutableBufferPointer { sPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_ica(dataPtr.baseAddress, CInt(selectedChannels.count), CInt(cols), CInt(num_components), wPtr.baseAddress, kPtr.baseAddress, aPtr.baseAddress, sPtr.baseAddress), "Failed to perform ICA") + } + } + } + } + } + } + return ICAResult( + w: BrainFlowArray.reshape_data_to_2d(num_rows: num_components, num_cols: num_components, linear_buffer: w), + k: BrainFlowArray.reshape_data_to_2d(num_rows: num_components, num_cols: selectedChannels.count, linear_buffer: k), + a: BrainFlowArray.reshape_data_to_2d(num_rows: selectedChannels.count, num_cols: num_components, linear_buffer: a), + s: BrainFlowArray.reshape_data_to_2d(num_rows: num_components, num_cols: cols, linear_buffer: s) + ) + } + + public static func calc_stddev(data: [Double], start_pos: Int? = nil, end_pos: Int? = nil) throws -> Double { + var input = data + let start = start_pos ?? 0 + let end = end_pos ?? data.count + guard start >= 0, end <= data.count, start < end else { throw invalidArguments("Invalid position arguments") } + var output = 0.0 + try input.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.calc_stddev(pointer.baseAddress, CInt(start), CInt(end), &output), "Failed to calc stddev") + } + } + return output + } + + public static func get_railed_percentage(data: [Double], gain: Int) throws -> Double { + guard !data.isEmpty else { throw invalidArguments("data must be non-empty") } + var input = data + var output = 0.0 + try input.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_railed_percentage(pointer.baseAddress, CInt(data.count), CInt(gain), &output), "Failed to get railed percentage") + } + } + return output + } + + public static func get_oxygen_level(ppg_ir: [Double], ppg_red: [Double], sampling_rate: Int, coef1: Double = 1.5958422, coef2: Double = -34.6596622, coef3: Double = 112.6898759) throws -> Double { + guard !ppg_ir.isEmpty, ppg_ir.count == ppg_red.count else { throw invalidArguments("PPG arrays must be non-empty and have equal lengths") } + guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") } + var ir = ppg_ir + var red = ppg_red + var output = 0.0 + try ir.withUnsafeMutableBufferPointer { irPtr in + try red.withUnsafeMutableBufferPointer { redPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_oxygen_level(irPtr.baseAddress, redPtr.baseAddress, CInt(ppg_ir.count), CInt(sampling_rate), coef1, coef2, coef3, &output), "Failed to get oxygen level") + } + } + } + return output + } + + public static func get_heart_rate(ppg_ir: [Double], ppg_red: [Double], sampling_rate: Int, fft_size: Int) throws -> Double { + guard !ppg_ir.isEmpty, ppg_ir.count == ppg_red.count else { throw invalidArguments("PPG arrays must be non-empty and have equal lengths") } + guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") } + guard fft_size >= 1024, fft_size % 2 == 0 else { throw invalidArguments("fft_size must be even and at least 1024") } + var ir = ppg_ir + var red = ppg_red + var output = 0.0 + try ir.withUnsafeMutableBufferPointer { irPtr in + try red.withUnsafeMutableBufferPointer { redPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_heart_rate(irPtr.baseAddress, redPtr.baseAddress, CInt(ppg_ir.count), CInt(sampling_rate), CInt(fft_size), &output), "Failed to get heart rate") + } + } + } + return output + } + + public static func get_nearest_power_of_two(_ value: Int) throws -> Int { + var output: CInt = 0 + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_nearest_power_of_two(CInt(value), &output), "Failed to get nearest power of two") + } + return Int(output) + } + + public static func write_file(data: [[Double]], file_name: String, file_mode: String) throws { + let (rows, cols) = try BrainFlowArray.validateRectangular(data) + var linear = reshape_data_to_1d(num_rows: rows, num_cols: cols, buf: data) + try file_name.withCString { fileNamePtr in + try file_mode.withCString { fileModePtr in + try linear.withUnsafeMutableBufferPointer { linearPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.write_file(linearPtr.baseAddress, CInt(rows), CInt(cols), fileNamePtr, fileModePtr), "Failed to write file") + } + } + } + } + } + + public static func read_file(_ file_name: String) throws -> [[Double]] { + var elements: CInt = 0 + try file_name.withCString { fileNamePtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_num_elements_in_file(fileNamePtr, &elements), "Failed to determine number of file elements") + } + } + guard elements >= 0 else { throw invalidArguments("File element count must be non-negative") } + var data = [Double](repeating: 0.0, count: Int(elements)) + var rows: CInt = 0 + var cols: CInt = 0 + try file_name.withCString { fileNamePtr in + try data.withUnsafeMutableBufferPointer { dataPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.read_file(dataPtr.baseAddress, &rows, &cols, fileNamePtr, elements), "Failed to read file") + } + } + } + return BrainFlowArray.reshape_data_to_2d(num_rows: Int(rows), num_cols: Int(cols), linear_buffer: data) + } + + public static func reshape_data_to_1d(num_rows: Int, num_cols: Int, buf: [[Double]]) -> [Double] { + BrainFlowArray.reshape_data_to_1d(num_rows: num_rows, num_cols: num_cols, buf: buf) + } + + public static func reshape_data_to_2d(num_rows: Int, num_cols: Int, linear_buffer: [Double]) -> [[Double]] { + BrainFlowArray.reshape_data_to_2d(num_rows: num_rows, num_cols: num_cols, linear_buffer: linear_buffer) + } + + private static func withMutableData(_ data: inout [Double], _ body: (UnsafeMutablePointer?, Int) throws -> T) throws -> T { + try data.withUnsafeMutableBufferPointer { pointer in + try body(pointer.baseAddress, pointer.count) + } + } + + private static func getVersion(function: KeyPath) throws -> String { + let maxLength = 64 + var bytes = [CChar](repeating: 0, count: maxLength) + var length: CInt = 0 + try bytes.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native[keyPath: function](pointer.baseAddress, &length, CInt(maxLength)), "Error in get_version") + } + } + return String(bytes: bytes.prefix(Int(length)).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } +} + +final class DataFilterNative { + typealias VersionFunction = @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt) -> CInt + + let perform_lowpass: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, CInt, CInt, Double) -> CInt + let perform_highpass: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, CInt, CInt, Double) -> CInt + let perform_bandpass: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, Double, CInt, CInt, Double) -> CInt + let perform_bandstop: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, Double, CInt, CInt, Double) -> CInt + let remove_environmental_noise: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt) -> CInt + let perform_rolling_filter: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt) -> CInt + let perform_downsampling: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?) -> CInt + let perform_wavelet_transform: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let perform_inverse_wavelet_transform: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let perform_wavelet_denoising: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, CInt, CInt, CInt) -> CInt + let get_csp: @convention(c) (UnsafePointer?, UnsafePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let get_window: @convention(c) (CInt, CInt, UnsafeMutablePointer?) -> CInt + let perform_fft: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let perform_ifft: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, UnsafeMutablePointer?) -> CInt + let get_nearest_power_of_two: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt + let get_psd: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let detrend: @convention(c) (UnsafeMutablePointer?, CInt, CInt) -> CInt + let calc_stddev: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?) -> CInt + let get_psd_welch: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let get_band_power: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, Double, Double, UnsafeMutablePointer?) -> CInt + let get_custom_band_powers: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let get_railed_percentage: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?) -> CInt + let get_oxygen_level: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, CInt, Double, Double, Double, UnsafeMutablePointer?) -> CInt + let get_heart_rate: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?) -> CInt + let restore_data_from_wavelet_detailed_coeffs: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, UnsafeMutablePointer?) -> CInt + let detect_peaks_z_score: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, Double, UnsafeMutablePointer?) -> CInt + let perform_ica: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let set_log_level_data_handler: @convention(c) (CInt) -> CInt + let set_log_file_data_handler: @convention(c) (UnsafePointer?) -> CInt + let log_message_data_handler: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt + let write_file: @convention(c) (UnsafePointer?, CInt, CInt, UnsafePointer?, UnsafePointer?) -> CInt + let read_file: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafePointer?, CInt) -> CInt + let get_num_elements_in_file: @convention(c) (UnsafePointer?, UnsafeMutablePointer?) -> CInt + let get_version_data_handler: VersionFunction + + private static let lock = NSLock() + private static var cached: DataFilterNative? + + static func withData(_ body: (DataFilterNative) throws -> T) throws -> T { + try body(load()) + } + + private static func load() throws -> DataFilterNative { + lock.lock() + defer { lock.unlock() } + if let cached { return cached } + let value = try DataFilterNative(library: NativeLibraries.dataHandler.load()) + cached = value + return value + } + + private init(library: NativeLibrary) throws { + perform_lowpass = try library.symbol("perform_lowpass", as: type(of: perform_lowpass)) + perform_highpass = try library.symbol("perform_highpass", as: type(of: perform_highpass)) + perform_bandpass = try library.symbol("perform_bandpass", as: type(of: perform_bandpass)) + perform_bandstop = try library.symbol("perform_bandstop", as: type(of: perform_bandstop)) + remove_environmental_noise = try library.symbol("remove_environmental_noise", as: type(of: remove_environmental_noise)) + perform_rolling_filter = try library.symbol("perform_rolling_filter", as: type(of: perform_rolling_filter)) + perform_downsampling = try library.symbol("perform_downsampling", as: type(of: perform_downsampling)) + perform_wavelet_transform = try library.symbol("perform_wavelet_transform", as: type(of: perform_wavelet_transform)) + perform_inverse_wavelet_transform = try library.symbol("perform_inverse_wavelet_transform", as: type(of: perform_inverse_wavelet_transform)) + perform_wavelet_denoising = try library.symbol("perform_wavelet_denoising", as: type(of: perform_wavelet_denoising)) + get_csp = try library.symbol("get_csp", as: type(of: get_csp)) + get_window = try library.symbol("get_window", as: type(of: get_window)) + perform_fft = try library.symbol("perform_fft", as: type(of: perform_fft)) + perform_ifft = try library.symbol("perform_ifft", as: type(of: perform_ifft)) + get_nearest_power_of_two = try library.symbol("get_nearest_power_of_two", as: type(of: get_nearest_power_of_two)) + get_psd = try library.symbol("get_psd", as: type(of: get_psd)) + detrend = try library.symbol("detrend", as: type(of: detrend)) + calc_stddev = try library.symbol("calc_stddev", as: type(of: calc_stddev)) + get_psd_welch = try library.symbol("get_psd_welch", as: type(of: get_psd_welch)) + get_band_power = try library.symbol("get_band_power", as: type(of: get_band_power)) + get_custom_band_powers = try library.symbol("get_custom_band_powers", as: type(of: get_custom_band_powers)) + get_railed_percentage = try library.symbol("get_railed_percentage", as: type(of: get_railed_percentage)) + get_oxygen_level = try library.symbol("get_oxygen_level", as: type(of: get_oxygen_level)) + get_heart_rate = try library.symbol("get_heart_rate", as: type(of: get_heart_rate)) + restore_data_from_wavelet_detailed_coeffs = try library.symbol("restore_data_from_wavelet_detailed_coeffs", as: type(of: restore_data_from_wavelet_detailed_coeffs)) + detect_peaks_z_score = try library.symbol("detect_peaks_z_score", as: type(of: detect_peaks_z_score)) + perform_ica = try library.symbol("perform_ica", as: type(of: perform_ica)) + set_log_level_data_handler = try library.symbol("set_log_level_data_handler", as: type(of: set_log_level_data_handler)) + set_log_file_data_handler = try library.symbol("set_log_file_data_handler", as: type(of: set_log_file_data_handler)) + log_message_data_handler = try library.symbol("log_message_data_handler", as: type(of: log_message_data_handler)) + write_file = try library.symbol("write_file", as: type(of: write_file)) + read_file = try library.symbol("read_file", as: type(of: read_file)) + get_num_elements_in_file = try library.symbol("get_num_elements_in_file", as: type(of: get_num_elements_in_file)) + get_version_data_handler = try library.symbol("get_version_data_handler", as: type(of: get_version_data_handler)) + } +} diff --git a/swift_package/Sources/BrainFlow/MLModel.swift b/swift_package/Sources/BrainFlow/MLModel.swift new file mode 100644 index 000000000..59ce3a8d3 --- /dev/null +++ b/swift_package/Sources/BrainFlow/MLModel.swift @@ -0,0 +1,140 @@ +import Foundation + +public final class MLModel { + private let params: BrainFlowModelParams + private let serialized_params: String + + public init(params: BrainFlowModelParams) throws { + self.params = params + serialized_params = try params.to_json() + } + + public static func set_log_level(_ log_level: Int) throws { + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.set_log_level_ml_module(CInt(log_level)), "Error in set_log_level") + } + } + + public static func set_log_level(_ log_level: LogLevels) throws { + try set_log_level(log_level.rawValue) + } + + public static func enable_ml_logger() throws { + try set_log_level(LogLevels.LEVEL_INFO) + } + + public static func enable_dev_ml_logger() throws { + try set_log_level(LogLevels.LEVEL_TRACE) + } + + public static func disable_ml_logger() throws { + try set_log_level(LogLevels.LEVEL_OFF) + } + + public static func set_log_file(_ log_file: String) throws { + try log_file.withCString { path in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.set_log_file_ml_module(path), "Error in set_log_file") + } + } + } + + public static func log_message(_ log_level: Int, message: String) throws { + var mutableMessage = Array(message.utf8CString) + try mutableMessage.withUnsafeMutableBufferPointer { pointer in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.log_message_ml_module(CInt(log_level), pointer.baseAddress), "Error in log_message") + } + } + } + + public static func release_all() throws { + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.release_all(), "Error in release_all") + } + } + + public static func get_version() throws -> String { + let maxLength = 64 + var bytes = [CChar](repeating: 0, count: maxLength) + var length: CInt = 0 + try bytes.withUnsafeMutableBufferPointer { pointer in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.get_version_ml_module(pointer.baseAddress, &length, CInt(maxLength)), "Error in get_version") + } + } + return String(bytes: bytes.prefix(Int(length)).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } + + public func prepare() throws { + try serialized_params.withCString { paramsPtr in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.prepare(paramsPtr), "Error in prepare") + } + } + } + + public func release() throws { + try serialized_params.withCString { paramsPtr in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.release(paramsPtr), "Error in release") + } + } + } + + public func predict(input_data: [Double]) throws -> [Double] { + var input = input_data + var output = [Double](repeating: 0.0, count: params.max_array_size) + var outputLen: CInt = 0 + try serialized_params.withCString { paramsPtr in + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.predict(inputPtr.baseAddress, CInt(input_data.count), outputPtr.baseAddress, &outputLen, paramsPtr), "Error in predict") + } + } + } + } + return Array(output.prefix(Int(outputLen))) + } +} + +final class MLModelNative { + typealias VersionFunction = @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt) -> CInt + + let prepare: @convention(c) (UnsafePointer?) -> CInt + let predict: @convention(c) (UnsafeMutablePointer?, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafePointer?) -> CInt + let release: @convention(c) (UnsafePointer?) -> CInt + let release_all: @convention(c) () -> CInt + let set_log_level_ml_module: @convention(c) (CInt) -> CInt + let set_log_file_ml_module: @convention(c) (UnsafePointer?) -> CInt + let log_message_ml_module: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt + let get_version_ml_module: VersionFunction + + private static let lock = NSLock() + private static var cached: MLModelNative? + + static func withML(_ body: (MLModelNative) throws -> T) throws -> T { + try body(load()) + } + + private static func load() throws -> MLModelNative { + lock.lock() + defer { lock.unlock() } + if let cached { return cached } + let value = try MLModelNative(library: NativeLibraries.mlModule.load()) + cached = value + return value + } + + private init(library: NativeLibrary) throws { + prepare = try library.symbol("prepare", as: type(of: prepare)) + predict = try library.symbol("predict", as: type(of: predict)) + release = try library.symbol("release", as: type(of: release)) + release_all = try library.symbol("release_all", as: type(of: release_all)) + set_log_level_ml_module = try library.symbol("set_log_level_ml_module", as: type(of: set_log_level_ml_module)) + set_log_file_ml_module = try library.symbol("set_log_file_ml_module", as: type(of: set_log_file_ml_module)) + log_message_ml_module = try library.symbol("log_message_ml_module", as: type(of: log_message_ml_module)) + get_version_ml_module = try library.symbol("get_version_ml_module", as: type(of: get_version_ml_module)) + } +} diff --git a/swift_package/Sources/BrainFlowCLI/main.swift b/swift_package/Sources/BrainFlowCLI/main.swift new file mode 100644 index 000000000..c098c9aa8 --- /dev/null +++ b/swift_package/Sources/BrainFlowCLI/main.swift @@ -0,0 +1,30 @@ +import BrainFlow +import Foundation + +let boardId = BoardIds.SYNTHETIC_BOARD +var params = BrainFlowInputParams() + +do { + try BoardShim.enable_board_logger() + let board = try BoardShim(board_id: boardId, input_params: params) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + Thread.sleep(forTimeInterval: 2.0) + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + let rows = data.count + let cols = data.first?.count ?? 0 + let samplingRate = try BoardShim.get_sampling_rate(board_id: boardId.rawValue) + let eegChannels = try BoardShim.get_eeg_channels(board_id: boardId.rawValue) + + print("BrainFlow Swift synthetic board sample") + print("board_id=\(boardId.rawValue)") + print("sampling_rate=\(samplingRate)") + print("rows=\(rows) cols=\(cols)") + print("eeg_channels=\(eegChannels)") +} catch { + fputs("BrainFlow CLI failed: \(error)\n", stderr) + exit(1) +} diff --git a/swift_package/Sources/BrainFlowMacDemo/main.swift b/swift_package/Sources/BrainFlowMacDemo/main.swift new file mode 100644 index 000000000..a1de361a3 --- /dev/null +++ b/swift_package/Sources/BrainFlowMacDemo/main.swift @@ -0,0 +1,254 @@ +import BrainFlow +import Foundation +import SwiftUI + +#if os(macOS) +import AppKit +#endif + +@main +struct BrainFlowMacDemoApp: App { + private let autorun = ProcessInfo.processInfo.environment["BRAINFLOW_MAC_DEMO_AUTORUN"] == "1" + + var body: some Scene { + WindowGroup { + ContentView(autorun: autorun) + } + } +} + +struct ContentView: View { + let autorun: Bool + + @State private var status = "Idle" + @State private var rows = 0 + @State private var cols = 0 + @State private var isRunning = false + @State private var board: BoardShim? + @State private var didAutorun = false + @State private var pollingTask: Task? + @State private var eegSeries = [[Double]]() + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text("BrainFlow Synthetic Board") + .font(.title2) + .fontWeight(.semibold) + + VStack(alignment: .leading, spacing: 8) { + infoRow("Status", status) + infoRow("Rows", "\(rows)") + infoRow("Samples", "\(cols)") + } + + EEGPlotView(series: eegSeries) + .frame(height: 150) + + HStack { + Button(isRunning ? "Stop" : "Start") { + isRunning ? stop() : start() + } + .keyboardShortcut(.defaultAction) + + Button("Read") { + read() + } + .disabled(isRunning) + + Button("Release") { + release() + } + } + } + .padding(24) + .frame(minWidth: 520, minHeight: 420) + .task { + guard autorun, !didAutorun else { return } + didAutorun = true + await runAutomatedDemo() + } + .onDisappear { + pollingTask?.cancel() + } + } + + private func infoRow(_ label: String, _ value: String) -> some View { + HStack(alignment: .firstTextBaseline) { + Text(label) + .fontWeight(.medium) + .frame(width: 84, alignment: .leading) + Text(value) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + + private func start() { + do { + pollingTask?.cancel() + try? board?.release_session() + + let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + self.board = board + rows = try BoardShim.get_num_rows(board_id: BoardIds.SYNTHETIC_BOARD) + cols = 0 + eegSeries = [] + status = "Streaming synthetic data" + isRunning = true + startPolling() + } catch { + status = "Start failed: \(error)" + } + } + + private func stop() { + do { + pollingTask?.cancel() + pollCurrentData() + try board?.stop_stream() + isRunning = false + status = "Stopped" + } catch { + status = "Stop failed: \(error)" + } + } + + private func read() { + do { + let data = try board?.get_board_data() ?? [] + updateDisplay(with: data) + status = "Read \(cols) samples" + } catch { + status = "Read failed: \(error)" + } + } + + private func release() { + do { + pollingTask?.cancel() + if isRunning { + try board?.stop_stream() + } + try board?.release_session() + board = nil + isRunning = false + status = "Released" + } catch { + status = "Release failed: \(error)" + } + } + + private func startPolling() { + pollingTask?.cancel() + pollingTask = Task { @MainActor in + while !Task.isCancelled { + pollCurrentData() + try? await Task.sleep(nanoseconds: 250_000_000) + } + } + } + + private func pollCurrentData() { + guard let board else { return } + + do { + let bufferedSamples = try board.get_board_data_count() + let previewSamples = max(min(bufferedSamples, 250), 1) + let data = try board.get_current_board_data(num_samples: previewSamples) + updateDisplay(with: data, sampleCount: bufferedSamples) + } catch { + status = "Read failed: \(error)" + } + } + + private func updateDisplay(with data: [[Double]], sampleCount: Int? = nil) { + rows = data.count + cols = sampleCount ?? (data.first?.count ?? 0) + + let eegChannels = (try? BoardShim.get_eeg_channels(board_id: BoardIds.SYNTHETIC_BOARD)) ?? [] + eegSeries = eegChannels.prefix(4).compactMap { channel in + guard channel >= 0, channel < data.count else { return nil } + return Array(data[channel].suffix(250)) + } + } + + @MainActor + private func runAutomatedDemo() async { + start() + guard isRunning else { + print("BrainFlowMacDemo virtual board demo failed: \(status)") + terminateIfRequested() + return + } + + try? await Task.sleep(nanoseconds: 2_000_000_000) + stop() + read() + let measuredRows = rows + let measuredCols = cols + release() + status = "Demo complete: \(measuredCols) samples" + print("BrainFlowMacDemo virtual board demo passed: rows=\(measuredRows) samples=\(measuredCols)") + terminateIfRequested() + } + + private func terminateIfRequested() { + guard ProcessInfo.processInfo.environment["BRAINFLOW_MAC_DEMO_EXIT_AFTER_AUTORUN"] == "1" else { return } + #if os(macOS) + NSApplication.shared.terminate(nil) + #endif + } +} + +private struct EEGPlotView: View { + let series: [[Double]] + private let colors: [Color] = [.blue, .green, .orange, .purple] + + var body: some View { + GeometryReader { proxy in + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: 8) + .fill(Color.secondary.opacity(0.12)) + + ForEach(Array(series.prefix(4).enumerated()), id: \.offset) { index, values in + path(for: values, channelIndex: index, channelCount: max(series.prefix(4).count, 1), size: proxy.size) + .stroke(colors[index % colors.count], lineWidth: 1.5) + } + + if series.isEmpty { + Text("Waiting for samples") + .foregroundStyle(.secondary) + .padding(12) + } + } + } + } + + private func path(for values: [Double], channelIndex: Int, channelCount: Int, size: CGSize) -> Path { + let samples = values.filter { $0.isFinite } + guard samples.count > 1 else { return Path() } + + let minValue = samples.min() ?? 0.0 + let maxValue = samples.max() ?? 0.0 + let span = max(maxValue - minValue, 1.0) + let laneHeight = size.height / CGFloat(channelCount) + let laneTop = laneHeight * CGFloat(channelIndex) + let lanePadding = laneHeight * 0.12 + let drawableHeight = max(laneHeight - lanePadding * 2, 1) + let stepX = size.width / CGFloat(samples.count - 1) + + var path = Path() + for (index, sample) in samples.enumerated() { + let normalized = (sample - minValue) / span + let x = CGFloat(index) * stepX + let y = laneTop + lanePadding + CGFloat(1.0 - normalized) * drawableHeight + if index == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } + return path + } +} diff --git a/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift b/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift new file mode 100644 index 000000000..54ccae074 --- /dev/null +++ b/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift @@ -0,0 +1,182 @@ +import XCTest +@testable import BrainFlow + +final class BrainFlowTests: XCTestCase { + private func requireNativeLibraries() throws { + do { + _ = try BoardShim.get_version() + } catch { + throw XCTSkip("BrainFlow native libraries are not available; build BrainFlow into installed/lib or set BRAINFLOW_LIB_DIR.") + } + } + + private func assertInvalidArguments( + _ expression: @autoclosure () throws -> T, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertThrowsError(try expression(), file: file, line: line) { error in + guard let brainFlowError = error as? BrainFlowError else { + return XCTFail("Expected BrainFlowError, got \(error)", file: file, line: line) + } + XCTAssertEqual( + brainFlowError.exit_code, + BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.rawValue, + file: file, + line: line + ) + } + } + + func testInputParamsJSON() throws { + var params = BrainFlowInputParams() + params.serial_port = "/dev/ttyUSB0" + params.set_master_board(.SYNTHETIC_BOARD) + let json = try params.to_json() + + XCTAssertTrue(json.contains("serial_port")) + XCTAssertTrue(json.contains("ttyUSB0")) + XCTAssertTrue(json.contains("master_board")) + } + + func testBoardShimRejectsInvalidArgumentsBeforeNativeCalls() throws { + let board = try BoardShim(board_id: .SYNTHETIC_BOARD) + + assertInvalidArguments(try board.start_stream(buffer_size: 0)) + assertInvalidArguments(try board.config_board_with_bytes([])) + assertInvalidArguments(try board.get_current_board_data(num_samples: 0)) + } + + func testDataFilterRejectsInvalidArgumentsBeforeNativeCalls() throws { + assertInvalidArguments(try DataFilter.get_csp(data: [[[1.0, 2.0]], [[3.0]]], labels: [0.0, 1.0])) + assertInvalidArguments(try DataFilter.get_csp(data: [[[1.0, 2.0]]], labels: [])) + assertInvalidArguments(try DataFilter.get_window(window_function: WindowOperations.HANNING.rawValue, window_len: 0)) + assertInvalidArguments(try DataFilter.perform_ifft(data: [Complex(real: 1.0, imag: 0.0)])) + assertInvalidArguments(try DataFilter.get_psd_welch(data: [1.0, 2.0, 3.0], nfft: 4, overlap: 0, sampling_rate: 250, window: WindowOperations.NO_WINDOW.rawValue)) + assertInvalidArguments(try DataFilter.get_band_power(psd: PSD(ampl: [1.0], freq: []), freq_start: 1.0, freq_end: 2.0)) + assertInvalidArguments(try DataFilter.get_custom_band_powers( + data: [[1.0, 2.0]], + bands: [FrequencyBand(start: 1.0, stop: 2.0)], + channels: [1], + sampling_rate: 250, + apply_filter: false + )) + assertInvalidArguments(try DataFilter.perform_ica(data: [[1.0, 2.0], [3.0, 4.0]], num_components: 3)) + assertInvalidArguments(try DataFilter.calc_stddev(data: [1.0, 2.0], start_pos: 1, end_pos: 3)) + assertInvalidArguments(try DataFilter.get_railed_percentage(data: [], gain: 24)) + assertInvalidArguments(try DataFilter.get_oxygen_level(ppg_ir: [1.0], ppg_red: [1.0, 2.0], sampling_rate: 25)) + assertInvalidArguments(try DataFilter.get_heart_rate(ppg_ir: [1.0, 2.0], ppg_red: [1.0, 2.0], sampling_rate: 25, fft_size: 1023)) + } + + func testBrainFlowGetDataSyntheticBoard() throws { + try requireNativeLibraries() + let board = try BoardShim(board_id: .SYNTHETIC_BOARD) + try board.prepare_session() + XCTAssertTrue(try board.is_prepared()) + + try board.start_stream(buffer_size: 45000) + Thread.sleep(forTimeInterval: 1.0) + XCTAssertGreaterThan(try board.get_board_data_count(), 0) + + let currentData = try board.get_current_board_data(num_samples: 16) + XCTAssertEqual(currentData.count, try BoardShim.get_num_rows(board_id: BoardIds.SYNTHETIC_BOARD.rawValue)) + XCTAssertLessThanOrEqual(currentData.first?.count ?? 0, 16) + + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + XCTAssertEqual(data.count, try BoardShim.get_num_rows(board_id: BoardIds.SYNTHETIC_BOARD.rawValue)) + XCTAssertGreaterThan(data.first?.count ?? 0, 0) + } + + func testMarkersSyntheticBoard() throws { + try requireNativeLibraries() + let board = try BoardShim(board_id: .SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + try board.insert_marker(1.0) + Thread.sleep(forTimeInterval: 0.5) + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + let markerChannel = try BoardShim.get_marker_channel(board_id: BoardIds.SYNTHETIC_BOARD.rawValue) + XCTAssertTrue(data[markerChannel].contains { abs($0 - 1.0) < 0.0001 }) + } + + func testReadWriteFile() throws { + try requireNativeLibraries() + let data = [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0] + ] + let fileName = NSTemporaryDirectory() + "/brainflow_swift_read_write.csv" + try DataFilter.write_file(data: data, file_name: fileName, file_mode: "w") + let restored = try DataFilter.read_file(fileName) + + XCTAssertEqual(restored.count, data.count) + XCTAssertEqual(restored.first?.count, data.first?.count) + } + + func testDownsamplingAndTransforms() throws { + try requireNativeLibraries() + let data = Array(0..<128).map(Double.init) + let downsampled = try DataFilter.perform_downsampling(data: data, period: 4, operation: .MEAN) + XCTAssertEqual(downsampled.count, 32) + + let fft = try DataFilter.perform_fft(data: data, start_pos: 0, end_pos: 128, window: .NO_WINDOW) + XCTAssertEqual(fft.count, 65) + + let restored = try DataFilter.perform_ifft(data: fft) + XCTAssertEqual(restored.count, 128) + } + + func testSignalFilteringDenoisingAndBandPower() throws { + try requireNativeLibraries() + var data = (0..<256).map { index in sin(Double(index) / 10.0) } + try DataFilter.perform_lowpass(data: &data, sampling_rate: 250, cutoff: 30.0, order: 4, filter_type: .BUTTERWORTH, ripple: 0.0) + try DataFilter.perform_highpass(data: &data, sampling_rate: 250, cutoff: 1.0, order: 4, filter_type: .BUTTERWORTH, ripple: 0.0) + try DataFilter.perform_wavelet_denoising( + data: &data, + wavelet: .DB5, + decomposition_level: 3, + wavelet_denoising: .SURESHRINK, + threshold: .HARD, + extension_type: .SYMMETRIC, + noise_level: .FIRST_LEVEL + ) + + let psd = try DataFilter.get_psd(data: data, start_pos: 0, end_pos: data.count, sampling_rate: 250, window: WindowOperations.HANNING.rawValue) + let power = try DataFilter.get_band_power(psd: psd, freq_start: 4.0, freq_end: 30.0) + XCTAssertTrue(power.isFinite) + } + + func testICA() throws { + try requireNativeLibraries() + let rows = 4 + let cols = 128 + let data = (0..&2\n exit 1\n fi\n if [ \"$PLATFORM_NAME\" = \"iphonesimulator\" ]; then\n find \"$xcframework\" -path \"*/${framework_name}.framework\" -type d | grep simulator | head -n 1\n elif [ \"$PLATFORM_NAME\" = \"iphoneos\" ]; then\n find \"$xcframework\" -path \"*/${framework_name}.framework\" -type d | grep '/ios-' | grep -v simulator | head -n 1\n else\n echo \"error: unsupported platform $PLATFORM_NAME for BrainFlow iOS demo\" >&2\n exit 1\n fi\n}\nfor framework_name in BoardController DataHandler MLModule; do\n src_framework=\"$(select_framework_slice \"$framework_name\")\"\n if [ -z \"$src_framework\" ]; then\n echo \"error: unable to select $framework_name slice for $PLATFORM_NAME from $XCFRAMEWORKS_DIR\" >&2\n exit 1\n fi\n dst_framework=\"$FRAMEWORKS_DIR/${framework_name}.framework\"\n rm -rf \"$dst_framework\"\n cp -R \"$src_framework\" \"$dst_framework\"\n codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY:--}\" --timestamp=none \"$dst_framework\" || codesign --force --sign - --timestamp=none \"$dst_framework\"\ndone\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A10000000000000000000031 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A10000000000000000000020 /* BrainFlowiOSDemoApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A10000000000000000000040 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + A10000000000000000000041 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A10000000000000000000043 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.brainflow.demo.ios; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A10000000000000000000044 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.brainflow.demo.ios; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A10000000000000000000042 /* Build configuration list for PBXProject "BrainFlowiOSDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A10000000000000000000040 /* Debug */, + A10000000000000000000041 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A10000000000000000000045 /* Build configuration list for PBXNativeTarget "BrainFlowiOSDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A10000000000000000000043 /* Debug */, + A10000000000000000000044 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../.." */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../..; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A10000000000000000000051 /* BrainFlow */ = { + isa = XCSwiftPackageProductDependency; + package = A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../.." */; + productName = BrainFlow; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = A10000000000000000000001 /* Project object */; +} diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme new file mode 100644 index 000000000..2abd7e3dd --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift new file mode 100644 index 000000000..8e6d72fbc --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift @@ -0,0 +1,303 @@ +import BrainFlow +import Foundation +import SwiftUI + +@main +struct BrainFlowiOSDemoApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} + +private struct BoardOption: Identifiable { + let id: Int + let title: String + let boardId: Int +} + +private let boardOptions = [ + BoardOption(id: BoardIds.SYNTHETIC_BOARD.rawValue, title: "Synthetic", boardId: BoardIds.SYNTHETIC_BOARD.rawValue), + BoardOption(id: BoardIds.MUSE_2_BOARD.rawValue, title: "Muse 2", boardId: BoardIds.MUSE_2_BOARD.rawValue), + BoardOption(id: BoardIds.MUSE_S_BOARD.rawValue, title: "Muse S", boardId: BoardIds.MUSE_S_BOARD.rawValue), + BoardOption(id: BoardIds.MUSE_2016_BOARD.rawValue, title: "Muse 2016", boardId: BoardIds.MUSE_2016_BOARD.rawValue), + BoardOption(id: BoardIds.MUSE_S_ATHENA_BOARD.rawValue, title: "Muse S Athena", boardId: BoardIds.MUSE_S_ATHENA_BOARD.rawValue) +] + +struct ContentView: View { + @State private var selectedBoardId = BoardIds.SYNTHETIC_BOARD.rawValue + @State private var activeBoardId = BoardIds.SYNTHETIC_BOARD.rawValue + @State private var serialNumber = "" + @State private var macAddress = "" + @State private var timeout = "15" + @State private var status = "Idle" + @State private var sampleCount = 0 + @State private var rowCount = 0 + @State private var board: BoardShim? + @State private var isStreaming = false + @State private var didRunAutomatedDemo = false + @State private var eegSeries = [[Double]]() + @State private var pollingTask: Task? + + var body: some View { + NavigationView { + Form { + Section("Board") { + Picker("Board", selection: $selectedBoardId) { + ForEach(boardOptions) { option in + Text(option.title).tag(option.boardId) + } + } + .disabled(isStreaming) + + if selectedBoardId != BoardIds.SYNTHETIC_BOARD.rawValue { + TextField("Serial Number", text: $serialNumber) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .disabled(isStreaming) + TextField("MAC Address", text: $macAddress) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .disabled(isStreaming) + TextField("Timeout", text: $timeout) + .keyboardType(.numberPad) + .disabled(isStreaming) + } + } + + Section("Session") { + infoRow("Status", status) + infoRow("Rows", "\(rowCount)") + infoRow("Samples", "\(sampleCount)") + } + + Section("EEG") { + EEGPlotView(series: eegSeries) + .frame(height: 160) + .padding(.vertical, 8) + } + + Section { + Button(isStreaming ? "Stop Stream" : "Start Stream") { + isStreaming ? stopStream() : startStream() + } + Button("Read Data") { + readData() + } + .disabled(isStreaming || board == nil) + Button("Release Session") { + releaseSession() + } + .disabled(board == nil) + } + } + .navigationTitle("BrainFlow Demo") + } + .navigationViewStyle(.stack) + .task { + await runAutomatedDemoIfRequested() + } + .onDisappear { + pollingTask?.cancel() + } + } + + private func infoRow(_ title: String, _ value: String) -> some View { + HStack { + Text(title) + Spacer() + Text(value) + .foregroundColor(.secondary) + .multilineTextAlignment(.trailing) + } + } + + private func startStream() { + do { + pollingTask?.cancel() + if isStreaming { + try? board?.stop_stream() + } + try? board?.release_session() + + var params = BrainFlowInputParams() + params.serial_number = serialNumber.trimmingCharacters(in: .whitespacesAndNewlines) + params.mac_address = macAddress.trimmingCharacters(in: .whitespacesAndNewlines) + params.timeout = Int(timeout) ?? 15 + + let board = try BoardShim(board_id: selectedBoardId, input_params: params) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + self.board = board + activeBoardId = selectedBoardId + status = "Streaming \(boardName(for: selectedBoardId))" + rowCount = try BoardShim.get_num_rows(board_id: selectedBoardId) + sampleCount = 0 + eegSeries = [] + isStreaming = true + startPolling() + } catch { + status = "Start failed: \(error)" + } + } + + private func stopStream() { + do { + pollingTask?.cancel() + pollCurrentData() + try board?.stop_stream() + status = "Stopped" + isStreaming = false + } catch { + status = "Stop failed: \(error)" + } + } + + private func readData() { + guard let board else { + status = "No active session" + return + } + + do { + let data = try board.get_board_data() + updateDisplay(with: data, boardId: activeBoardId) + status = "Read complete" + } catch { + status = "Read failed: \(error)" + } + } + + private func releaseSession() { + do { + pollingTask?.cancel() + if isStreaming { + try board?.stop_stream() + } + try board?.release_session() + board = nil + isStreaming = false + status = "Released" + } catch { + status = "Release failed: \(error)" + } + } + + private func startPolling() { + pollingTask?.cancel() + pollingTask = Task { @MainActor in + while !Task.isCancelled { + pollCurrentData() + try? await Task.sleep(nanoseconds: 250_000_000) + } + } + } + + private func pollCurrentData() { + guard let board else { return } + + do { + let bufferedSamples = try board.get_board_data_count() + let previewSamples = max(min(bufferedSamples, 250), 1) + let data = try board.get_current_board_data(num_samples: previewSamples) + updateDisplay(with: data, boardId: activeBoardId, sampleCount: bufferedSamples) + } catch { + status = "Read failed: \(error)" + } + } + + private func updateDisplay(with data: [[Double]], boardId: Int, sampleCount: Int? = nil) { + rowCount = data.count + self.sampleCount = sampleCount ?? (data.first?.count ?? 0) + + let eegChannels = (try? BoardShim.get_eeg_channels(board_id: boardId)) ?? [] + eegSeries = eegChannels.prefix(4).compactMap { channel in + guard channel >= 0, channel < data.count else { return nil } + return Array(data[channel].suffix(250)) + } + } + + private func boardName(for boardId: Int) -> String { + boardOptions.first { $0.boardId == boardId }?.title ?? "Board \(boardId)" + } + + private func runAutomatedDemoIfRequested() async { + let processInfo = ProcessInfo.processInfo + let shouldAutorun = processInfo.environment["BRAINFLOW_IOS_DEMO_AUTORUN"] == "1" || + processInfo.arguments.contains("--autorun") + guard !didRunAutomatedDemo, shouldAutorun else { + return + } + + didRunAutomatedDemo = true + selectedBoardId = BoardIds.SYNTHETIC_BOARD.rawValue + status = "Autorun starting" + startStream() + + try? await Task.sleep(nanoseconds: 2_000_000_000) + + stopStream() + readData() + let rows = rowCount + let samples = sampleCount + releaseSession() + + if rows > 0 && samples > 0 { + status = "Autorun passed: \(samples) samples" + print("BrainFlowiOSDemo autorun passed rows=\(rows) samples=\(samples)") + } else { + status = "Autorun failed" + print("BrainFlowiOSDemo autorun failed rows=\(rows) samples=\(samples)") + } + } +} + +private struct EEGPlotView: View { + let series: [[Double]] + private let colors: [Color] = [.blue, .green, .orange, .purple] + + var body: some View { + GeometryReader { proxy in + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(Color(.secondarySystemGroupedBackground)) + ForEach(Array(series.prefix(4).enumerated()), id: \.offset) { index, values in + path(for: values, channelIndex: index, channelCount: max(series.prefix(4).count, 1), size: proxy.size) + .stroke(colors[index % colors.count], lineWidth: 1.5) + } + if series.isEmpty { + Text("Waiting for samples") + .foregroundColor(.secondary) + } + } + } + } + + private func path(for values: [Double], channelIndex: Int, channelCount: Int, size: CGSize) -> Path { + let samples = values.filter { $0.isFinite } + guard samples.count > 1 else { return Path() } + + let minValue = samples.min() ?? 0.0 + let maxValue = samples.max() ?? 0.0 + let span = max(maxValue - minValue, 1.0) + let laneHeight = size.height / CGFloat(channelCount) + let laneTop = laneHeight * CGFloat(channelIndex) + let lanePadding = laneHeight * 0.12 + let drawableHeight = max(laneHeight - lanePadding * 2, 1) + let stepX = size.width / CGFloat(samples.count - 1) + + var path = Path() + for (index, sample) in samples.enumerated() { + let normalized = (sample - minValue) / span + let x = CGFloat(index) * stepX + let y = laneTop + lanePadding + CGFloat(1.0 - normalized) * drawableHeight + if index == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } + return path + } +} diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist b/swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist new file mode 100644 index 000000000..30e89aafd --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDisplayName + BrainFlow Demo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSBluetoothAlwaysUsageDescription + BrainFlow uses Bluetooth to connect to supported Muse boards. + UILaunchScreen + + + diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy b/swift_package/examples/apps/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..196836a1f --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + + diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md b/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md new file mode 100644 index 000000000..535f18272 --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md @@ -0,0 +1,38 @@ +# BrainFlow iOS Demo + +This sample is a normal Xcode iOS application that exercises the BrainFlow Swift package with the synthetic board, so it does not need external hardware for simulator, TestFlight, or App Review smoke testing. + +## Run In Simulator + +From the repository root, regenerate the Apple artifacts first: + +```bash +tools/apple/regenerate_artifacts.sh +tools/apple/verify_xcframeworks.sh build/apple_xcframeworks +``` + +Then open `BrainFlowiOSDemo.xcodeproj` in Xcode, select an iPhone simulator, and run the `BrainFlowiOSDemo` scheme. The app target links the local Swift package at `../../../..` and embeds native framework slices from `../../../../../build/apple_xcframeworks/XCFrameworks` by default. + +Set `BRAINFLOW_APPLE_XCFRAMEWORKS_DIR` in the Xcode build environment to use a different artifact directory. + +For command-line smoke testing, pass `--autorun` as a launch argument. The app starts the synthetic board, records data, stops streaming, releases the session, and displays the row/sample count and EEG plot. + +## App Store Preparation + +The simulator build is not enough for App Store distribution. For an iPhone archive, use the generated XCFrameworks and ensure the archive embeds signed `iphoneos` framework slices for: + +- `BoardController.framework` +- `DataHandler.framework` +- `MLModule.framework` + +Muse native BLE boards require BrainFlow native libraries built with BLE support for the target platform. The demo exposes board selection plus serial number, MAC address, and timeout fields for native BLE connections. + +Before upload, also replace the placeholders below. + +App Store placeholders to replace before upload: + +- Bundle ID: `org.brainflow.demo.ios` +- Display name and app icon +- Signing team and provisioning profile +- Screenshots for required iPhone/iPad sizes +- App privacy answers matching the final native libraries and any real-board permissions diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements b/swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements new file mode 100644 index 000000000..13cb114cf --- /dev/null +++ b/swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/Info.plist b/swift_package/examples/apps/macos/BrainFlowMacDemo/Info.plist new file mode 100644 index 000000000..3be9880f3 --- /dev/null +++ b/swift_package/examples/apps/macos/BrainFlowMacDemo/Info.plist @@ -0,0 +1,18 @@ + + + + + CFBundleDisplayName + BrainFlow Demo + CFBundleIdentifier + org.brainflow.demo.macos + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 12.0 + NSHighResolutionCapable + + + diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy b/swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..196836a1f --- /dev/null +++ b/swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + + diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md b/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md new file mode 100644 index 000000000..cfd300646 --- /dev/null +++ b/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md @@ -0,0 +1,32 @@ +# BrainFlow macOS Demo + +The buildable macOS SwiftUI demo target lives in `swift_package` as `BrainFlowMacDemo`. + +For Mac App Store distribution, use this folder's entitlements and privacy manifest as starting assets in an Xcode app target. Embed BrainFlow XCFramework products in the app bundle, sign them with the same team, and keep the sandbox entitlement enabled. + +Release placeholders to replace before upload: + +- Bundle ID: `org.brainflow.demo.macos` +- Signing team and provisioning profile +- App icon +- Mac App Store screenshots +- Final privacy answers for any real-board connectivity features + +Source-development smoke test: + +```bash +cd swift_package +BRAINFLOW_LIB_DIR=../installed/lib swift run BrainFlowMacDemo +``` + +App-bundle smoke test: + +```bash +tools/apple/regenerate_artifacts.sh +tools/apple/package_macos_demo_app.sh +``` + +The app bundle is written to `build/apple_xcframeworks/BrainFlowMacDemo.app` and embeds +`BoardController.framework`, `DataHandler.framework`, and `MLModule.framework` from +`build/apple_xcframeworks/XCFrameworks` by default. Run the app without `BRAINFLOW_LIB_DIR` to +verify production-style framework loading from the app bundle. diff --git a/swift_package/examples/tests/band_power/band_power.swift b/swift_package/examples/tests/band_power/band_power.swift new file mode 100644 index 000000000..7bbc8ec73 --- /dev/null +++ b/swift_package/examples/tests/band_power/band_power.swift @@ -0,0 +1,19 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum BandPowerExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + let data = try sample.firstEEGChannel() + let psd = try DataFilter.get_psd( + data: data, + start_pos: 0, + end_pos: data.count, + sampling_rate: sample.samplingRate, + window: WindowOperations.HANNING.rawValue + ) + let alpha = try DataFilter.get_band_power(psd: psd, freq_start: 8.0, freq_end: 13.0) + print("Alpha power: \(alpha)") + } +} diff --git a/swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift b/swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift new file mode 100644 index 000000000..5f28a1d36 --- /dev/null +++ b/swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift @@ -0,0 +1,18 @@ +import BrainFlow +import Foundation + +@main +enum BrainFlowGetDataExample { + static func main() throws { + let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + Thread.sleep(forTimeInterval: 5.0) + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + print("Rows: \(data.count)") + print("Samples: \(data.first?.count ?? 0)") + } +} diff --git a/swift_package/examples/tests/denoising/denoising.swift b/swift_package/examples/tests/denoising/denoising.swift new file mode 100644 index 000000000..a4488b966 --- /dev/null +++ b/swift_package/examples/tests/denoising/denoising.swift @@ -0,0 +1,20 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum DenoisingExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + var data = try sample.firstEEGChannel() + try DataFilter.perform_wavelet_denoising( + data: &data, + wavelet: WaveletTypes.DB5, + decomposition_level: 3, + wavelet_denoising: WaveletDenoisingTypes.SURESHRINK, + threshold: ThresholdTypes.HARD, + extension_type: WaveletExtensionTypes.SYMMETRIC, + noise_level: NoiseEstimationLevelTypes.FIRST_LEVEL + ) + print(data.prefix(10)) + } +} diff --git a/swift_package/examples/tests/downsampling/downsampling.swift b/swift_package/examples/tests/downsampling/downsampling.swift new file mode 100644 index 000000000..feeceddba --- /dev/null +++ b/swift_package/examples/tests/downsampling/downsampling.swift @@ -0,0 +1,12 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum DownsamplingExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + let data = try sample.firstEEGChannel() + let downsampled = try DataFilter.perform_downsampling(data: data, period: 4, operation: AggOperations.MEAN) + print(downsampled) + } +} diff --git a/swift_package/examples/tests/eeg_metrics/eeg_metrics.swift b/swift_package/examples/tests/eeg_metrics/eeg_metrics.swift new file mode 100644 index 000000000..bfbe2d8ec --- /dev/null +++ b/swift_package/examples/tests/eeg_metrics/eeg_metrics.swift @@ -0,0 +1,22 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum EEGMetricsExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read() + let bandPowers = try DataFilter.get_avg_band_powers( + data: sample.data, + channels: sample.eegChannels, + sampling_rate: sample.samplingRate, + apply_filter: true + ) + + let params = BrainFlowModelParams(metric: BrainFlowMetrics.MINDFULNESS, classifier: BrainFlowClassifiers.DEFAULT_CLASSIFIER) + let model = try MLModel(params: params) + try model.prepare() + let prediction = try model.predict(input_data: bandPowers.average) + try model.release() + print(prediction) + } +} diff --git a/swift_package/examples/tests/ica/ica.swift b/swift_package/examples/tests/ica/ica.swift new file mode 100644 index 000000000..92840e65f --- /dev/null +++ b/swift_package/examples/tests/ica/ica.swift @@ -0,0 +1,14 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum ICAExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 500) + let channels = Array(sample.eegChannels.prefix(4)) + let ica = try DataFilter.perform_ica(data: sample.data, num_components: 2, channels: channels) + + print("W: \(ica.w.count)x\(ica.w.first?.count ?? 0)") + print("S: \(ica.s.count)x\(ica.s.first?.count ?? 0)") + } +} diff --git a/swift_package/examples/tests/markers/markers.swift b/swift_package/examples/tests/markers/markers.swift new file mode 100644 index 000000000..4e614afd8 --- /dev/null +++ b/swift_package/examples/tests/markers/markers.swift @@ -0,0 +1,20 @@ +import BrainFlow +import Foundation + +@main +enum MarkersExample { + static func main() throws { + let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + try board.insert_marker(1.0) + Thread.sleep(forTimeInterval: 1.0) + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + let markerChannel = try BoardShim.get_marker_channel(board_id: BoardIds.SYNTHETIC_BOARD.rawValue) + print("Marker channel: \(markerChannel)") + print("Samples: \(data[markerChannel].count)") + } +} diff --git a/swift_package/examples/tests/read_write_file/read_write_file.swift b/swift_package/examples/tests/read_write_file/read_write_file.swift new file mode 100644 index 000000000..f4f7afa86 --- /dev/null +++ b/swift_package/examples/tests/read_write_file/read_write_file.swift @@ -0,0 +1,16 @@ +import BrainFlow +import BrainFlowExampleSupport +import Foundation + +@main +enum ReadWriteFileExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(seconds: 2.0) + + let fileName = NSTemporaryDirectory() + "/brainflow_swift.csv" + try DataFilter.write_file(data: sample.data, file_name: fileName, file_mode: "w") + let restored = try DataFilter.read_file(fileName) + print("Original: \(sample.data.count)x\(sample.data.first?.count ?? 0)") + print("Restored: \(restored.count)x\(restored.first?.count ?? 0)") + } +} diff --git a/swift_package/examples/tests/signal_filtering/signal_filtering.swift b/swift_package/examples/tests/signal_filtering/signal_filtering.swift new file mode 100644 index 000000000..1572cf3be --- /dev/null +++ b/swift_package/examples/tests/signal_filtering/signal_filtering.swift @@ -0,0 +1,13 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum SignalFilteringExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + var data = try sample.firstEEGChannel() + try DataFilter.perform_lowpass(data: &data, sampling_rate: sample.samplingRate, cutoff: 30.0, order: 4, filter_type: FilterTypes.BUTTERWORTH, ripple: 0.0) + try DataFilter.perform_highpass(data: &data, sampling_rate: sample.samplingRate, cutoff: 1.0, order: 4, filter_type: FilterTypes.BUTTERWORTH, ripple: 0.0) + print(data.prefix(10)) + } +} diff --git a/swift_package/examples/tests/support/SyntheticBoardData.swift b/swift_package/examples/tests/support/SyntheticBoardData.swift new file mode 100644 index 000000000..29d051feb --- /dev/null +++ b/swift_package/examples/tests/support/SyntheticBoardData.swift @@ -0,0 +1,42 @@ +import BrainFlow +import Foundation + +public struct SyntheticBoardData { + public let boardId: BoardIds + public let data: [[Double]] + public let eegChannels: [Int] + public let samplingRate: Int + + public func firstEEGChannel() throws -> [Double] { + guard let channel = eegChannels.first, channel >= 0, channel < data.count else { + throw BrainFlowError("No EEG channel found in synthetic board data", BrainFlowExitCodes.GENERAL_ERROR.rawValue) + } + return data[channel] + } +} + +public enum SyntheticBoardDataReader { + public static func read(seconds: TimeInterval = 5.0, maxSamples: Int? = nil) throws -> SyntheticBoardData { + let boardId = BoardIds.SYNTHETIC_BOARD + let board = try BoardShim(board_id: boardId) + try board.prepare_session() + + do { + try board.start_stream(buffer_size: 45_000) + Thread.sleep(forTimeInterval: seconds) + try board.stop_stream() + let data = try board.get_board_data(maxSamples) + try board.release_session() + + return SyntheticBoardData( + boardId: boardId, + data: data, + eegChannels: try BoardShim.get_eeg_channels(board_id: boardId), + samplingRate: try BoardShim.get_sampling_rate(board_id: boardId) + ) + } catch { + try? board.release_session() + throw error + } + } +} diff --git a/swift_package/examples/tests/transforms/transforms.swift b/swift_package/examples/tests/transforms/transforms.swift new file mode 100644 index 000000000..a14f32c9a --- /dev/null +++ b/swift_package/examples/tests/transforms/transforms.swift @@ -0,0 +1,14 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum TransformsExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + let data = try sample.firstEEGChannel() + let fft = try DataFilter.perform_fft(data: data, start_pos: 0, end_pos: data.count, window: WindowOperations.HANNING) + let restored = try DataFilter.perform_ifft(data: fft) + print("FFT bins: \(fft.count)") + print("Restored samples: \(restored.count)") + } +} diff --git a/tools/apple/build_xcframeworks.sh b/tools/apple/build_xcframeworks.sh new file mode 100755 index 000000000..462b3542c --- /dev/null +++ b/tools/apple/build_xcframeworks.sh @@ -0,0 +1,732 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BUILD_ROOT="${ROOT_DIR}/build_apple" +OUTPUT_DIR="${ROOT_DIR}/build/apple_xcframeworks" +CONFIGURATION="Release" +BUILD_FROM_SOURCE=1 +BUILD_MACOS=1 +BUILD_IOS=1 +BUILD_IOS_SIM=1 +MACOS_PREFIX="" +IOS_PREFIX="" +IOS_SIM_PREFIX="" +BRAINFLOW_VERSION="${BRAINFLOW_VERSION:-0.0.1}" +RELEASE_BASE_URL="${BRAINFLOW_APPLE_RELEASE_BASE_URL:-}" + +usage() { + cat <<'USAGE' +Usage: tools/apple/build_xcframeworks.sh [options] + +Build BrainFlow native Apple binaries and package them as framework-wrapped +XCFrameworks for standard iOS/macOS app embedding. + +Options: + --output Output directory. Default: build/apple_xcframeworks + --build-root CMake build root. Default: build_apple + --configuration CMake configuration. Default: Release + --skip-build Package existing install prefixes instead of building. + --skip-macos-build Reuse --macos-prefix and build/package the other slices. + --skip-ios-build Reuse --ios-prefix and build/package the other slices. + --skip-ios-sim-build Reuse --ios-sim-prefix and build/package the other slices. + --macos-prefix Existing macOS install prefix for --skip-build. + --ios-prefix Existing iOS device install prefix for --skip-build. + --ios-sim-prefix Existing iOS simulator install prefix for --skip-build. + -h, --help Show this help. + +Environment: + BRAINFLOW_APPLE_BUILD_BLE=ON|OFF Default: OFF + BRAINFLOW_APPLE_BUILD_BLUETOOTH=ON|OFF Default: OFF + BRAINFLOW_APPLE_BUILD_ONNX=ON|OFF Default: OFF + BRAINFLOW_APPLE_RELEASE_BASE_URL= Base URL for SwiftPM release zips. + +The script packages every produced dynamic library as an XCFramework. The +BrainFlow core libraries are required: + libBoardController.dylib, libDataHandler.dylib, libMLModule.dylib +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --output) + OUTPUT_DIR="$2" + shift 2 + ;; + --build-root) + BUILD_ROOT="$2" + shift 2 + ;; + --configuration) + CONFIGURATION="$2" + shift 2 + ;; + --skip-build) + BUILD_FROM_SOURCE=0 + shift + ;; + --skip-macos-build) + BUILD_MACOS=0 + shift + ;; + --skip-ios-build) + BUILD_IOS=0 + shift + ;; + --skip-ios-sim-build) + BUILD_IOS_SIM=0 + shift + ;; + --macos-prefix) + MACOS_PREFIX="$2" + shift 2 + ;; + --ios-prefix) + IOS_PREFIX="$2" + shift 2 + ;; + --ios-sim-prefix) + IOS_SIM_PREFIX="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "error: unknown option $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +absolute_path() { + case "$1" in + /*) echo "$1" ;; + *) echo "${ROOT_DIR}/$1" ;; + esac +} + +BUILD_ROOT="$(absolute_path "${BUILD_ROOT}")" +OUTPUT_DIR="$(absolute_path "${OUTPUT_DIR}")" + +command -v cmake >/dev/null || { echo "error: cmake is required" >&2; exit 1; } +command -v ninja >/dev/null || { echo "error: ninja is required" >&2; exit 1; } +command -v swift >/dev/null || { echo "error: swift is required" >&2; exit 1; } +command -v xcodebuild >/dev/null || { echo "error: xcodebuild is required" >&2; exit 1; } +command -v install_name_tool >/dev/null || { echo "error: install_name_tool is required" >&2; exit 1; } +command -v vtool >/dev/null || { echo "error: vtool is required" >&2; exit 1; } + +BUILD_BLE="${BRAINFLOW_APPLE_BUILD_BLE:-OFF}" +BUILD_BLUETOOTH="${BRAINFLOW_APPLE_BUILD_BLUETOOTH:-OFF}" +BUILD_ONNX="${BRAINFLOW_APPLE_BUILD_ONNX:-OFF}" + +MACOS_PREFIX="${MACOS_PREFIX:-${BUILD_ROOT}/installed_macos}" +IOS_PREFIX="${IOS_PREFIX:-${BUILD_ROOT}/installed_ios}" +IOS_SIM_PREFIX="${IOS_SIM_PREFIX:-${BUILD_ROOT}/installed_ios_sim}" +MACOS_PREFIX="$(absolute_path "${MACOS_PREFIX}")" +IOS_PREFIX="$(absolute_path "${IOS_PREFIX}")" +IOS_SIM_PREFIX="$(absolute_path "${IOS_SIM_PREFIX}")" +RELEASE_BASE_URL="${RELEASE_BASE_URL:-https://github.com/brainflow-dev/brainflow/releases/download/${BRAINFLOW_VERSION}}" +RELEASE_BASE_URL="${RELEASE_BASE_URL%/}" + +REQUIRED_LIBS=( + libBoardController.dylib + libDataHandler.dylib + libMLModule.dylib +) + +cmake_common_args=( + -G Ninja + "-DCMAKE_BUILD_TYPE=${CONFIGURATION}" + "-DBRAINFLOW_VERSION=${BRAINFLOW_VERSION}" + -DBUILD_TESTS=OFF + -DBUILD_SYNCHRONI_SDK=OFF + -DBUILD_PERIPHERY=OFF + "-DBUILD_BLE=${BUILD_BLE}" + "-DBUILD_BLUETOOTH=${BUILD_BLUETOOTH}" + "-DBUILD_ONNX=${BUILD_ONNX}" + -DBRAINFLOW_COPY_TO_PACKAGE_DIRS=OFF +) + +sanitize_bundle_version() { + local version="$1" + if [[ "${version}" =~ ^[0-9]+([.][0-9]+){0,2}$ ]]; then + echo "${version}" + else + echo "0.0.1" + fi +} + +FRAMEWORK_BUNDLE_SHORT_VERSION="$(sanitize_bundle_version "${BRAINFLOW_VERSION}")" +FRAMEWORK_BUNDLE_VERSION="$(sanitize_bundle_version "${BRAINFLOW_APPLE_FRAMEWORK_BUILD:-${BRAINFLOW_VERSION}}")" + +build_prefix() { + local build_dir="$1" + local install_prefix="$2" + shift 2 + + rm -rf "${install_prefix}" + cmake -S "${ROOT_DIR}" -B "${build_dir}" \ + "${cmake_common_args[@]}" \ + "-DCMAKE_INSTALL_PREFIX=${install_prefix}" \ + "$@" + ninja -C "${build_dir}" clean + ninja -C "${build_dir}" install +} + +if [[ "${BUILD_FROM_SOURCE}" -eq 0 ]]; then + BUILD_MACOS=0 + BUILD_IOS=0 + BUILD_IOS_SIM=0 +fi + +if [[ "${BUILD_MACOS}" -eq 1 ]]; then + build_prefix "${BUILD_ROOT}/macos" "${MACOS_PREFIX}" \ + -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=12.0 +fi + +if [[ "${BUILD_IOS}" -eq 1 ]]; then + build_prefix "${BUILD_ROOT}/ios" "${IOS_PREFIX}" \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_SYSROOT=iphoneos \ + -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \ + -DBRAINFLOW_APPLE_DYNAMIC_FRAMEWORKS=ON +fi + +if [[ "${BUILD_IOS_SIM}" -eq 1 ]]; then + build_prefix "${BUILD_ROOT}/ios-simulator" "${IOS_SIM_PREFIX}" \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_SYSROOT=iphonesimulator \ + -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \ + -DBRAINFLOW_APPLE_DYNAMIC_FRAMEWORKS=ON +fi + +for prefix in "${MACOS_PREFIX}" "${IOS_PREFIX}" "${IOS_SIM_PREFIX}"; do + if [[ ! -d "${prefix}/lib" ]]; then + echo "error: missing install prefix library directory: ${prefix}/lib" >&2 + exit 1 + fi +done + +for lib in "${REQUIRED_LIBS[@]}"; do + for prefix in "${MACOS_PREFIX}" "${IOS_PREFIX}" "${IOS_SIM_PREFIX}"; do + if [[ ! -f "${prefix}/lib/${lib}" ]]; then + echo "error: missing required BrainFlow library ${prefix}/lib/${lib}" >&2 + exit 1 + fi + done +done + +rm -rf "${OUTPUT_DIR}" +mkdir -p "${OUTPUT_DIR}/XCFrameworks" "${OUTPUT_DIR}/FrameworkSlices" "${OUTPUT_DIR}/BrainFlowSwiftBinaryPackage" + +framework_name_for_lib() { + local lib_name + lib_name="$(basename "$1")" + lib_name="${lib_name%.dylib}" + lib_name="${lib_name#lib}" + echo "${lib_name}" +} + +is_required_lib() { + local lib_name="$1" + local required + for required in "${REQUIRED_LIBS[@]}"; do + if [[ "${required}" == "${lib_name}" ]]; then + return 0 + fi + done + return 1 +} + +macho_supports_platform() { + local binary_path="$1" + local expected_platform="$2" + vtool -show-build "${binary_path}" 2>/dev/null | grep -q "platform ${expected_platform}" +} + +bundle_identifier_for_framework() { + local framework_name="$1" + local identifier_name + identifier_name="$(echo "${framework_name}" | tr '[:upper:]' '[:lower:]' | tr -c '[:alnum:]' '-')" + identifier_name="${identifier_name%-}" + echo "org.brainflow.${identifier_name}" +} + +write_info_plist() { + local plist_path="$1" + local framework_name="$2" + local platform_name="$3" + local bundle_identifier + bundle_identifier="$(bundle_identifier_for_framework "${framework_name}")" + + cat > "${plist_path}" < + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${framework_name} + CFBundleIdentifier + ${bundle_identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${framework_name} + CFBundlePackageType + FMWK + CFBundleShortVersionString + ${FRAMEWORK_BUNDLE_SHORT_VERSION} + CFBundleSupportedPlatforms + + ${platform_name} + + CFBundleVersion + ${FRAMEWORK_BUNDLE_VERSION} + + +PLIST +} + +write_module_map() { + local module_map_path="$1" + local framework_name="$2" + local umbrella_header="$3" + + if [[ ! "${framework_name}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then + return + fi + + cat > "${module_map_path}" < "${header_path}" <<'HEADER' +#pragma once +#include "board_controller.h" +#include "board_info_getter.h" +#include "brainflow_array.h" +#include "brainflow_constants.h" +#include "brainflow_exception.h" +#include "brainflow_input_params.h" +#include "shared_export.h" +HEADER + ;; + DataHandler) + cat > "${header_path}" <<'HEADER' +#pragma once +#include "brainflow_array.h" +#include "brainflow_constants.h" +#include "data_handler.h" +#include "shared_export.h" +HEADER + ;; + MLModule) + cat > "${header_path}" <<'HEADER' +#pragma once +#include "brainflow_constants.h" +#include "brainflow_model_params.h" +#include "ml_module.h" +#include "shared_export.h" +HEADER + ;; + *) + cat > "${header_path}" <
&2 + exit 1 + fi + cp "${source_header}" "${destination_dir}/" +} + +copy_public_headers_for_framework() { + local install_prefix="$1" + local framework_name="$2" + local destination_dir="$3" + local headers=() + local header + + case "${framework_name}" in + BoardController) + headers=( + board_controller.h + board_info_getter.h + brainflow_array.h + brainflow_constants.h + brainflow_exception.h + brainflow_input_params.h + shared_export.h + ) + ;; + DataHandler) + headers=( + brainflow_array.h + brainflow_constants.h + data_handler.h + shared_export.h + ) + ;; + MLModule) + headers=( + brainflow_constants.h + brainflow_model_params.h + ml_module.h + shared_export.h + ) + ;; + *) + return + ;; + esac + + for header in "${headers[@]}"; do + copy_public_header "${install_prefix}" "${header}" "${destination_dir}" + done +} + +create_framework_slice() { + local install_prefix="$1" + local lib_name="$2" + local framework_name="$3" + local slice_name="$4" + local platform_name="$5" + local slice_dir="${OUTPUT_DIR}/FrameworkSlices/${framework_name}/${slice_name}/${framework_name}.framework" + + rm -rf "${slice_dir}" + mkdir -p "${slice_dir}/Headers" "${slice_dir}/Modules" + + cp "${install_prefix}/lib/${lib_name}" "${slice_dir}/${framework_name}" + chmod 755 "${slice_dir}/${framework_name}" + install_name_tool -id "@rpath/${framework_name}.framework/${framework_name}" "${slice_dir}/${framework_name}" || true + + copy_public_headers_for_framework "${install_prefix}" "${framework_name}" "${slice_dir}/Headers" + write_umbrella_header "${slice_dir}/Headers/${framework_name}.h" "${framework_name}" + write_module_map "${slice_dir}/Modules/module.modulemap" "${framework_name}" "${framework_name}.h" + write_info_plist "${slice_dir}/Info.plist" "${framework_name}" "${platform_name}" +} + +all_libs="$( + { + find "${MACOS_PREFIX}/lib" "${IOS_PREFIX}/lib" "${IOS_SIM_PREFIX}/lib" -maxdepth 1 -type f -name '*.dylib' -print + } | xargs -n 1 basename | sort -u +)" + +while IFS= read -r lib_name; do + [[ -n "${lib_name}" ]] || continue + + framework_name="$(framework_name_for_lib "${lib_name}")" + args=() + + if [[ -f "${MACOS_PREFIX}/lib/${lib_name}" ]]; then + if macho_supports_platform "${MACOS_PREFIX}/lib/${lib_name}" MACOS; then + create_framework_slice "${MACOS_PREFIX}" "${lib_name}" "${framework_name}" macos MacOSX + args+=(-framework "${OUTPUT_DIR}/FrameworkSlices/${framework_name}/macos/${framework_name}.framework") + elif is_required_lib "${lib_name}"; then + echo "error: ${MACOS_PREFIX}/lib/${lib_name} is not a macOS Mach-O binary" >&2 + exit 1 + else + echo "warning: skipping non-macOS optional library ${MACOS_PREFIX}/lib/${lib_name}" >&2 + fi + fi + if [[ -f "${IOS_PREFIX}/lib/${lib_name}" ]]; then + if macho_supports_platform "${IOS_PREFIX}/lib/${lib_name}" IOS; then + create_framework_slice "${IOS_PREFIX}" "${lib_name}" "${framework_name}" ios iPhoneOS + args+=(-framework "${OUTPUT_DIR}/FrameworkSlices/${framework_name}/ios/${framework_name}.framework") + elif is_required_lib "${lib_name}"; then + echo "error: ${IOS_PREFIX}/lib/${lib_name} is not an iOS device Mach-O binary" >&2 + exit 1 + else + echo "warning: skipping non-iOS optional library ${IOS_PREFIX}/lib/${lib_name}" >&2 + fi + fi + if [[ -f "${IOS_SIM_PREFIX}/lib/${lib_name}" ]]; then + if macho_supports_platform "${IOS_SIM_PREFIX}/lib/${lib_name}" IOSSIMULATOR; then + create_framework_slice "${IOS_SIM_PREFIX}" "${lib_name}" "${framework_name}" ios-simulator iPhoneSimulator + args+=(-framework "${OUTPUT_DIR}/FrameworkSlices/${framework_name}/ios-simulator/${framework_name}.framework") + elif is_required_lib "${lib_name}"; then + echo "error: ${IOS_SIM_PREFIX}/lib/${lib_name} is not an iOS simulator Mach-O binary" >&2 + exit 1 + else + echo "warning: skipping non-iOS-simulator optional library ${IOS_SIM_PREFIX}/lib/${lib_name}" >&2 + fi + fi + + if [[ "${#args[@]}" -gt 0 ]]; then + xcodebuild -create-xcframework "${args[@]}" -output "${OUTPUT_DIR}/XCFrameworks/${framework_name}.xcframework" + fi +done <<< "${all_libs}" + +swift_binary_package="${OUTPUT_DIR}/BrainFlowSwiftBinaryPackage" +mkdir -p "${swift_binary_package}/Sources" +cp -R "${ROOT_DIR}/swift_package/Sources/BrainFlow" "${swift_binary_package}/Sources/BrainFlow" +mkdir -p "${swift_binary_package}/XCFrameworks" +cp -R "${OUTPUT_DIR}/XCFrameworks/"*.xcframework "${swift_binary_package}/XCFrameworks/" + +cat > "${swift_binary_package}/Package.swift" <<'PACKAGE' +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "BrainFlow", + platforms: [ + .macOS(.v12), + .iOS(.v15) + ], + products: [ + .library(name: "BrainFlow", targets: ["BrainFlow"]) + ], + targets: [ + .target( + name: "BrainFlow", + dependencies: [ + "BoardController", + "DataHandler", + "MLModule" + ] + ), + .binaryTarget(name: "BoardController", path: "XCFrameworks/BoardController.xcframework"), + .binaryTarget(name: "DataHandler", path: "XCFrameworks/DataHandler.xcframework"), + .binaryTarget(name: "MLModule", path: "XCFrameworks/MLModule.xcframework") + ] +) +PACKAGE + +cat > "${swift_binary_package}/README.md" <<'README' +# BrainFlow Swift Binary Package + +This package is generated by `tools/apple/build_xcframeworks.sh`. +It contains the BrainFlow Swift API and prebuilt Apple XCFrameworks for +`BoardController`, `DataHandler`, and `MLModule`. + +Add this package to an iOS or macOS app through Xcode or Swift Package Manager. +Xcode embeds and signs the binary frameworks during app builds. + +Do not edit generated framework contents by hand. Update BrainFlow source, rerun +the Apple artifact script, and publish the regenerated zip plus checksums and +manifest from the same build. +README + +swiftpm_artifact_dir="${OUTPUT_DIR}/SwiftPMArtifacts" +remote_swift_package="${OUTPUT_DIR}/BrainFlowSwiftPackageRemote" +rm -rf "${swiftpm_artifact_dir}" "${remote_swift_package}" +mkdir -p "${swiftpm_artifact_dir}" "${remote_swift_package}/Sources" +cp -R "${ROOT_DIR}/swift_package/Sources/BrainFlow" "${remote_swift_package}/Sources/BrainFlow" + +create_swiftpm_framework_zip() { + local framework_name="$1" + local xcframework_path="${OUTPUT_DIR}/XCFrameworks/${framework_name}.xcframework" + local zip_path="${swiftpm_artifact_dir}/${framework_name}.xcframework.zip" + + if [[ ! -d "${xcframework_path}" ]]; then + echo "error: missing SwiftPM XCFramework artifact ${xcframework_path}" >&2 + exit 1 + fi + + rm -f "${zip_path}" + ( + cd "${OUTPUT_DIR}/XCFrameworks" + /usr/bin/zip -qry "${zip_path}" "${framework_name}.xcframework" + ) + swift package compute-checksum "${zip_path}" +} + +board_controller_swiftpm_checksum="$(create_swiftpm_framework_zip BoardController)" +data_handler_swiftpm_checksum="$(create_swiftpm_framework_zip DataHandler)" +ml_module_swiftpm_checksum="$(create_swiftpm_framework_zip MLModule)" + +cat > "${OUTPUT_DIR}/swiftpm-checksums.txt" < "${OUTPUT_DIR}/swiftpm-checksums.json" < "${remote_swift_package}/Package.swift" < "${remote_swift_package}/README.md" </dev/null && git -C "${ROOT_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + source_revision="$(git -C "${ROOT_DIR}" rev-parse HEAD 2>/dev/null || echo unknown)" +fi + +json_escape() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + value="${value//$'\n'/ }" + printf '%s' "${value}" +} + +xcode_version="$(xcodebuild -version 2>/dev/null | tr '\n' ' ' | sed 's/[[:space:]]*$//' || true)" +cmake_version="$(cmake --version 2>/dev/null | head -n 1 | awk '{print $3}' || true)" +ninja_version="$(ninja --version 2>/dev/null || true)" + +( + cd "${OUTPUT_DIR}" + find XCFrameworks BrainFlowSwiftBinaryPackage BrainFlowSwiftPackageRemote SwiftPMArtifacts -type f -print | LC_ALL=C sort | while IFS= read -r artifact_file; do + shasum -a 256 "${artifact_file}" + done > checksums.sha256 + shasum -a 256 swiftpm-checksums.txt swiftpm-checksums.json >> checksums.sha256 +) + +cat > "${OUTPUT_DIR}/manifest.json" < BrainFlowAppleXCFrameworks.zip.sha256 +) + +rm -rf "${OUTPUT_DIR}/FrameworkSlices" + +echo "BrainFlow Apple XCFramework artifacts written to ${OUTPUT_DIR}" +echo "Swift binary package: ${swift_binary_package}" +echo "Archive: ${OUTPUT_DIR}/BrainFlowAppleXCFrameworks.zip" diff --git a/tools/apple/package_macos_demo_app.sh b/tools/apple/package_macos_demo_app.sh new file mode 100755 index 000000000..b2399f7f2 --- /dev/null +++ b/tools/apple/package_macos_demo_app.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +XCFRAMEWORKS_DIR="${BRAINFLOW_APPLE_XCFRAMEWORKS_DIR:-${ROOT_DIR}/build/apple_xcframeworks/XCFrameworks}" +OUTPUT_APP="${1:-${ROOT_DIR}/build/apple_xcframeworks/BrainFlowMacDemo.app}" +CONFIGURATION="${BRAINFLOW_MAC_DEMO_CONFIGURATION:-release}" + +command -v swift >/dev/null || { echo "error: swift is required" >&2; exit 1; } +command -v codesign >/dev/null || { echo "error: codesign is required" >&2; exit 1; } + +if [[ ! -d "${XCFRAMEWORKS_DIR}" ]]; then + echo "error: missing XCFramework directory: ${XCFRAMEWORKS_DIR}" >&2 + exit 1 +fi + +swift_configuration_flag=() +swift_build_dir="debug" +if [[ "${CONFIGURATION}" == "release" ]]; then + swift_configuration_flag=(-c release) + swift_build_dir="release" +fi + +( + cd "${ROOT_DIR}/swift_package" + swift build "${swift_configuration_flag[@]}" --product BrainFlowMacDemo +) + +executable="${ROOT_DIR}/swift_package/.build/${swift_build_dir}/BrainFlowMacDemo" +if [[ ! -x "${executable}" ]]; then + echo "error: missing built executable: ${executable}" >&2 + exit 1 +fi + +rm -rf "${OUTPUT_APP}" +mkdir -p "${OUTPUT_APP}/Contents/MacOS" "${OUTPUT_APP}/Contents/Frameworks" "${OUTPUT_APP}/Contents/Resources" +cp "${executable}" "${OUTPUT_APP}/Contents/MacOS/BrainFlowMacDemo" +cp "${ROOT_DIR}/swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy" "${OUTPUT_APP}/Contents/Resources/PrivacyInfo.xcprivacy" +cp "${ROOT_DIR}/swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements" "${OUTPUT_APP}/Contents/Resources/BrainFlowMacDemo.entitlements" + +cat > "${OUTPUT_APP}/Contents/Info.plist" <<'PLIST' + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + BrainFlowMacDemo + CFBundleIdentifier + org.brainflow.demo.macos + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + BrainFlowMacDemo + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 12.0 + NSPrincipalClass + NSApplication + + +PLIST + +select_macos_framework_slice() { + local framework_name="$1" + local xcframework="${XCFRAMEWORKS_DIR}/${framework_name}.xcframework" + if [[ ! -d "${xcframework}" ]]; then + echo "error: missing BrainFlow XCFramework ${xcframework}" >&2 + exit 1 + fi + find "${xcframework}" -path "*/${framework_name}.framework" -type d | grep '/macos-' | head -n 1 || true +} + +for framework_name in BoardController DataHandler MLModule; do + src_framework="$(select_macos_framework_slice "${framework_name}")" + if [[ -z "${src_framework}" ]]; then + echo "error: unable to select macOS slice for ${framework_name}" >&2 + exit 1 + fi + cp -R "${src_framework}" "${OUTPUT_APP}/Contents/Frameworks/${framework_name}.framework" + codesign --force --sign - --timestamp=none "${OUTPUT_APP}/Contents/Frameworks/${framework_name}.framework" +done + +codesign --force --sign - --timestamp=none "${OUTPUT_APP}" + +echo "BrainFlowMacDemo app bundle written to ${OUTPUT_APP}" diff --git a/tools/apple/regenerate_artifacts.sh b/tools/apple/regenerate_artifacts.sh new file mode 100755 index 000000000..e60766a41 --- /dev/null +++ b/tools/apple/regenerate_artifacts.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ARTIFACT_DIR="${BRAINFLOW_APPLE_ARTIFACT_DIR:-${ROOT_DIR}/build/apple_xcframeworks}" + +"${ROOT_DIR}/tools/apple/build_xcframeworks.sh" \ + --output "${ARTIFACT_DIR}" \ + "$@" + +"${ROOT_DIR}/tools/apple/verify_xcframeworks.sh" "${ARTIFACT_DIR}" + +echo "Regenerated Apple artifacts in ${ARTIFACT_DIR}" diff --git a/tools/apple/regenerate_lfs_artifacts.sh b/tools/apple/regenerate_lfs_artifacts.sh new file mode 100755 index 000000000..ad4df6ed9 --- /dev/null +++ b/tools/apple/regenerate_lfs_artifacts.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +exec "${ROOT_DIR}/tools/apple/regenerate_artifacts.sh" "$@" diff --git a/tools/apple/test_swift_binary_package.sh b/tools/apple/test_swift_binary_package.sh new file mode 100755 index 000000000..ac507a9b7 --- /dev/null +++ b/tools/apple/test_swift_binary_package.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ARTIFACT_DIR="${1:-${ROOT_DIR}/build/apple_xcframeworks}" +ARTIFACT_DIR="$(cd "${ARTIFACT_DIR}" && pwd)" +PACKAGE_DIR="${ARTIFACT_DIR}/BrainFlowSwiftBinaryPackage" + +command -v swift >/dev/null || { echo "error: swift is required" >&2; exit 1; } + +if [[ ! -f "${PACKAGE_DIR}/Package.swift" ]]; then + echo "error: missing generated Swift binary package: ${PACKAGE_DIR}" >&2 + exit 1 +fi + +smoke_dir="$(mktemp -d "${TMPDIR:-/tmp}/brainflow-binary-smoke.XXXXXX")" +trap 'rm -rf "${smoke_dir}"' EXIT + +mkdir -p "${smoke_dir}/Sources/BrainFlowBinarySmoke" +mkdir -p "${smoke_dir}/.cache" "${smoke_dir}/.swiftpm" "${smoke_dir}/ModuleCache" "${smoke_dir}/home" +export CLANG_MODULE_CACHE_PATH="${CLANG_MODULE_CACHE_PATH:-${smoke_dir}/ModuleCache}" +export HOME="${BRAINFLOW_SWIFT_BINARY_SMOKE_HOME:-${smoke_dir}/home}" +export XDG_CACHE_HOME="${XDG_CACHE_HOME:-${smoke_dir}/.cache}" +export SWIFTPM_HOME="${SWIFTPM_HOME:-${smoke_dir}/.swiftpm}" + +cat > "${smoke_dir}/Package.swift" < "${smoke_dir}/Sources/BrainFlowBinarySmoke/main.swift" <<'SWIFT' +import BrainFlow +import Foundation + +let board = try BoardShim(board_id: .SYNTHETIC_BOARD) +try board.prepare_session() +try board.start_stream(buffer_size: 45000) +Thread.sleep(forTimeInterval: 1.0) +try board.stop_stream() +let data = try board.get_board_data() +try board.release_session() + +let expectedRows = try BoardShim.get_num_rows(board_id: BoardIds.SYNTHETIC_BOARD.rawValue) +let samples = data.first?.count ?? 0 + +guard data.count == expectedRows, samples > 0 else { + fputs("Binary package smoke failed: rows=\(data.count) expected=\(expectedRows) samples=\(samples)\n", stderr) + exit(1) +} + +print("BrainFlow Swift binary package smoke passed: rows=\(data.count) samples=\(samples)") +SWIFT + +( + cd "${smoke_dir}" + swift run \ + --disable-sandbox \ + --disable-dependency-cache \ + --manifest-cache local \ + --cache-path "${smoke_dir}/.cache/swiftpm" \ + --config-path "${smoke_dir}/.swiftpm/config" \ + --security-path "${smoke_dir}/.swiftpm/security" \ + --scratch-path "${smoke_dir}/.build" \ + BrainFlowBinarySmoke +) diff --git a/tools/apple/verify_xcframeworks.sh b/tools/apple/verify_xcframeworks.sh new file mode 100755 index 000000000..4b85212c1 --- /dev/null +++ b/tools/apple/verify_xcframeworks.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ARTIFACT_DIR="${1:-${ROOT_DIR}/build/apple_xcframeworks}" +XCFRAMEWORK_DIR="${ARTIFACT_DIR}/XCFrameworks" + +required_frameworks=( + BoardController + DataHandler + MLModule +) + +if [[ ! -d "${XCFRAMEWORK_DIR}" ]]; then + echo "error: missing XCFramework directory: ${XCFRAMEWORK_DIR}" >&2 + exit 1 +fi + +command -v vtool >/dev/null || { echo "error: vtool is required" >&2; exit 1; } +command -v shasum >/dev/null || { echo "error: shasum is required" >&2; exit 1; } +command -v swift >/dev/null || { echo "error: swift is required" >&2; exit 1; } +command -v unzip >/dev/null || { echo "error: unzip is required" >&2; exit 1; } + +macho_supports_platform() { + local binary_path="$1" + local expected_platform="$2" + vtool -show-build "${binary_path}" 2>/dev/null | grep -q "platform ${expected_platform}" +} + +require_platform_slice() { + local path="$1" + local framework="$2" + local label="$3" + local expected_platform="$4" + local find_pattern="$5" + local found=0 + + while IFS= read -r binary; do + if macho_supports_platform "${binary}" "${expected_platform}"; then + found=1 + break + fi + done < <(find "${path}" -type f -path "${find_pattern}" -print) + + if [[ "${found}" -ne 1 ]]; then + echo "error: ${framework}.xcframework is missing a ${label} Mach-O slice" >&2 + exit 1 + fi +} + +for framework in "${required_frameworks[@]}"; do + path="${XCFRAMEWORK_DIR}/${framework}.xcframework" + if [[ ! -d "${path}" ]]; then + echo "error: missing required XCFramework: ${path}" >&2 + exit 1 + fi + + info="${path}/Info.plist" + available_libraries="$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries' "${info}" 2>/dev/null || true)" + for required_platform in macos ios; do + if ! grep -q "SupportedPlatform = ${required_platform}" <<< "${available_libraries}"; then + echo "error: ${framework}.xcframework is missing ${required_platform} support" >&2 + exit 1 + fi + done + + while IFS= read -r binary; do + [[ -n "${binary}" ]] || continue + file "${binary}" + if otool -D "${binary}" >/dev/null 2>&1; then + install_name="$(otool -D "${binary}" | tail -n 1)" + expected="@rpath/${framework}.framework/${framework}" + if [[ "${install_name}" != "${expected}" ]]; then + echo "error: ${binary} install name is ${install_name}, expected ${expected}" >&2 + exit 1 + fi + fi + done < <(find "${path}" -type f -path "*/${framework}.framework/${framework}" -print) + + require_platform_slice "${path}" "${framework}" "macOS" MACOS "*/macos-*/${framework}.framework/${framework}" + require_platform_slice "${path}" "${framework}" "iOS device" IOS "*/ios-arm64/${framework}.framework/${framework}" + require_platform_slice "${path}" "${framework}" "iOS simulator" IOSSIMULATOR "*/ios-*-simulator/${framework}.framework/${framework}" +done + +package_dir="${ARTIFACT_DIR}/BrainFlowSwiftBinaryPackage" +if [[ ! -f "${package_dir}/Package.swift" ]]; then + echo "error: missing generated Swift binary package: ${package_dir}" >&2 + exit 1 +fi + +swiftpm_artifact_dir="${ARTIFACT_DIR}/SwiftPMArtifacts" +swiftpm_checksum_file="${ARTIFACT_DIR}/swiftpm-checksums.txt" +remote_package_dir="${ARTIFACT_DIR}/BrainFlowSwiftPackageRemote" + +if [[ ! -d "${swiftpm_artifact_dir}" ]]; then + echo "error: missing SwiftPM artifact directory: ${swiftpm_artifact_dir}" >&2 + exit 1 +fi + +if [[ ! -f "${swiftpm_checksum_file}" ]]; then + echo "error: missing SwiftPM checksum file: ${swiftpm_checksum_file}" >&2 + exit 1 +fi + +if [[ ! -f "${ARTIFACT_DIR}/swiftpm-checksums.json" ]]; then + echo "error: missing SwiftPM checksum JSON: ${ARTIFACT_DIR}/swiftpm-checksums.json" >&2 + exit 1 +fi + +if [[ ! -f "${remote_package_dir}/Package.swift" ]]; then + echo "error: missing generated remote Swift package: ${remote_package_dir}" >&2 + exit 1 +fi + +require_swiftpm_artifact() { + local framework="$1" + local zip_path="${swiftpm_artifact_dir}/${framework}.xcframework.zip" + local artifact_path="SwiftPMArtifacts/${framework}.xcframework.zip" + local recorded_checksum + local computed_checksum + local entry_count=0 + + if [[ ! -f "${zip_path}" ]]; then + echo "error: missing SwiftPM XCFramework zip: ${zip_path}" >&2 + exit 1 + fi + + while IFS= read -r zip_entry; do + [[ -n "${zip_entry}" ]] || continue + entry_count=$((entry_count + 1)) + case "${zip_entry}" in + "${framework}.xcframework"|\ + "${framework}.xcframework/"|\ + "${framework}.xcframework/"*) ;; + *) + echo "error: ${zip_path} must contain ${framework}.xcframework at the archive root; found ${zip_entry}" >&2 + exit 1 + ;; + esac + done < <(unzip -Z1 "${zip_path}") + + if [[ "${entry_count}" -eq 0 ]]; then + echo "error: ${zip_path} is empty" >&2 + exit 1 + fi + + if ! unzip -Z1 "${zip_path}" | grep -qx "${framework}.xcframework/Info.plist"; then + echo "error: ${zip_path} does not contain ${framework}.xcframework/Info.plist" >&2 + exit 1 + fi + + recorded_checksum="$(awk -v artifact="${artifact_path}" '$2 == artifact { print $1 }' "${swiftpm_checksum_file}")" + if [[ -z "${recorded_checksum}" ]]; then + echo "error: missing SwiftPM checksum entry for ${artifact_path}" >&2 + exit 1 + fi + + computed_checksum="$(swift package compute-checksum "${zip_path}")" + if [[ "${computed_checksum}" != "${recorded_checksum}" ]]; then + echo "error: SwiftPM checksum mismatch for ${zip_path}: ${computed_checksum}, expected ${recorded_checksum}" >&2 + exit 1 + fi + + if ! grep -Fq "${framework}.xcframework.zip" "${remote_package_dir}/Package.swift"; then + echo "error: remote Swift package does not reference ${framework}.xcframework.zip" >&2 + exit 1 + fi + + if ! grep -Fq "${recorded_checksum}" "${remote_package_dir}/Package.swift"; then + echo "error: remote Swift package does not reference checksum for ${framework}" >&2 + exit 1 + fi +} + +for framework in "${required_frameworks[@]}"; do + require_swiftpm_artifact "${framework}" +done + +swiftpm_dump_tmp="$(mktemp -d "${TMPDIR:-/tmp}/brainflow-remote-package.XXXXXX")" +trap 'rm -rf "${swiftpm_dump_tmp}"' EXIT +( + cd "${remote_package_dir}" + export HOME="${swiftpm_dump_tmp}/home" + export XDG_CACHE_HOME="${swiftpm_dump_tmp}/cache" + export SWIFTPM_HOME="${swiftpm_dump_tmp}/swiftpm" + mkdir -p "${HOME}" "${XDG_CACHE_HOME}" "${SWIFTPM_HOME}" + swift package dump-package >/dev/null +) + +if [[ ! -f "${ARTIFACT_DIR}/BrainFlowAppleXCFrameworks.zip" ]]; then + echo "error: missing XCFramework archive: ${ARTIFACT_DIR}/BrainFlowAppleXCFrameworks.zip" >&2 + exit 1 +fi + +if [[ ! -f "${ARTIFACT_DIR}/checksums.sha256" ]]; then + echo "error: missing artifact checksums: ${ARTIFACT_DIR}/checksums.sha256" >&2 + exit 1 +fi + +if [[ ! -f "${ARTIFACT_DIR}/BrainFlowAppleXCFrameworks.zip.sha256" ]]; then + echo "error: missing archive checksum: ${ARTIFACT_DIR}/BrainFlowAppleXCFrameworks.zip.sha256" >&2 + exit 1 +fi + +( + cd "${ARTIFACT_DIR}" + shasum -a 256 -q -c checksums.sha256 + shasum -a 256 -q -c BrainFlowAppleXCFrameworks.zip.sha256 +) + +echo "BrainFlow Apple XCFramework verification passed: ${ARTIFACT_DIR}"