From 7fa2f20f3e4b67fe1844e3e9cd2d9a0a08e5b8dd Mon Sep 17 00:00:00 2001 From: tsvvladimir Date: Mon, 18 May 2026 09:56:30 +0200 Subject: [PATCH 1/6] Add Swift bindings and sample apps --- .github/workflows/run_unix.yml | 16 + .gitignore | 4 + docs/BuildBrainFlow.rst | 23 +- docs/UserAPI.rst | 2 +- docs/requirements.txt | 6 + .../BrainFlowiOSDemoApp.swift | 90 ++ samples/ios/BrainFlowiOSDemo/Info.plist | 16 + .../BrainFlowiOSDemo/PrivacyInfo.xcprivacy | 14 + samples/ios/BrainFlowiOSDemo/README.md | 13 + .../BrainFlowMacDemo.entitlements | 8 + samples/macos/BrainFlowMacDemo/Info.plist | 18 + .../BrainFlowMacDemo/PrivacyInfo.xcprivacy | 14 + samples/macos/BrainFlowMacDemo/README.md | 20 + swift_package/Docs/APIParity.md | 40 + swift_package/Docs/AppStoreReadiness.md | 39 + swift_package/Package.swift | 33 + swift_package/README.md | 49 ++ .../Sources/BrainFlow/BoardShim.swift | 648 ++++++++++++++ .../Sources/BrainFlow/BrainFlowEnums.swift | 236 ++++++ .../Sources/BrainFlow/BrainFlowError.swift | 59 ++ .../Sources/BrainFlow/BrainFlowNative.swift | 147 ++++ .../Sources/BrainFlow/BrainFlowParams.swift | 107 +++ .../Sources/BrainFlow/BrainFlowTypes.swift | 106 +++ .../Sources/BrainFlow/DataFilter.swift | 788 ++++++++++++++++++ swift_package/Sources/BrainFlow/MLModel.swift | 140 ++++ swift_package/Sources/BrainFlowCLI/main.swift | 30 + .../Sources/BrainFlowMacDemo/main.swift | 147 ++++ .../Tests/BrainFlowTests/BrainFlowTests.swift | 135 +++ .../tests/band_power/band_power.swift | 6 + .../brainflow_get_data.swift | 13 + .../examples/tests/denoising/denoising.swift | 13 + .../tests/downsampling/downsampling.swift | 5 + .../tests/eeg_metrics/eeg_metrics.swift | 21 + swift_package/examples/tests/ica/ica.swift | 15 + .../examples/tests/markers/markers.swift | 15 + .../read_write_file/read_write_file.swift | 16 + .../signal_filtering/signal_filtering.swift | 6 + .../tests/transforms/transforms.swift | 7 + 38 files changed, 3063 insertions(+), 2 deletions(-) create mode 100644 samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift create mode 100644 samples/ios/BrainFlowiOSDemo/Info.plist create mode 100644 samples/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy create mode 100644 samples/ios/BrainFlowiOSDemo/README.md create mode 100644 samples/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements create mode 100644 samples/macos/BrainFlowMacDemo/Info.plist create mode 100644 samples/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy create mode 100644 samples/macos/BrainFlowMacDemo/README.md create mode 100644 swift_package/Docs/APIParity.md create mode 100644 swift_package/Docs/AppStoreReadiness.md create mode 100644 swift_package/Package.swift create mode 100644 swift_package/README.md create mode 100644 swift_package/Sources/BrainFlow/BoardShim.swift create mode 100644 swift_package/Sources/BrainFlow/BrainFlowEnums.swift create mode 100644 swift_package/Sources/BrainFlow/BrainFlowError.swift create mode 100644 swift_package/Sources/BrainFlow/BrainFlowNative.swift create mode 100644 swift_package/Sources/BrainFlow/BrainFlowParams.swift create mode 100644 swift_package/Sources/BrainFlow/BrainFlowTypes.swift create mode 100644 swift_package/Sources/BrainFlow/DataFilter.swift create mode 100644 swift_package/Sources/BrainFlow/MLModel.swift create mode 100644 swift_package/Sources/BrainFlowCLI/main.swift create mode 100644 swift_package/Sources/BrainFlowMacDemo/main.swift create mode 100644 swift_package/Tests/BrainFlowTests/BrainFlowTests.swift create mode 100644 swift_package/examples/tests/band_power/band_power.swift create mode 100644 swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift create mode 100644 swift_package/examples/tests/denoising/denoising.swift create mode 100644 swift_package/examples/tests/downsampling/downsampling.swift create mode 100644 swift_package/examples/tests/eeg_metrics/eeg_metrics.swift create mode 100644 swift_package/examples/tests/ica/ica.swift create mode 100644 swift_package/examples/tests/markers/markers.swift create mode 100644 swift_package/examples/tests/read_write_file/read_write_file.swift create mode 100644 swift_package/examples/tests/signal_filtering/signal_filtering.swift create mode 100644 swift_package/examples/tests/transforms/transforms.swift diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index 13a2a8bfb..e0878c960 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -82,6 +82,22 @@ 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: Compile BrainFlow Ubuntu if: (matrix.os == 'ubuntu-latest') run: | diff --git a/.gitignore b/.gitignore index d12a18464..95f02aa70 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,10 @@ ipch/ *.opensdf *.sdf *.cachefile +.swiftpm/ +.build/ +xcuserdata/ +DerivedData/ *.VC.db *.VC.VC.opendb diff --git a/docs/BuildBrainFlow.rst b/docs/BuildBrainFlow.rst index 2832c68f3..92c49debd 100644 --- a/docs/BuildBrainFlow.rst +++ b/docs/BuildBrainFlow.rst @@ -152,7 +152,28 @@ 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 + +The Swift package 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`. + +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:`samples/ios`, :code:`samples/macos`, and :code:`swift_package/Docs/AppStoreReadiness.md`. iOS runtime support requires BrainFlow native libraries compiled and embedded for the target iOS architectures. Docker Image -------------- 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/docs/requirements.txt b/docs/requirements.txt index 30c5e76be..6a286aa65 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,9 @@ sphinx==4.0.2 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.5 breathe==4.33.1 brainflow pandas @@ -10,3 +15,4 @@ sphinxcontrib-matlabdomain==0.19.1 docutils<0.17 sphinx_rtd_theme==0.4.3 jinja2==3.1.2 +urllib3<2 diff --git a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift new file mode 100644 index 000000000..74bb936f1 --- /dev/null +++ b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift @@ -0,0 +1,90 @@ +import BrainFlow +import SwiftUI + +@main +struct BrainFlowiOSDemoApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} + +struct ContentView: View { + @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 + + var body: some View { + NavigationStack { + Form { + Section("Synthetic Board") { + LabeledContent("Status", value: status) + LabeledContent("Rows", value: "\(rowCount)") + LabeledContent("Samples", value: "\(sampleCount)") + } + + Section { + Button(isStreaming ? "Stop Stream" : "Start Stream") { + isStreaming ? stopStream() : startStream() + } + Button("Read Data") { + readData() + } + .disabled(isStreaming) + Button("Release Session") { + releaseSession() + } + } + } + .navigationTitle("BrainFlow Demo") + } + } + + private func startStream() { + do { + let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + self.board = board + status = "Streaming" + isStreaming = true + } catch { + status = "Start failed" + } + } + + private func stopStream() { + do { + try board?.stop_stream() + status = "Stopped" + isStreaming = false + } catch { + status = "Stop failed" + } + } + + private func readData() { + do { + let data = try board?.get_board_data() ?? [] + rowCount = data.count + sampleCount = data.first?.count ?? 0 + status = "Read complete" + } catch { + status = "Read failed" + } + } + + private func releaseSession() { + do { + try board?.release_session() + board = nil + isStreaming = false + status = "Released" + } catch { + status = "Release failed" + } + } +} diff --git a/samples/ios/BrainFlowiOSDemo/Info.plist b/samples/ios/BrainFlowiOSDemo/Info.plist new file mode 100644 index 000000000..0c6e2cb56 --- /dev/null +++ b/samples/ios/BrainFlowiOSDemo/Info.plist @@ -0,0 +1,16 @@ + + + + + CFBundleDisplayName + BrainFlow Demo + CFBundleIdentifier + org.brainflow.demo.ios + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + UILaunchScreen + + + diff --git a/samples/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy b/samples/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..196836a1f --- /dev/null +++ b/samples/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + + diff --git a/samples/ios/BrainFlowiOSDemo/README.md b/samples/ios/BrainFlowiOSDemo/README.md new file mode 100644 index 000000000..ab69c0f7d --- /dev/null +++ b/samples/ios/BrainFlowiOSDemo/README.md @@ -0,0 +1,13 @@ +# BrainFlow iOS Demo + +This sample is a minimal SwiftUI app that exercises the BrainFlow Swift package with the synthetic board, so it does not need external hardware for App Review or TestFlight smoke testing. + +To make a distributable app, create an iOS app target in Xcode, add `swift_package` as a local package dependency, add these source files, and embed signed BrainFlow native libraries or XCFrameworks that include the iOS slices you intend to ship. + +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/samples/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements b/samples/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements new file mode 100644 index 000000000..13cb114cf --- /dev/null +++ b/samples/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/samples/macos/BrainFlowMacDemo/Info.plist b/samples/macos/BrainFlowMacDemo/Info.plist new file mode 100644 index 000000000..3be9880f3 --- /dev/null +++ b/samples/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/samples/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy b/samples/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..196836a1f --- /dev/null +++ b/samples/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + + diff --git a/samples/macos/BrainFlowMacDemo/README.md b/samples/macos/BrainFlowMacDemo/README.md new file mode 100644 index 000000000..ebcc92537 --- /dev/null +++ b/samples/macos/BrainFlowMacDemo/README.md @@ -0,0 +1,20 @@ +# BrainFlow macOS Demo + +The buildable macOS SwiftUI demo target lives in `swift_package` as `BrainFlowMacDemo`. + +For Mac App Store distribution, use this folder's `Info.plist`, entitlements, and privacy manifest as starting assets in an Xcode app target. Embed the BrainFlow dynamic libraries or XCFrameworks 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 + +Local smoke test: + +```bash +cd swift_package +BRAINFLOW_LIB_DIR=../installed/lib swift run BrainFlowMacDemo +``` 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..670f46101 --- /dev/null +++ b/swift_package/Docs/AppStoreReadiness.md @@ -0,0 +1,39 @@ +# 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 native libraries or XCFrameworks in the app bundle and sign them with the app. +- 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. + +## iOS + +- Create an iOS app target in Xcode. +- Add `swift_package` as a local package dependency. +- Add the files from `samples/ios/BrainFlowiOSDemo`. +- Provide iOS-compatible BrainFlow native binaries. The current high-level Swift package compiles for iOS, but the app can only run BrainFlow calls when matching native libraries are embedded. +- Keep permissions minimal. The synthetic-board demo needs no Bluetooth, network, or file permissions. +- Test via TestFlight before App Store submission. + +## macOS + +- Use `swift_package` product `BrainFlowMacDemo` for local development, or create an Xcode app target for App Store archiving. +- Add the files from `samples/macos/BrainFlowMacDemo`. +- Enable App Sandbox. +- Embed and sign BrainFlow dylibs/XCFrameworks. +- Verify dynamic loading works inside the archived 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. +- iOS and macOS app targets launch, handle missing native libraries gracefully, and run the synthetic-board workflow when libraries 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/Package.swift b/swift_package/Package.swift new file mode 100644 index 000000000..29bc93eed --- /dev/null +++ b/swift_package/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +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"]) + ], + targets: [ + .target( + name: "BrainFlow" + ), + .executableTarget( + name: "BrainFlowCLI", + dependencies: ["BrainFlow"] + ), + .executableTarget( + name: "BrainFlowMacDemo", + dependencies: ["BrainFlow"] + ), + .testTarget( + name: "BrainFlowTests", + dependencies: ["BrainFlow"] + ) + ] +) diff --git a/swift_package/README.md b/swift_package/README.md new file mode 100644 index 000000000..6519ca2d0 --- /dev/null +++ b/swift_package/README.md @@ -0,0 +1,49 @@ +# 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 +``` + +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` + +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. +- `samples/ios/BrainFlowiOSDemo` contains iOS SwiftUI source and release-prep metadata for creating an Xcode app target. +- `samples/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..035c47036 --- /dev/null +++ b/swift_package/Sources/BrainFlow/BoardShim.swift @@ -0,0 +1,648 @@ +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 { + 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) + var responseLen: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.config_board(configPtr, &response, &responseLen, CInt(response.count), CInt(board_id), params), + "Error in config_board" + ) + } + return String(cString: response) + } + } + } + + public func config_board_with_bytes(_ bytes: [UInt8]) throws { + guard !bytes.isEmpty else { return } + try serialized_params.withCString { params in + try bytes.withUnsafeBufferPointer { buffer in + let raw = UnsafeRawPointer(buffer.baseAddress!).assumingMemoryBound(to: CChar.self) + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.config_board_with_bytes(raw, CInt(bytes.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]] { + 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..fa82063df --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowNative.swift @@ -0,0 +1,147 @@ +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 { + #if os(Linux) + return RTLD_NOW | RTLD_GLOBAL + #else + return RTLD_NOW | RTLD_GLOBAL + #endif + } + + 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)) + } + } + candidates.append(contentsOf: names) + return unique(candidates) + } + + 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: Result? + + init(names: [String]) { + self.names = names + } + + func load() throws -> NativeLibrary { + lock.lock() + defer { lock.unlock() } + if let storage { + return try storage.get() + } + let result = Result { try NativeLibrary(names: names) } + storage = result + return try result.get() + } +} 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..7f1bf4f82 --- /dev/null +++ b/swift_package/Sources/BrainFlow/DataFilter.swift @@ -0,0 +1,788 @@ +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 else { throw invalidArguments("Invalid CSP data") } + let nEpochs = data.count + let nChannels = firstEpoch.count + let nTimes = firstChannel.count + 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] { + 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] { + 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 % 2 == 0 else { throw invalidArguments("nfft must be even") } + 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 { + 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 { + guard !channels.isEmpty, !bands.isEmpty else { throw invalidArguments("Channels and bands must be non-empty") } + let cols = data[channels[0]].count + 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..= 1 else { throw invalidArguments("num_components must be positive") } + 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 + 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 { + 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 { + 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 { + 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") + } + } + 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..df8b4b68a --- /dev/null +++ b/swift_package/Sources/BrainFlowMacDemo/main.swift @@ -0,0 +1,147 @@ +import BrainFlow +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 + + 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)") + } + + HStack { + Button(isRunning ? "Stop" : "Start") { + isRunning ? stop() : start() + } + .keyboardShortcut(.defaultAction) + + Button("Read") { + read() + } + .disabled(isRunning) + + Button("Release") { + release() + } + } + } + .padding(24) + .frame(minWidth: 420, minHeight: 240) + .task { + guard autorun, !didAutorun else { return } + didAutorun = true + await runAutomatedDemo() + } + } + + 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 { + let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + self.board = board + status = "Streaming synthetic data" + isRunning = true + } catch { + status = "Start failed: \(error)" + } + } + + private func stop() { + do { + try board?.stop_stream() + isRunning = false + status = "Stopped" + } catch { + status = "Stop failed: \(error)" + } + } + + private func read() { + do { + let data = try board?.get_board_data() ?? [] + rows = data.count + cols = data.first?.count ?? 0 + status = "Read \(cols) samples" + } catch { + status = "Read failed: \(error)" + } + } + + private func release() { + do { + try board?.release_session() + board = nil + isRunning = false + status = "Released" + } catch { + status = "Release failed: \(error)" + } + } + + @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 + } +} diff --git a/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift b/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift new file mode 100644 index 000000000..0b2142a9f --- /dev/null +++ b/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift @@ -0,0 +1,135 @@ +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.") + } + } + + 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 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.. Date: Wed, 20 May 2026 01:07:56 +0200 Subject: [PATCH 2/6] Add Xcode iOS demo project --- .gitignore | 1 + .../project.pbxproj | 375 ++++++++++++++++++ .../xcschemes/BrainFlowiOSDemo.xcscheme | 79 ++++ .../BrainFlowiOSDemoApp.swift | 52 ++- samples/ios/BrainFlowiOSDemo/Info.plist | 12 +- samples/ios/BrainFlowiOSDemo/README.md | 35 +- 6 files changed, 545 insertions(+), 9 deletions(-) create mode 100644 samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj create mode 100644 samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme diff --git a/.gitignore b/.gitignore index 95f02aa70..ff0fed197 100644 --- a/.gitignore +++ b/.gitignore @@ -347,6 +347,7 @@ ASALocalRun/ .vscode/ installed* +build_ios_sim/ compiled/ python/flowcat.egg-info/ .Rproj.user diff --git a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj new file mode 100644 index 000000000..43fb05e65 --- /dev/null +++ b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj @@ -0,0 +1,375 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + A10000000000000000000020 /* BrainFlowiOSDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000011 /* BrainFlowiOSDemoApp.swift */; }; + A10000000000000000000021 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000013 /* PrivacyInfo.xcprivacy */; }; + A10000000000000000000022 /* BrainFlow in Frameworks */ = {isa = PBXBuildFile; productRef = A10000000000000000000051 /* BrainFlow */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A10000000000000000000010 /* BrainFlowiOSDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BrainFlowiOSDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A10000000000000000000011 /* BrainFlowiOSDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrainFlowiOSDemoApp.swift; sourceTree = ""; }; + A10000000000000000000012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A10000000000000000000013 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A10000000000000000000033 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A10000000000000000000022 /* BrainFlow in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A10000000000000000000002 = { + isa = PBXGroup; + children = ( + A10000000000000000000003 /* BrainFlowiOSDemo */, + A10000000000000000000004 /* Products */, + ); + sourceTree = ""; + }; + A10000000000000000000003 /* BrainFlowiOSDemo */ = { + isa = PBXGroup; + children = ( + A10000000000000000000011 /* BrainFlowiOSDemoApp.swift */, + A10000000000000000000012 /* Info.plist */, + A10000000000000000000013 /* PrivacyInfo.xcprivacy */, + ); + name = BrainFlowiOSDemo; + sourceTree = ""; + }; + A10000000000000000000004 /* Products */ = { + isa = PBXGroup; + children = ( + A10000000000000000000010 /* BrainFlowiOSDemo.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A10000000000000000000030 /* BrainFlowiOSDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = A10000000000000000000045 /* Build configuration list for PBXNativeTarget "BrainFlowiOSDemo" */; + buildPhases = ( + A10000000000000000000031 /* Sources */, + A10000000000000000000033 /* Frameworks */, + A10000000000000000000032 /* Resources */, + A10000000000000000000034 /* Embed BrainFlow Native Libraries */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = BrainFlowiOSDemo; + packageProductDependencies = ( + A10000000000000000000051 /* BrainFlow */, + ); + productName = BrainFlowiOSDemo; + productReference = A10000000000000000000010 /* BrainFlowiOSDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A10000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1650; + LastUpgradeCheck = 1650; + TargetAttributes = { + A10000000000000000000030 = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = A10000000000000000000042 /* Build configuration list for PBXProject "BrainFlowiOSDemo" */; + compatibilityVersion = "Xcode 15.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A10000000000000000000002; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../swift_package" */, + ); + preferredProjectObjectVersion = 60; + productRefGroup = A10000000000000000000004 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A10000000000000000000030 /* BrainFlowiOSDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A10000000000000000000032 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A10000000000000000000021 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + A10000000000000000000034 /* Embed BrainFlow Native Libraries */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Embed BrainFlow Native Libraries"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/libBoardController.dylib", + "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/libDataHandler.dylib", + "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/libMLModule.dylib", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -euo pipefail\nDEFAULT_LIB_DIR=\"${SRCROOT}/../../../installed_ios_sim/lib\"\nif [ \"${PLATFORM_NAME}\" = \"iphoneos\" ]; then\n DEFAULT_LIB_DIR=\"${SRCROOT}/../../../installed_ios/lib\"\nfi\nLIB_DIR=\"${BRAINFLOW_IOS_NATIVE_LIB_DIR:-${DEFAULT_LIB_DIR}}\"\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nmkdir -p \"$FRAMEWORKS_DIR\"\nfor lib in libBoardController.dylib libDataHandler.dylib libMLModule.dylib; do\n if [ ! -f \"$LIB_DIR/$lib\" ]; then\n echo \"error: missing BrainFlow native library $LIB_DIR/$lib\" >&2\n exit 1\n fi\n cp \"$LIB_DIR/$lib\" \"$FRAMEWORKS_DIR/$lib\"\n codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY:--}\" --timestamp=none \"$FRAMEWORKS_DIR/$lib\" || codesign --force --sign - --timestamp=none \"$FRAMEWORKS_DIR/$lib\"\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 "../../../swift_package" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../swift_package; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A10000000000000000000051 /* BrainFlow */ = { + isa = XCSwiftPackageProductDependency; + package = A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../swift_package" */; + productName = BrainFlow; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = A10000000000000000000001 /* Project object */; +} diff --git a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme new file mode 100644 index 000000000..2abd7e3dd --- /dev/null +++ b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift index 74bb936f1..9018a9881 100644 --- a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift +++ b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift @@ -16,14 +16,15 @@ struct ContentView: View { @State private var rowCount = 0 @State private var board: BoardShim? @State private var isStreaming = false + @State private var didRunAutomatedDemo = false var body: some View { - NavigationStack { + NavigationView { Form { Section("Synthetic Board") { - LabeledContent("Status", value: status) - LabeledContent("Rows", value: "\(rowCount)") - LabeledContent("Samples", value: "\(sampleCount)") + infoRow("Status", status) + infoRow("Rows", "\(rowCount)") + infoRow("Samples", "\(sampleCount)") } Section { @@ -41,6 +42,20 @@ struct ContentView: View { } .navigationTitle("BrainFlow Demo") } + .navigationViewStyle(.stack) + .task { + await runAutomatedDemoIfRequested() + } + } + + private func infoRow(_ title: String, _ value: String) -> some View { + HStack { + Text(title) + Spacer() + Text(value) + .foregroundColor(.secondary) + .multilineTextAlignment(.trailing) + } } private func startStream() { @@ -87,4 +102,33 @@ struct ContentView: View { status = "Release failed" } } + + 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 + 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)") + } + } } diff --git a/samples/ios/BrainFlowiOSDemo/Info.plist b/samples/ios/BrainFlowiOSDemo/Info.plist index 0c6e2cb56..143eaf855 100644 --- a/samples/ios/BrainFlowiOSDemo/Info.plist +++ b/samples/ios/BrainFlowiOSDemo/Info.plist @@ -4,12 +4,18 @@ CFBundleDisplayName BrainFlow Demo + CFBundleExecutable + $(EXECUTABLE_NAME) CFBundleIdentifier - org.brainflow.demo.ios + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) UILaunchScreen diff --git a/samples/ios/BrainFlowiOSDemo/README.md b/samples/ios/BrainFlowiOSDemo/README.md index ab69c0f7d..46d930e2c 100644 --- a/samples/ios/BrainFlowiOSDemo/README.md +++ b/samples/ios/BrainFlowiOSDemo/README.md @@ -1,8 +1,39 @@ # BrainFlow iOS Demo -This sample is a minimal SwiftUI app that exercises the BrainFlow Swift package with the synthetic board, so it does not need external hardware for App Review or TestFlight smoke testing. +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. -To make a distributable app, create an iOS app target in Xcode, add `swift_package` as a local package dependency, add these source files, and embed signed BrainFlow native libraries or XCFrameworks that include the iOS slices you intend to ship. +## Run In Simulator + +From the repository root, build simulator native libraries first: + +```bash +cmake -S . -B build_ios_sim -G Ninja \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_SYSROOT=iphonesimulator \ + -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DCMAKE_INSTALL_PREFIX=installed_ios_sim \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_BLUETOOTH=OFF \ + -DBUILD_BLE=OFF \ + -DBUILD_ONNX=OFF \ + -DBUILD_TESTS=OFF \ + -DBUILD_SYNCHRONI_SDK=OFF +ninja -C build_ios_sim install +``` + +Then open `BrainFlowiOSDemo.xcodeproj` in Xcode, select an iPhone simulator, and run the `BrainFlowiOSDemo` scheme. The app target links the local Swift package at `../../../swift_package` and embeds native libraries from `../../../installed_ios_sim/lib` by default. + +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. + +## App Store Preparation + +The simulator build is not enough for App Store distribution. For an iPhone archive, build signed `iphoneos` native libraries into `installed_ios/lib` or set `BRAINFLOW_IOS_NATIVE_LIB_DIR` to a directory containing the device slices for: + +- `libBoardController.dylib` +- `libDataHandler.dylib` +- `libMLModule.dylib` + +Before upload, also replace the placeholders below. App Store placeholders to replace before upload: From 52034224bf50c8479d0149fe8e26fb184154a59b Mon Sep 17 00:00:00 2001 From: tsvvladimir Date: Wed, 20 May 2026 09:59:04 +0200 Subject: [PATCH 3/6] Address Swift binding review feedback --- .github/workflows/run_unix.yml | 18 ++ docs/BuildBrainFlow.rst | 5 +- docs/Examples.rst | 2 + docs/requirements.txt | 6 - .../BrainFlowiOSDemoApp.swift | 134 --------- swift_package/Docs/AppStoreReadiness.md | 10 +- swift_package/Package.swift | 28 +- swift_package/README.md | 7 +- .../project.pbxproj | 10 +- .../xcschemes/BrainFlowiOSDemo.xcscheme | 0 .../BrainFlowiOSDemoApp.swift | 256 ++++++++++++++++++ .../apps}/ios/BrainFlowiOSDemo/Info.plist | 2 + .../BrainFlowiOSDemo/PrivacyInfo.xcprivacy | 0 .../apps}/ios/BrainFlowiOSDemo/README.md | 6 +- .../BrainFlowMacDemo.entitlements | 0 .../apps}/macos/BrainFlowMacDemo/Info.plist | 0 .../BrainFlowMacDemo/PrivacyInfo.xcprivacy | 0 .../apps}/macos/BrainFlowMacDemo/README.md | 0 .../tests/band_power/band_power.swift | 21 +- .../brainflow_get_data.swift | 23 +- .../examples/tests/denoising/denoising.swift | 29 +- .../tests/downsampling/downsampling.swift | 13 +- .../tests/eeg_metrics/eeg_metrics.swift | 37 +-- swift_package/examples/tests/ica/ica.swift | 23 +- .../examples/tests/markers/markers.swift | 27 +- .../read_write_file/read_write_file.swift | 24 +- .../signal_filtering/signal_filtering.swift | 15 +- .../tests/support/SyntheticBoardData.swift | 42 +++ .../tests/transforms/transforms.swift | 17 +- 29 files changed, 507 insertions(+), 248 deletions(-) delete mode 100644 samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift rename {samples => swift_package/examples/apps}/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj (93%) rename {samples => swift_package/examples/apps}/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme (100%) create mode 100644 swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift rename {samples => swift_package/examples/apps}/ios/BrainFlowiOSDemo/Info.plist (85%) rename {samples => swift_package/examples/apps}/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy (100%) rename {samples => swift_package/examples/apps}/ios/BrainFlowiOSDemo/README.md (82%) rename {samples => swift_package/examples/apps}/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements (100%) rename {samples => swift_package/examples/apps}/macos/BrainFlowMacDemo/Info.plist (100%) rename {samples => swift_package/examples/apps}/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy (100%) rename {samples => swift_package/examples/apps}/macos/BrainFlowMacDemo/README.md (100%) create mode 100644 swift_package/examples/tests/support/SyntheticBoardData.swift diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index e0878c960..c4a464014 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -98,6 +98,24 @@ jobs: 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: Compile BrainFlow Ubuntu if: (matrix.os == 'ubuntu-latest') run: | diff --git a/docs/BuildBrainFlow.rst b/docs/BuildBrainFlow.rst index 92c49debd..0e8eae9f6 100644 --- a/docs/BuildBrainFlow.rst +++ b/docs/BuildBrainFlow.rst @@ -163,8 +163,9 @@ Local build example: 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 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`. +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`. The macOS demo can be built with: @@ -173,7 +174,7 @@ The macOS demo can be built with: 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:`samples/ios`, :code:`samples/macos`, and :code:`swift_package/Docs/AppStoreReadiness.md`. iOS runtime support requires BrainFlow native libraries compiled and embedded for the target iOS architectures. +iOS and Mac App Store sample source and release-preparation notes are available in :code:`swift_package/examples/apps` and :code:`swift_package/Docs/AppStoreReadiness.md`. iOS runtime support requires BrainFlow native libraries compiled for the target iOS architectures and embedded as signed app-bundle libraries or XCFrameworks. 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/requirements.txt b/docs/requirements.txt index 6a286aa65..30c5e76be 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,9 +1,4 @@ sphinx==4.0.2 -sphinxcontrib-applehelp==1.0.2 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==1.0.3 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.5 breathe==4.33.1 brainflow pandas @@ -15,4 +10,3 @@ sphinxcontrib-matlabdomain==0.19.1 docutils<0.17 sphinx_rtd_theme==0.4.3 jinja2==3.1.2 -urllib3<2 diff --git a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift b/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift deleted file mode 100644 index 9018a9881..000000000 --- a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift +++ /dev/null @@ -1,134 +0,0 @@ -import BrainFlow -import SwiftUI - -@main -struct BrainFlowiOSDemoApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} - -struct ContentView: View { - @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 - - var body: some View { - NavigationView { - Form { - Section("Synthetic Board") { - infoRow("Status", status) - infoRow("Rows", "\(rowCount)") - infoRow("Samples", "\(sampleCount)") - } - - Section { - Button(isStreaming ? "Stop Stream" : "Start Stream") { - isStreaming ? stopStream() : startStream() - } - Button("Read Data") { - readData() - } - .disabled(isStreaming) - Button("Release Session") { - releaseSession() - } - } - } - .navigationTitle("BrainFlow Demo") - } - .navigationViewStyle(.stack) - .task { - await runAutomatedDemoIfRequested() - } - } - - private func infoRow(_ title: String, _ value: String) -> some View { - HStack { - Text(title) - Spacer() - Text(value) - .foregroundColor(.secondary) - .multilineTextAlignment(.trailing) - } - } - - private func startStream() { - do { - let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) - try board.prepare_session() - try board.start_stream(buffer_size: 45000) - self.board = board - status = "Streaming" - isStreaming = true - } catch { - status = "Start failed" - } - } - - private func stopStream() { - do { - try board?.stop_stream() - status = "Stopped" - isStreaming = false - } catch { - status = "Stop failed" - } - } - - private func readData() { - do { - let data = try board?.get_board_data() ?? [] - rowCount = data.count - sampleCount = data.first?.count ?? 0 - status = "Read complete" - } catch { - status = "Read failed" - } - } - - private func releaseSession() { - do { - try board?.release_session() - board = nil - isStreaming = false - status = "Released" - } catch { - status = "Release failed" - } - } - - 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 - 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)") - } - } -} diff --git a/swift_package/Docs/AppStoreReadiness.md b/swift_package/Docs/AppStoreReadiness.md index 670f46101..d03c3828e 100644 --- a/swift_package/Docs/AppStoreReadiness.md +++ b/swift_package/Docs/AppStoreReadiness.md @@ -14,17 +14,17 @@ This checklist is intentionally separate from the sample source because final Ap ## iOS -- Create an iOS app target in Xcode. -- Add `swift_package` as a local package dependency. -- Add the files from `samples/ios/BrainFlowiOSDemo`. +- Use `examples/apps/ios/BrainFlowiOSDemo` as the Xcode app project. +- Keep `swift_package` as the local package dependency. - Provide iOS-compatible BrainFlow native binaries. The current high-level Swift package compiles for iOS, but the app can only run BrainFlow calls when matching native libraries are embedded. -- Keep permissions minimal. The synthetic-board demo needs no Bluetooth, network, or file permissions. +- 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 create an Xcode app target for App Store archiving. -- Add the files from `samples/macos/BrainFlowMacDemo`. +- Add the files from `examples/apps/macos/BrainFlowMacDemo`. - Enable App Sandbox. - Embed and sign BrainFlow dylibs/XCFrameworks. - Verify dynamic loading works inside the archived app bundle, not only with `BRAINFLOW_LIB_DIR`. diff --git a/swift_package/Package.swift b/swift_package/Package.swift index 29bc93eed..142926dd2 100644 --- a/swift_package/Package.swift +++ b/swift_package/Package.swift @@ -2,6 +2,19 @@ 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: [ @@ -12,11 +25,16 @@ let package = Package( .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"] @@ -29,5 +47,11 @@ let package = Package( 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 index 6519ca2d0..49cd65d7e 100644 --- a/swift_package/README.md +++ b/swift_package/README.md @@ -9,9 +9,10 @@ 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 loader searches `BRAINFLOW_LIB_DIR`, `DYLD_LIBRARY_PATH`, `LD_LIBRARY_PATH`, `installed/lib`, app bundle resources, and the current directory for: +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` @@ -45,5 +46,5 @@ try DataFilter.perform_lowpass( ## Apps - `swift run BrainFlowMacDemo` builds a simple macOS SwiftUI demo against the synthetic board. -- `samples/ios/BrainFlowiOSDemo` contains iOS SwiftUI source and release-prep metadata for creating an Xcode app target. -- `samples/macos/BrainFlowMacDemo` contains Mac App Store release-prep metadata for an Xcode app bundle. +- `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/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj similarity index 93% rename from samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj rename to swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj index 43fb05e65..5e9416ee2 100644 --- a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj @@ -107,7 +107,7 @@ mainGroup = A10000000000000000000002; minimizedProjectReferenceProxies = 1; packageReferences = ( - A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../swift_package" */, + A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../.." */, ); preferredProjectObjectVersion = 60; productRefGroup = A10000000000000000000004 /* Products */; @@ -151,7 +151,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -euo pipefail\nDEFAULT_LIB_DIR=\"${SRCROOT}/../../../installed_ios_sim/lib\"\nif [ \"${PLATFORM_NAME}\" = \"iphoneos\" ]; then\n DEFAULT_LIB_DIR=\"${SRCROOT}/../../../installed_ios/lib\"\nfi\nLIB_DIR=\"${BRAINFLOW_IOS_NATIVE_LIB_DIR:-${DEFAULT_LIB_DIR}}\"\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nmkdir -p \"$FRAMEWORKS_DIR\"\nfor lib in libBoardController.dylib libDataHandler.dylib libMLModule.dylib; do\n if [ ! -f \"$LIB_DIR/$lib\" ]; then\n echo \"error: missing BrainFlow native library $LIB_DIR/$lib\" >&2\n exit 1\n fi\n cp \"$LIB_DIR/$lib\" \"$FRAMEWORKS_DIR/$lib\"\n codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY:--}\" --timestamp=none \"$FRAMEWORKS_DIR/$lib\" || codesign --force --sign - --timestamp=none \"$FRAMEWORKS_DIR/$lib\"\ndone\n"; + shellScript = "set -euo pipefail\nDEFAULT_LIB_DIR=\"${SRCROOT}/../../../../../installed_ios_sim/lib\"\nif [ \"${PLATFORM_NAME}\" = \"iphoneos\" ]; then\n DEFAULT_LIB_DIR=\"${SRCROOT}/../../../../../installed_ios/lib\"\nfi\nLIB_DIR=\"${BRAINFLOW_IOS_NATIVE_LIB_DIR:-${DEFAULT_LIB_DIR}}\"\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nmkdir -p \"$FRAMEWORKS_DIR\"\nfor lib in libBoardController.dylib libDataHandler.dylib libMLModule.dylib; do\n if [ ! -f \"$LIB_DIR/$lib\" ]; then\n echo \"error: missing BrainFlow native library $LIB_DIR/$lib\" >&2\n exit 1\n fi\n cp \"$LIB_DIR/$lib\" \"$FRAMEWORKS_DIR/$lib\"\n codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY:--}\" --timestamp=none \"$FRAMEWORKS_DIR/$lib\" || codesign --force --sign - --timestamp=none \"$FRAMEWORKS_DIR/$lib\"\ndone\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -357,16 +357,16 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../swift_package" */ = { + A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../.." */ = { isa = XCLocalSwiftPackageReference; - relativePath = ../../../swift_package; + relativePath = ../../../..; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ A10000000000000000000051 /* BrainFlow */ = { isa = XCSwiftPackageProductDependency; - package = A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../swift_package" */; + package = A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../.." */; productName = BrainFlow; }; /* End XCSwiftPackageProductDependency section */ diff --git a/samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme similarity index 100% rename from samples/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme rename to swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme 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..45e535cfa --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift @@ -0,0 +1,256 @@ +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]]() + + 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() + } + } + + private func infoRow(_ title: String, _ value: String) -> some View { + HStack { + Text(title) + Spacer() + Text(value) + .foregroundColor(.secondary) + .multilineTextAlignment(.trailing) + } + } + + private func startStream() { + do { + 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))" + isStreaming = true + } catch { + status = "Start failed: \(error)" + } + } + + private func stopStream() { + do { + 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 { + try board?.release_session() + board = nil + isStreaming = false + status = "Released" + } catch { + status = "Release failed: \(error)" + } + } + + private func updateDisplay(with data: [[Double]], boardId: Int) { + rowCount = data.count + 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) + } + } + } + } + + 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/samples/ios/BrainFlowiOSDemo/Info.plist b/swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist similarity index 85% rename from samples/ios/BrainFlowiOSDemo/Info.plist rename to swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist index 143eaf855..30e89aafd 100644 --- a/samples/ios/BrainFlowiOSDemo/Info.plist +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist @@ -16,6 +16,8 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + NSBluetoothAlwaysUsageDescription + BrainFlow uses Bluetooth to connect to supported Muse boards. UILaunchScreen diff --git a/samples/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy b/swift_package/examples/apps/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy similarity index 100% rename from samples/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy rename to swift_package/examples/apps/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy diff --git a/samples/ios/BrainFlowiOSDemo/README.md b/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md similarity index 82% rename from samples/ios/BrainFlowiOSDemo/README.md rename to swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md index 46d930e2c..3ae60c676 100644 --- a/samples/ios/BrainFlowiOSDemo/README.md +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md @@ -21,9 +21,9 @@ cmake -S . -B build_ios_sim -G Ninja \ ninja -C build_ios_sim install ``` -Then open `BrainFlowiOSDemo.xcodeproj` in Xcode, select an iPhone simulator, and run the `BrainFlowiOSDemo` scheme. The app target links the local Swift package at `../../../swift_package` and embeds native libraries from `../../../installed_ios_sim/lib` by default. +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 libraries from `../../../../../installed_ios_sim/lib` by default. -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. +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 @@ -33,6 +33,8 @@ The simulator build is not enough for App Store distribution. For an iPhone arch - `libDataHandler.dylib` - `libMLModule.dylib` +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: diff --git a/samples/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements b/swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements similarity index 100% rename from samples/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements rename to swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements diff --git a/samples/macos/BrainFlowMacDemo/Info.plist b/swift_package/examples/apps/macos/BrainFlowMacDemo/Info.plist similarity index 100% rename from samples/macos/BrainFlowMacDemo/Info.plist rename to swift_package/examples/apps/macos/BrainFlowMacDemo/Info.plist diff --git a/samples/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy b/swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy similarity index 100% rename from samples/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy rename to swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy diff --git a/samples/macos/BrainFlowMacDemo/README.md b/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md similarity index 100% rename from samples/macos/BrainFlowMacDemo/README.md rename to swift_package/examples/apps/macos/BrainFlowMacDemo/README.md diff --git a/swift_package/examples/tests/band_power/band_power.swift b/swift_package/examples/tests/band_power/band_power.swift index 883c856f3..7bbc8ec73 100644 --- a/swift_package/examples/tests/band_power/band_power.swift +++ b/swift_package/examples/tests/band_power/band_power.swift @@ -1,6 +1,19 @@ import BrainFlow +import BrainFlowExampleSupport -let data = Array(0..<256).map { sin(Double($0) / 10.0) } -let psd = try DataFilter.get_psd(data: data, start_pos: 0, end_pos: data.count, sampling_rate: 250, 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)") +@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 index abdf591f0..5f28a1d36 100644 --- a/swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift +++ b/swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift @@ -1,13 +1,18 @@ import BrainFlow import Foundation -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() +@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)") + 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 index 2ba9d72d0..a4488b966 100644 --- a/swift_package/examples/tests/denoising/denoising.swift +++ b/swift_package/examples/tests/denoising/denoising.swift @@ -1,13 +1,20 @@ import BrainFlow +import BrainFlowExampleSupport -var data = Array(0..<256).map { sin(Double($0) / 10.0) } -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)) +@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 index 41fa5f332..feeceddba 100644 --- a/swift_package/examples/tests/downsampling/downsampling.swift +++ b/swift_package/examples/tests/downsampling/downsampling.swift @@ -1,5 +1,12 @@ import BrainFlow +import BrainFlowExampleSupport -let data = Array(0..<256).map(Double.init) -let downsampled = try DataFilter.perform_downsampling(data: data, period: 4, operation: AggOperations.MEAN) -print(downsampled) +@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 index 9c466e638..bfbe2d8ec 100644 --- a/swift_package/examples/tests/eeg_metrics/eeg_metrics.swift +++ b/swift_package/examples/tests/eeg_metrics/eeg_metrics.swift @@ -1,21 +1,22 @@ import BrainFlow -import Foundation +import BrainFlowExampleSupport -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() +@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 channels = try BoardShim.get_eeg_channels(board_id: BoardIds.SYNTHETIC_BOARD.rawValue) -let samplingRate = try BoardShim.get_sampling_rate(board_id: BoardIds.SYNTHETIC_BOARD.rawValue) -let bandPowers = try DataFilter.get_avg_band_powers(data: data, channels: channels, sampling_rate: samplingRate, apply_filter: true) - -var 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) + 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 index 803132c50..92840e65f 100644 --- a/swift_package/examples/tests/ica/ica.swift +++ b/swift_package/examples/tests/ica/ica.swift @@ -1,15 +1,14 @@ import BrainFlow -import Foundation +import BrainFlowExampleSupport -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(500) -try board.release_session() +@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) -let channels = Array(try BoardShim.get_eeg_channels(board_id: BoardIds.SYNTHETIC_BOARD.rawValue).prefix(4)) -let ica = try DataFilter.perform_ica(data: 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)") + 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 index 0e23ca7ca..4e614afd8 100644 --- a/swift_package/examples/tests/markers/markers.swift +++ b/swift_package/examples/tests/markers/markers.swift @@ -1,15 +1,20 @@ import BrainFlow import Foundation -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() +@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)") + 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 index 1e57fd320..f4f7afa86 100644 --- a/swift_package/examples/tests/read_write_file/read_write_file.swift +++ b/swift_package/examples/tests/read_write_file/read_write_file.swift @@ -1,16 +1,16 @@ import BrainFlow +import BrainFlowExampleSupport import Foundation -let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) -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() +@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: data, file_name: fileName, file_mode: "w") -let restored = try DataFilter.read_file(fileName) -print("Original: \(data.count)x\(data.first?.count ?? 0)") -print("Restored: \(restored.count)x\(restored.first?.count ?? 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 index c9826e6ae..1572cf3be 100644 --- a/swift_package/examples/tests/signal_filtering/signal_filtering.swift +++ b/swift_package/examples/tests/signal_filtering/signal_filtering.swift @@ -1,6 +1,13 @@ import BrainFlow +import BrainFlowExampleSupport -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: FilterTypes.BUTTERWORTH, ripple: 0.0) -try DataFilter.perform_highpass(data: &data, sampling_rate: 250, cutoff: 1.0, order: 4, filter_type: FilterTypes.BUTTERWORTH, ripple: 0.0) -print(data.prefix(10)) +@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 index f68785df8..a14f32c9a 100644 --- a/swift_package/examples/tests/transforms/transforms.swift +++ b/swift_package/examples/tests/transforms/transforms.swift @@ -1,7 +1,14 @@ import BrainFlow +import BrainFlowExampleSupport -let data = Array(0..<256).map { sin(Double($0) / 10.0) } -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)") +@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)") + } +} From a5ba117ecd3fe132b3a7e5cd19c83f1a6ad97de5 Mon Sep 17 00:00:00 2001 From: tsvvladimir Date: Wed, 20 May 2026 21:01:49 +0200 Subject: [PATCH 4/6] Harden Swift binding validation --- .../Sources/BrainFlow/BoardShim.swift | 33 ++++++++----- .../Sources/BrainFlow/BrainFlowNative.swift | 14 ++---- .../Sources/BrainFlow/DataFilter.swift | 36 ++++++++++++-- .../Tests/BrainFlowTests/BrainFlowTests.swift | 47 +++++++++++++++++++ 4 files changed, 104 insertions(+), 26 deletions(-) diff --git a/swift_package/Sources/BrainFlow/BoardShim.swift b/swift_package/Sources/BrainFlow/BoardShim.swift index 035c47036..4aa5d270c 100644 --- a/swift_package/Sources/BrainFlow/BoardShim.swift +++ b/swift_package/Sources/BrainFlow/BoardShim.swift @@ -28,6 +28,7 @@ public final class BoardShim { } 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 @@ -86,34 +87,40 @@ public final class BoardShim { 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 BoardShimNative.withBoard { native in - try checkBrainFlowExitCode( - native.config_board(configPtr, &response, &responseLen, CInt(response.count), CInt(board_id), params), - "Error in config_board" - ) + 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" + ) + } } - return String(cString: response) + 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 { return } + guard !bytes.isEmpty else { throw invalidArguments("bytes must be non-empty") } try serialized_params.withCString { params in try bytes.withUnsafeBufferPointer { buffer in - let raw = UnsafeRawPointer(buffer.baseAddress!).assumingMemoryBound(to: CChar.self) - try BoardShimNative.withBoard { native in - try checkBrainFlowExitCode( - native.config_board_with_bytes(raw, CInt(bytes.count), CInt(board_id), params), - "Error in config_board_with_bytes" - ) + 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 diff --git a/swift_package/Sources/BrainFlow/BrainFlowNative.swift b/swift_package/Sources/BrainFlow/BrainFlowNative.swift index fa82063df..6c06e7a4c 100644 --- a/swift_package/Sources/BrainFlow/BrainFlowNative.swift +++ b/swift_package/Sources/BrainFlow/BrainFlowNative.swift @@ -39,11 +39,7 @@ final class NativeLibrary { } private static var openFlags: Int32 { - #if os(Linux) return RTLD_NOW | RTLD_GLOBAL - #else - return RTLD_NOW | RTLD_GLOBAL - #endif } private static func candidatePaths(for names: [String]) -> [String] { @@ -128,7 +124,7 @@ enum NativeLibraries { final class LazyNativeLibrary { private let names: [String] private let lock = NSLock() - private var storage: Result? + private var storage: NativeLibrary? init(names: [String]) { self.names = names @@ -138,10 +134,10 @@ final class LazyNativeLibrary { lock.lock() defer { lock.unlock() } if let storage { - return try storage.get() + return storage } - let result = Result { try NativeLibrary(names: names) } - storage = result - return try result.get() + let library = try NativeLibrary(names: names) + storage = library + return library } } diff --git a/swift_package/Sources/BrainFlow/DataFilter.swift b/swift_package/Sources/BrainFlow/DataFilter.swift index 7f1bf4f82..ec21ef5a9 100644 --- a/swift_package/Sources/BrainFlow/DataFilter.swift +++ b/swift_package/Sources/BrainFlow/DataFilter.swift @@ -339,10 +339,16 @@ public enum DataFilter { } public static func get_csp(data: [[[Double]]], labels: [Double]) throws -> CSPResult { - guard let firstEpoch = data.first, let firstChannel = firstEpoch.first else { throw invalidArguments("Invalid CSP data") } + 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 { @@ -368,6 +374,7 @@ public enum DataFilter { } 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 @@ -413,6 +420,7 @@ public enum DataFilter { } 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 @@ -461,7 +469,10 @@ public enum DataFilter { } public static func get_psd_welch(data: [Double], nfft: Int, overlap: Int, sampling_rate: Int, window: Int) throws -> PSD { - guard nfft % 2 == 0 else { throw invalidArguments("nfft must be even") } + 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) @@ -482,6 +493,8 @@ public enum DataFilter { } 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 @@ -513,8 +526,11 @@ public enum DataFilter { 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") } - let cols = data[channels[0]].count + 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 { @@ -543,7 +559,11 @@ public enum DataFilter { 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..= 1 else { throw invalidArguments("num_components must be positive") } + guard !selectedChannels.isEmpty else { throw invalidArguments("channels must be non-empty") } + guard selectedChannels.allSatisfy({ $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 { @@ -578,6 +598,7 @@ public enum DataFilter { 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 @@ -588,6 +609,7 @@ public enum DataFilter { } 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 @@ -599,6 +621,8 @@ public enum DataFilter { } 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 @@ -613,6 +637,9 @@ public enum DataFilter { } 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 @@ -655,6 +682,7 @@ public enum DataFilter { 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 diff --git a/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift b/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift index 0b2142a9f..54ccae074 100644 --- a/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift +++ b/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift @@ -10,6 +10,24 @@ final class BrainFlowTests: XCTestCase { } } + 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" @@ -21,6 +39,35 @@ final class BrainFlowTests: XCTestCase { 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) From 08f0601f3a965bca7ec10240a7a50a7e2030d470 Mon Sep 17 00:00:00 2001 From: tsvvladimir Date: Sun, 7 Jun 2026 23:38:14 +0200 Subject: [PATCH 5/6] Add Apple XCFramework artifact workflow --- .github/workflows/run_unix.yml | 47 ++ .gitignore | 2 + AGENTS.md | 40 ++ CMakeLists.txt | 6 + docs/BuildBrainFlow.rst | 13 +- src/utils/inc/runtime_dll_loader.h | 124 +++- swift_package/Docs/AppStoreReadiness.md | 24 +- swift_package/Docs/AppleBinaryDistribution.md | 136 +++++ swift_package/README.md | 21 + .../Sources/BrainFlow/BrainFlowNative.swift | 39 ++ .../Sources/BrainFlowMacDemo/main.swift | 113 +++- .../project.pbxproj | 8 +- .../BrainFlowiOSDemoApp.swift | 51 +- .../apps/ios/BrainFlowiOSDemo/README.md | 28 +- .../apps/macos/BrainFlowMacDemo/README.md | 16 +- tools/apple/build_xcframeworks.sh | 577 ++++++++++++++++++ tools/apple/package_macos_demo_app.sh | 92 +++ tools/apple/regenerate_artifacts.sh | 13 + tools/apple/regenerate_lfs_artifacts.sh | 5 + tools/apple/test_swift_binary_package.sh | 84 +++ tools/apple/verify_xcframeworks.sh | 110 ++++ 21 files changed, 1508 insertions(+), 41 deletions(-) create mode 100644 AGENTS.md create mode 100644 swift_package/Docs/AppleBinaryDistribution.md create mode 100755 tools/apple/build_xcframeworks.sh create mode 100755 tools/apple/package_macos_demo_app.sh create mode 100755 tools/apple/regenerate_artifacts.sh create mode 100755 tools/apple/regenerate_lfs_artifacts.sh create mode 100755 tools/apple/test_swift_binary_package.sh create mode 100755 tools/apple/verify_xcframeworks.sh diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index c4a464014..dcb937eba 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -116,6 +116,53 @@ jobs: 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/manifest.json - name: Compile BrainFlow Ubuntu if: (matrix.os == 'ubuntu-latest') run: | diff --git a/.gitignore b/.gitignore index ff0fed197..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 @@ -374,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..275f5a6dc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# 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` plus checksum files. + +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, run verification, and let CI upload +`BrainFlowAppleXCFrameworks.zip` plus `BrainFlowAppleXCFrameworks.zip.sha256`, `checksums.sha256`, +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, regenerate artifacts, verify the generated tree, smoke-test the generated +Swift binary package, and validate the iOS/macOS sample apps against the regenerated XCFrameworks. +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 0e8eae9f6..896e344b5 100644 --- a/docs/BuildBrainFlow.rst +++ b/docs/BuildBrainFlow.rst @@ -167,6 +167,17 @@ Local build example: 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:`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. + The macOS demo can be built with: .. code-block:: bash @@ -174,7 +185,7 @@ The macOS demo can be built with: 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` and :code:`swift_package/Docs/AppStoreReadiness.md`. iOS runtime support requires BrainFlow native libraries compiled for the target iOS architectures and embedded as signed app-bundle libraries or XCFrameworks. +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/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/AppStoreReadiness.md b/swift_package/Docs/AppStoreReadiness.md index d03c3828e..e1c6244dd 100644 --- a/swift_package/Docs/AppStoreReadiness.md +++ b/swift_package/Docs/AppStoreReadiness.md @@ -8,32 +8,42 @@ This checklist is intentionally separate from the sample source because final Ap - 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 native libraries or XCFrameworks in the app bundle and sign them with the app. +- 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. -- Keep `swift_package` as the local package dependency. -- Provide iOS-compatible BrainFlow native binaries. The current high-level Swift package compiles for iOS, but the app can only run BrainFlow calls when matching native libraries are embedded. +- 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 create an Xcode app target for App Store archiving. +- 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 dylibs/XCFrameworks. -- Verify dynamic loading works inside the archived app bundle, not only with `BRAINFLOW_LIB_DIR`. +- 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. -- iOS and macOS app targets launch, handle missing native libraries gracefully, and run the synthetic-board workflow when libraries are embedded. +- `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 `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, and optional native feature flags. +- 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..fbacbde1d --- /dev/null +++ b/swift_package/Docs/AppleBinaryDistribution.md @@ -0,0 +1,136 @@ +# 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/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. + +Generated artifacts are not committed to the source repository. CI uploads +`BrainFlowAppleXCFrameworks.zip` as a workflow artifact; releases can publish that zip and checksum +for downstream app developers. + +Add `BrainFlowSwiftBinaryPackage` to an app in Xcode or with Swift Package Manager. Xcode embeds +and signs the binary frameworks during app builds. + +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 and run `tools/apple/regenerate_artifacts.sh`. +3. Verify `build/apple_xcframeworks` with `tools/apple/verify_xcframeworks.sh`. +4. Run the generated Swift binary package smoke test: + +```bash +tools/apple/test_swift_binary_package.sh build/apple_xcframeworks +``` + +5. Build the iOS demo and package the macOS demo against `build/apple_xcframeworks/XCFrameworks`. +6. Publish `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, and optional native feature flags. 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 XCFramework archives from a release URL and use +URL-based `binaryTarget` declarations with checksums generated by `swift package compute-checksum`. +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/README.md b/swift_package/README.md index 49cd65d7e..5a15b845b 100644 --- a/swift_package/README.md +++ b/swift_package/README.md @@ -18,6 +18,27 @@ The Swift package does not vendor native BrainFlow binaries. Build native librar - `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. + +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 diff --git a/swift_package/Sources/BrainFlow/BrainFlowNative.swift b/swift_package/Sources/BrainFlow/BrainFlowNative.swift index 6c06e7a4c..f791d8217 100644 --- a/swift_package/Sources/BrainFlow/BrainFlowNative.swift +++ b/swift_package/Sources/BrainFlow/BrainFlowNative.swift @@ -74,12 +74,51 @@ final class NativeLibrary { 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) diff --git a/swift_package/Sources/BrainFlowMacDemo/main.swift b/swift_package/Sources/BrainFlowMacDemo/main.swift index df8b4b68a..a1de361a3 100644 --- a/swift_package/Sources/BrainFlowMacDemo/main.swift +++ b/swift_package/Sources/BrainFlowMacDemo/main.swift @@ -1,4 +1,5 @@ import BrainFlow +import Foundation import SwiftUI #if os(macOS) @@ -25,6 +26,8 @@ struct ContentView: View { @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) { @@ -38,6 +41,9 @@ struct ContentView: View { infoRow("Samples", "\(cols)") } + EEGPlotView(series: eegSeries) + .frame(height: 150) + HStack { Button(isRunning ? "Stop" : "Start") { isRunning ? stop() : start() @@ -55,12 +61,15 @@ struct ContentView: View { } } .padding(24) - .frame(minWidth: 420, minHeight: 240) + .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 { @@ -75,12 +84,19 @@ struct ContentView: View { 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)" } @@ -88,6 +104,8 @@ struct ContentView: View { private func stop() { do { + pollingTask?.cancel() + pollCurrentData() try board?.stop_stream() isRunning = false status = "Stopped" @@ -99,8 +117,7 @@ struct ContentView: View { private func read() { do { let data = try board?.get_board_data() ?? [] - rows = data.count - cols = data.first?.count ?? 0 + updateDisplay(with: data) status = "Read \(cols) samples" } catch { status = "Read failed: \(error)" @@ -109,6 +126,10 @@ struct ContentView: View { private func release() { do { + pollingTask?.cancel() + if isRunning { + try board?.stop_stream() + } try board?.release_session() board = nil isRunning = false @@ -118,6 +139,40 @@ struct ContentView: View { } } + 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() @@ -145,3 +200,55 @@ struct ContentView: View { #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/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj index 5e9416ee2..416709b3a 100644 --- a/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/project.pbxproj @@ -145,13 +145,13 @@ outputFileListPaths = ( ); outputPaths = ( - "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/libBoardController.dylib", - "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/libDataHandler.dylib", - "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/libMLModule.dylib", + "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/BoardController.framework/BoardController", + "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/DataHandler.framework/DataHandler", + "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/MLModule.framework/MLModule", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -euo pipefail\nDEFAULT_LIB_DIR=\"${SRCROOT}/../../../../../installed_ios_sim/lib\"\nif [ \"${PLATFORM_NAME}\" = \"iphoneos\" ]; then\n DEFAULT_LIB_DIR=\"${SRCROOT}/../../../../../installed_ios/lib\"\nfi\nLIB_DIR=\"${BRAINFLOW_IOS_NATIVE_LIB_DIR:-${DEFAULT_LIB_DIR}}\"\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nmkdir -p \"$FRAMEWORKS_DIR\"\nfor lib in libBoardController.dylib libDataHandler.dylib libMLModule.dylib; do\n if [ ! -f \"$LIB_DIR/$lib\" ]; then\n echo \"error: missing BrainFlow native library $LIB_DIR/$lib\" >&2\n exit 1\n fi\n cp \"$LIB_DIR/$lib\" \"$FRAMEWORKS_DIR/$lib\"\n codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY:--}\" --timestamp=none \"$FRAMEWORKS_DIR/$lib\" || codesign --force --sign - --timestamp=none \"$FRAMEWORKS_DIR/$lib\"\ndone\n"; + shellScript = "set -euo pipefail\nDEFAULT_XCFRAMEWORKS_DIR=\"${SRCROOT}/../../../../../build/apple_xcframeworks/XCFrameworks\"\nXCFRAMEWORKS_DIR=\"${BRAINFLOW_APPLE_XCFRAMEWORKS_DIR:-${DEFAULT_XCFRAMEWORKS_DIR}}\"\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nmkdir -p \"$FRAMEWORKS_DIR\"\nselect_framework_slice() {\n framework_name=\"$1\"\n xcframework=\"$XCFRAMEWORKS_DIR/${framework_name}.xcframework\"\n if [ ! -d \"$xcframework\" ]; then\n echo \"error: missing BrainFlow XCFramework $xcframework\" >&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 */ diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift index 45e535cfa..8e6d72fbc 100644 --- a/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift @@ -38,6 +38,7 @@ struct ContentView: View { @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 { @@ -97,6 +98,9 @@ struct ContentView: View { .task { await runAutomatedDemoIfRequested() } + .onDisappear { + pollingTask?.cancel() + } } private func infoRow(_ title: String, _ value: String) -> some View { @@ -111,6 +115,12 @@ struct ContentView: View { 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) @@ -122,7 +132,11 @@ struct ContentView: View { 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)" } @@ -130,6 +144,8 @@ struct ContentView: View { private func stopStream() { do { + pollingTask?.cancel() + pollCurrentData() try board?.stop_stream() status = "Stopped" isStreaming = false @@ -155,6 +171,10 @@ struct ContentView: View { private func releaseSession() { do { + pollingTask?.cancel() + if isStreaming { + try board?.stop_stream() + } try board?.release_session() board = nil isStreaming = false @@ -164,9 +184,32 @@ struct ContentView: View { } } - private func updateDisplay(with data: [[Double]], boardId: Int) { + 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 - sampleCount = data.first?.count ?? 0 + self.sampleCount = sampleCount ?? (data.first?.count ?? 0) let eegChannels = (try? BoardShim.get_eeg_channels(board_id: boardId)) ?? [] eegSeries = eegChannels.prefix(4).compactMap { channel in @@ -223,6 +266,10 @@ private struct EEGPlotView: View { 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) + } } } } diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md b/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md index 3ae60c676..535f18272 100644 --- a/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md @@ -4,34 +4,26 @@ This sample is a normal Xcode iOS application that exercises the BrainFlow Swift ## Run In Simulator -From the repository root, build simulator native libraries first: +From the repository root, regenerate the Apple artifacts first: ```bash -cmake -S . -B build_ios_sim -G Ninja \ - -DCMAKE_SYSTEM_NAME=iOS \ - -DCMAKE_OSX_SYSROOT=iphonesimulator \ - -DCMAKE_OSX_ARCHITECTURES=arm64 \ - -DCMAKE_INSTALL_PREFIX=installed_ios_sim \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_BLUETOOTH=OFF \ - -DBUILD_BLE=OFF \ - -DBUILD_ONNX=OFF \ - -DBUILD_TESTS=OFF \ - -DBUILD_SYNCHRONI_SDK=OFF -ninja -C build_ios_sim install +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 libraries from `../../../../../installed_ios_sim/lib` by default. +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, build signed `iphoneos` native libraries into `installed_ios/lib` or set `BRAINFLOW_IOS_NATIVE_LIB_DIR` to a directory containing the device slices for: +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: -- `libBoardController.dylib` -- `libDataHandler.dylib` -- `libMLModule.dylib` +- `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. diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md b/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md index ebcc92537..cfd300646 100644 --- a/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md +++ b/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md @@ -2,7 +2,7 @@ The buildable macOS SwiftUI demo target lives in `swift_package` as `BrainFlowMacDemo`. -For Mac App Store distribution, use this folder's `Info.plist`, entitlements, and privacy manifest as starting assets in an Xcode app target. Embed the BrainFlow dynamic libraries or XCFrameworks in the app bundle, sign them with the same team, and keep the sandbox entitlement enabled. +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: @@ -12,9 +12,21 @@ Release placeholders to replace before upload: - Mac App Store screenshots - Final privacy answers for any real-board connectivity features -Local smoke test: +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/tools/apple/build_xcframeworks.sh b/tools/apple/build_xcframeworks.sh new file mode 100755 index 000000000..3c0c11c33 --- /dev/null +++ b/tools/apple/build_xcframeworks.sh @@ -0,0 +1,577 @@ +#!/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}" + +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 + +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 + +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 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}" + +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 + +source_revision="unknown" +if command -v git >/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 -type f -print | LC_ALL=C sort | while IFS= read -r artifact_file; do + shasum -a 256 "${artifact_file}" + done > 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..1a2a21714 --- /dev/null +++ b/tools/apple/verify_xcframeworks.sh @@ -0,0 +1,110 @@ +#!/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; } + +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 + +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}" From 0eef31b52402d36590161084be039db1297a8851 Mon Sep 17 00:00:00 2001 From: tsvvladimir Date: Tue, 9 Jun 2026 09:06:24 +0200 Subject: [PATCH 6/6] Align Apple artifacts with SwiftPM binary release guidance --- .github/workflows/run_unix.yml | 4 + AGENTS.md | 21 ++- docs/BuildBrainFlow.rst | 4 +- swift_package/Docs/AppStoreReadiness.md | 9 +- swift_package/Docs/AppleBinaryDistribution.md | 48 ++++-- swift_package/README.md | 6 + tools/apple/build_xcframeworks.sh | 161 +++++++++++++++++- tools/apple/verify_xcframeworks.sh | 101 +++++++++++ 8 files changed, 325 insertions(+), 29 deletions(-) diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index dcb937eba..557001dae 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -162,7 +162,11 @@ jobs: ${{ 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/AGENTS.md b/AGENTS.md index 275f5a6dc..c805cb347 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,12 @@ 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` plus checksum files. +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`, @@ -29,12 +34,14 @@ The iOS demo and macOS packaging script default to `build/apple_xcframeworks/XCF CI may override artifact paths with `BRAINFLOW_APPLE_XCFRAMEWORKS_DIR`. -When changing Apple artifact generation, regenerate locally, run verification, and let CI upload -`BrainFlowAppleXCFrameworks.zip` plus `BrainFlowAppleXCFrameworks.zip.sha256`, `checksums.sha256`, -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. +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, regenerate artifacts, verify the generated tree, smoke-test the generated -Swift binary package, and validate the iOS/macOS sample apps against the regenerated XCFrameworks. +`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/docs/BuildBrainFlow.rst b/docs/BuildBrainFlow.rst index 896e344b5..4a8a14e2e 100644 --- a/docs/BuildBrainFlow.rst +++ b/docs/BuildBrainFlow.rst @@ -174,10 +174,12 @@ For production iOS and macOS applications, use Apple XCFramework artifacts and t 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:`BrainFlowAppleXCFrameworks.zip`, and checksum files. These generated headers and binaries are release/CI artifacts, not source files committed to the repository. +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 diff --git a/swift_package/Docs/AppStoreReadiness.md b/swift_package/Docs/AppStoreReadiness.md index e1c6244dd..172b8144b 100644 --- a/swift_package/Docs/AppStoreReadiness.md +++ b/swift_package/Docs/AppStoreReadiness.md @@ -38,11 +38,12 @@ This checklist is intentionally separate from the sample source because final Ap - 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 `BrainFlowAppleXCFrameworks.zip`, - `BrainFlowAppleXCFrameworks.zip.sha256`, `checksums.sha256`, and `manifest.json` from the same - build. +- 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, and optional native feature flags. + 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. diff --git a/swift_package/Docs/AppleBinaryDistribution.md b/swift_package/Docs/AppleBinaryDistribution.md index fbacbde1d..24c2dfbd4 100644 --- a/swift_package/Docs/AppleBinaryDistribution.md +++ b/swift_package/Docs/AppleBinaryDistribution.md @@ -23,6 +23,8 @@ 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: @@ -33,14 +35,22 @@ The generated package contains the BrainFlow Swift API plus binary targets for: 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; releases can publish that zip and checksum -for downstream app developers. +`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. @@ -77,31 +87,41 @@ Apple library releases should be reproducible from the same source revision as t 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 and run `tools/apple/regenerate_artifacts.sh`. -3. Verify `build/apple_xcframeworks` with `tools/apple/verify_xcframeworks.sh`. -4. Run the generated Swift binary package smoke test: +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 ``` -5. Build the iOS demo and package the macOS demo against `build/apple_xcframeworks/XCFrameworks`. -6. Publish `BrainFlowAppleXCFrameworks.zip`, `BrainFlowAppleXCFrameworks.zip.sha256`, - `checksums.sha256`, and `manifest.json` from the same CI run or GitHub Release. +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, and optional native feature flags. Use it as the compatibility record for -supporting downstream developers and for reproducing a release later. +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 XCFramework archives from a release URL and use -URL-based `binaryTarget` declarations with checksums generated by `swift package compute-checksum`. -For local development or downloaded release archives, the generated `BrainFlowSwiftBinaryPackage` -uses path-based binary targets. Apple documents both distribution modes in +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: diff --git a/swift_package/README.md b/swift_package/README.md index 5a15b845b..2f457b5cc 100644 --- a/swift_package/README.md +++ b/swift_package/README.md @@ -30,6 +30,12 @@ The generated `build/apple_xcframeworks/BrainFlowSwiftBinaryPackage` is a normal 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 diff --git a/tools/apple/build_xcframeworks.sh b/tools/apple/build_xcframeworks.sh index 3c0c11c33..462b3542c 100755 --- a/tools/apple/build_xcframeworks.sh +++ b/tools/apple/build_xcframeworks.sh @@ -13,6 +13,7 @@ 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' @@ -38,6 +39,7 @@ 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: @@ -99,8 +101,19 @@ while [[ $# -gt 0 ]]; do 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; } @@ -112,6 +125,11 @@ 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 @@ -520,6 +538,125 @@ 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)" @@ -539,9 +676,10 @@ ninja_version="$(ninja --version 2>/dev/null || true)" ( cd "${OUTPUT_DIR}" - find XCFrameworks BrainFlowSwiftBinaryPackage -type f -print | LC_ALL=C sort | while IFS= read -r artifact_file; do + 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" < "${OUTPUT_DIR}/manifest.json" < BrainFlowAppleXCFrameworks.zip.sha256 ) diff --git a/tools/apple/verify_xcframeworks.sh b/tools/apple/verify_xcframeworks.sh index 1a2a21714..4b85212c1 100755 --- a/tools/apple/verify_xcframeworks.sh +++ b/tools/apple/verify_xcframeworks.sh @@ -18,6 +18,8 @@ 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" @@ -86,6 +88,105 @@ if [[ ! -f "${package_dir}/Package.swift" ]]; then 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