diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index 13a2a8bfb..c4a464014 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -82,6 +82,40 @@ jobs: ninja install env: BRAINFLOW_VERSION: ${{ steps.version.outputs.version }} + - name: Build Swift Package MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/swift_package + swift --version + BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift build + - name: Test Swift Package MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/swift_package + BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift test + - name: Swift CLI Synthetic Board MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/swift_package + BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift run brainflow-swift-cli + - name: Swift Examples MacOS + if: (matrix.os == 'macos-14') + run: | + cd $GITHUB_WORKSPACE/swift_package + for example in \ + swift-brainflow-get-data \ + swift-markers \ + swift-read-write-file \ + swift-downsampling \ + swift-transforms \ + swift-signal-filtering \ + swift-denoising \ + swift-band-power \ + swift-eeg-metrics \ + swift-ica + do + BRAINFLOW_LIB_DIR=$GITHUB_WORKSPACE/installed/lib swift run "$example" + done - name: Compile BrainFlow Ubuntu if: (matrix.os == 'ubuntu-latest') run: | diff --git a/.gitignore b/.gitignore index 8a2beb362..9bc2484e5 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,10 @@ ipch/ *.opensdf *.sdf *.cachefile +.swiftpm/ +.build/ +xcuserdata/ +DerivedData/ *.VC.db *.VC.VC.opendb @@ -343,6 +347,7 @@ ASALocalRun/ .vscode/ installed* +build_ios_sim/ compiled/ python/flowcat.egg-info/ .Rproj.user @@ -384,4 +389,3 @@ Makefile *.cmd CMakeSettings.json ._.gitignore - diff --git a/docs/BuildBrainFlow.rst b/docs/BuildBrainFlow.rst index 2832c68f3..0e8eae9f6 100644 --- a/docs/BuildBrainFlow.rst +++ b/docs/BuildBrainFlow.rst @@ -152,7 +152,29 @@ Rust Swift ------- -You can build Swift binding for BrainFlow using xcode. Before that you need to compile C/C++ code :ref:`compilation-label` and ensure that native libraries are properly placed. Keep in mind that currently it supports only MacOS. +You can build Swift bindings for BrainFlow with Swift Package Manager or Xcode. Before running examples or tests you need to compile C/C++ code :ref:`compilation-label` and ensure that native libraries are available to the Swift runtime loader. + +Local build example: + +.. code-block:: bash + + python3 tools/build.py + cd swift_package + BRAINFLOW_LIB_DIR=../installed/lib swift build + BRAINFLOW_LIB_DIR=../installed/lib swift test + BRAINFLOW_LIB_DIR=../installed/lib swift run brainflow-swift-cli + BRAINFLOW_LIB_DIR=../installed/lib swift run swift-brainflow-get-data + +The Swift package intentionally does not vendor BrainFlow native binaries. Like source builds for other bindings, it dynamically loads native libraries built from this repository. The loader searches :code:`BRAINFLOW_LIB_DIR`, system library paths, :code:`installed/lib`, and app bundle resource/framework directories for :code:`libBoardController`, :code:`libDataHandler`, and :code:`libMLModule`. + +The macOS demo can be built with: + +.. code-block:: bash + + cd swift_package + BRAINFLOW_LIB_DIR=../installed/lib swift run BrainFlowMacDemo + +iOS and Mac App Store sample source and release-preparation notes are available in :code:`swift_package/examples/apps` 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/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/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..d03c3828e --- /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 + +- 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. +- 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 `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`. + +## 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..142926dd2 --- /dev/null +++ b/swift_package/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let exampleTargets: [(product: String, target: String, path: String)] = [ + ("swift-brainflow-get-data", "SwiftBrainFlowGetDataExample", "examples/tests/brainflow_get_data"), + ("swift-markers", "SwiftMarkersExample", "examples/tests/markers"), + ("swift-read-write-file", "SwiftReadWriteFileExample", "examples/tests/read_write_file"), + ("swift-downsampling", "SwiftDownsamplingExample", "examples/tests/downsampling"), + ("swift-transforms", "SwiftTransformsExample", "examples/tests/transforms"), + ("swift-signal-filtering", "SwiftSignalFilteringExample", "examples/tests/signal_filtering"), + ("swift-denoising", "SwiftDenoisingExample", "examples/tests/denoising"), + ("swift-band-power", "SwiftBandPowerExample", "examples/tests/band_power"), + ("swift-eeg-metrics", "SwiftEEGMetricsExample", "examples/tests/eeg_metrics"), + ("swift-ica", "SwiftICAExample", "examples/tests/ica") +] + +let package = Package( + name: "BrainFlow", + platforms: [ + .macOS(.v12), + .iOS(.v15) + ], + products: [ + .library(name: "BrainFlow", targets: ["BrainFlow"]), + .executable(name: "brainflow-swift-cli", targets: ["BrainFlowCLI"]), + .executable(name: "BrainFlowMacDemo", targets: ["BrainFlowMacDemo"]) + ] + exampleTargets.map { .executable(name: $0.product, targets: [$0.target]) }, + targets: [ + .target( + name: "BrainFlow" + ), + .target( + name: "BrainFlowExampleSupport", + dependencies: ["BrainFlow"], + path: "examples/tests/support" + ), + .executableTarget( + name: "BrainFlowCLI", + dependencies: ["BrainFlow"] + ), + .executableTarget( + name: "BrainFlowMacDemo", + dependencies: ["BrainFlow"] + ), + .testTarget( + name: "BrainFlowTests", + dependencies: ["BrainFlow"] + ) + ] + exampleTargets.map { + .executableTarget( + name: $0.target, + dependencies: ["BrainFlow", "BrainFlowExampleSupport"], + path: $0.path + ) + } +) diff --git a/swift_package/README.md b/swift_package/README.md new file mode 100644 index 000000000..49cd65d7e --- /dev/null +++ b/swift_package/README.md @@ -0,0 +1,50 @@ +# BrainFlow Swift + +Swift bindings call BrainFlow's native C ABI through runtime dynamic loading. Build the native libraries first, then point Swift at the installed library directory. + +```bash +cd .. +python3 tools/build.py + +cd swift_package +BRAINFLOW_LIB_DIR=../installed/lib swift test +BRAINFLOW_LIB_DIR=../installed/lib swift run brainflow-swift-cli +BRAINFLOW_LIB_DIR=../installed/lib swift run swift-brainflow-get-data +``` + +The Swift package does not vendor native BrainFlow binaries. Build native libraries from this repository and provide them at runtime. The loader searches `BRAINFLOW_LIB_DIR`, `DYLD_LIBRARY_PATH`, `LD_LIBRARY_PATH`, `installed/lib`, app bundle resources, and the current directory for: + +- `libBoardController.dylib` +- `libDataHandler.dylib` +- `libMLModule.dylib` + +On Linux the equivalent `.so` names are used. + +## API Coverage + +The package exposes Swift equivalents for the public Python/Java binding surface: + +- `BoardShim`: session lifecycle, streamers, data reads, markers, config, board metadata, logging, versions. +- `DataFilter`: filters, denoising, FFT/IFFT, PSD, band powers, CSP, ICA, file IO, statistics, logging, versions. +- `MLModel`: model params, prepare/release, predict, logging, versions. +- `BrainFlowInputParams`, `BrainFlowModelParams`, `BrainFlowError`, and public constants/enums. + +Swift in-place signal-processing methods take `inout [Double]`, for example: + +```swift +var data = Array(0..<256).map { sin(Double($0) / 10.0) } +try DataFilter.perform_lowpass( + data: &data, + sampling_rate: 250, + cutoff: 30.0, + order: 4, + filter_type: .BUTTERWORTH, + ripple: 0.0 +) +``` + +## Apps + +- `swift run BrainFlowMacDemo` builds a simple macOS SwiftUI demo against the synthetic board. +- `examples/apps/ios/BrainFlowiOSDemo` contains an Xcode iOS app project with synthetic-board autorun, Muse/native BLE board selection, and an EEG plot. +- `examples/apps/macos/BrainFlowMacDemo` contains Mac App Store release-prep metadata for an Xcode app bundle. diff --git a/swift_package/Sources/BrainFlow/BoardShim.swift b/swift_package/Sources/BrainFlow/BoardShim.swift new file mode 100644 index 000000000..4aa5d270c --- /dev/null +++ b/swift_package/Sources/BrainFlow/BoardShim.swift @@ -0,0 +1,655 @@ +import Foundation + +public final class BoardShim { + private let board_id: Int + private let input_params: BrainFlowInputParams + private let serialized_params: String + + public init(board_id: Int, input_params: BrainFlowInputParams = BrainFlowInputParams()) throws { + self.board_id = board_id + self.input_params = input_params + serialized_params = try input_params.to_json() + } + + public convenience init(board_id: BoardIds, input_params: BrainFlowInputParams = BrainFlowInputParams()) throws { + try self.init(board_id: board_id.rawValue, input_params: input_params) + } + + deinit { + try? release_session() + } + + public func prepare_session() throws { + try serialized_params.withCString { params in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.prepare_session(CInt(board_id), params), "Error in prepare_session") + } + } + } + + public func start_stream(buffer_size: Int = 450_000, streamer_params: String = "") throws { + guard buffer_size > 0 else { throw invalidArguments("buffer_size must be positive") } + try serialized_params.withCString { params in + try streamer_params.withCString { streamer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.start_stream(CInt(buffer_size), streamer, CInt(board_id), params), + "Error in start_stream" + ) + } + } + } + } + + public func stop_stream() throws { + try serialized_params.withCString { params in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.stop_stream(CInt(board_id), params), "Error in stop_stream") + } + } + } + + public func release_session() throws { + try serialized_params.withCString { params in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.release_session(CInt(board_id), params), "Error in release_session") + } + } + } + + public func add_streamer(_ streamer: String, preset: BrainFlowPresets = .DEFAULT_PRESET) throws { + try serialized_params.withCString { params in + try streamer.withCString { streamerPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.add_streamer(streamerPtr, CInt(preset.rawValue), CInt(board_id), params), + "Error in add_streamer" + ) + } + } + } + } + + public func delete_streamer(_ streamer: String, preset: BrainFlowPresets = .DEFAULT_PRESET) throws { + try serialized_params.withCString { params in + try streamer.withCString { streamerPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.delete_streamer(streamerPtr, CInt(preset.rawValue), CInt(board_id), params), + "Error in delete_streamer" + ) + } + } + } + } + + public func config_board(_ config: String) throws -> String { + try serialized_params.withCString { params in + try config.withCString { configPtr in + var response = [CChar](repeating: 0, count: 16_000) + let responseCapacity = response.count + var responseLen: CInt = 0 + try response.withUnsafeMutableBufferPointer { responsePtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.config_board(configPtr, responsePtr.baseAddress, &responseLen, CInt(responseCapacity), CInt(board_id), params), + "Error in config_board" + ) + } + } + let returnedCount = min(max(Int(responseLen), 0), responseCapacity) + return String(bytes: response.prefix(returnedCount).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } + } + } + + public func config_board_with_bytes(_ bytes: [UInt8]) throws { + guard !bytes.isEmpty else { throw invalidArguments("bytes must be non-empty") } + try serialized_params.withCString { params in + try bytes.withUnsafeBufferPointer { buffer in + try buffer.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buffer.count) { bytesPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.config_board_with_bytes(bytesPtr, CInt(buffer.count), CInt(board_id), params), + "Error in config_board_with_bytes" + ) + } + } + } + } + } + + public func get_current_board_data(num_samples: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [[Double]] { + guard num_samples > 0 else { throw invalidArguments("num_samples must be positive") } + let rows = try Self.get_num_rows(board_id: board_id, preset: preset) + var data = [Double](repeating: 0.0, count: rows * num_samples) + var returnedSamples: CInt = 0 + try serialized_params.withCString { params in + try data.withUnsafeMutableBufferPointer { dataPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.get_current_board_data(CInt(num_samples), CInt(preset.rawValue), dataPtr.baseAddress, &returnedSamples, CInt(board_id), params), + "Error in get_current_board_data" + ) + } + } + } + let count = Int(returnedSamples) + return BrainFlowArray.reshape_data_to_2d(num_rows: rows, num_cols: count, linear_buffer: Array(data.prefix(rows * count))) + } + + public func get_board_data(_ num_datapoints: Int? = nil, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [[Double]] { + let rows = try Self.get_num_rows(board_id: board_id, preset: preset) + let count = try num_datapoints ?? get_board_data_count(preset: preset) + guard count >= 0 else { throw invalidArguments("num_datapoints must be non-negative") } + var data = [Double](repeating: 0.0, count: rows * count) + try serialized_params.withCString { params in + try data.withUnsafeMutableBufferPointer { dataPtr in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.get_board_data(CInt(count), CInt(preset.rawValue), dataPtr.baseAddress, CInt(board_id), params), + "Error in get_board_data" + ) + } + } + } + return BrainFlowArray.reshape_data_to_2d(num_rows: rows, num_cols: count, linear_buffer: data) + } + + public func get_board_data_count(preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try serialized_params.withCString { params in + var result: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.get_board_data_count(CInt(preset.rawValue), &result, CInt(board_id), params), + "Error in get_board_data_count" + ) + } + return Int(result) + } + } + + public func get_board_id() -> Int { + board_id + } + + public func get_board_sampling_rate(preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try serialized_params.withCString { params in + var rate: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.get_board_sampling_rate(CInt(preset.rawValue), &rate, CInt(board_id), params), + "Error in get_board_sampling_rate" + ) + } + return Int(rate) + } + } + + public func insert_marker(_ value: Double, preset: BrainFlowPresets = .DEFAULT_PRESET) throws { + try serialized_params.withCString { params in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode( + native.insert_marker(value, CInt(preset.rawValue), CInt(board_id), params), + "Error in insert_marker" + ) + } + } + } + + public func is_prepared() throws -> Bool { + try serialized_params.withCString { params in + var prepared: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.is_prepared(&prepared, CInt(board_id), params), "Error in is_prepared") + } + return prepared != 0 + } + } + + public static func release_all_sessions() throws { + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.release_all_sessions(), "Error in release_all_sessions") + } + } + + public static func set_log_level(_ log_level: Int) throws { + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.set_log_level_board_controller(CInt(log_level)), "Error in set_log_level") + } + } + + public static func set_log_level(_ log_level: LogLevels) throws { + try set_log_level(log_level.rawValue) + } + + public static func enable_board_logger() throws { + try set_log_level(LogLevels.LEVEL_INFO) + } + + public static func enable_dev_board_logger() throws { + try set_log_level(LogLevels.LEVEL_TRACE) + } + + public static func disable_board_logger() throws { + try set_log_level(LogLevels.LEVEL_OFF) + } + + public static func set_log_file(_ log_file: String) throws { + try log_file.withCString { path in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.set_log_file_board_controller(path), "Error in set_log_file") + } + } + } + + public static func log_message(_ log_level: Int, message: String) throws { + var mutableMessage = Array(message.utf8CString) + try mutableMessage.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.log_message_board_controller(CInt(log_level), pointer.baseAddress), "Error in log_message") + } + } + } + + public static func log_message(_ log_level: LogLevels, message: String) throws { + try log_message(log_level.rawValue, message: message) + } + + public static func get_sampling_rate(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_sampling_rate) + } + + public static func get_sampling_rate(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_sampling_rate(board_id: board_id.rawValue, preset: preset) + } + + public static func get_package_num_channel(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_package_num_channel) + } + + public static func get_package_num_channel(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_package_num_channel(board_id: board_id.rawValue, preset: preset) + } + + public static func get_battery_channel(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_battery_channel) + } + + public static func get_battery_channel(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_battery_channel(board_id: board_id.rawValue, preset: preset) + } + + public static func get_num_rows(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_num_rows) + } + + public static func get_num_rows(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_num_rows(board_id: board_id.rawValue, preset: preset) + } + + public static func get_timestamp_channel(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_timestamp_channel) + } + + public static func get_timestamp_channel(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_timestamp_channel(board_id: board_id.rawValue, preset: preset) + } + + public static func get_marker_channel(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try getIntBoardInfo(board_id: board_id, preset: preset, function: \.get_marker_channel) + } + + public static func get_marker_channel(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> Int { + try get_marker_channel(board_id: board_id.rawValue, preset: preset) + } + + public static func get_eeg_names(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [String] { + let names = try getStringBoardInfo(board_id: board_id, preset: preset, maxLength: 4096, function: \.get_eeg_names) + return names.isEmpty ? [] : names.split(separator: ",").map(String.init) + } + + public static func get_eeg_names(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [String] { + try get_eeg_names(board_id: board_id.rawValue, preset: preset) + } + + public static func get_board_presets(board_id: Int) throws -> [BrainFlowPresets] { + var values = [CInt](repeating: 0, count: 512) + var length: CInt = 0 + try values.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native.get_board_presets(CInt(board_id), pointer.baseAddress, &length), "Error in get_board_presets") + } + } + return values.prefix(Int(length)).compactMap { BrainFlowPresets(rawValue: Int($0)) } + } + + public static func get_board_presets(board_id: BoardIds) throws -> [BrainFlowPresets] { + try get_board_presets(board_id: board_id.rawValue) + } + + public static func get_version() throws -> String { + try getVersion(function: \.get_version_board_controller) + } + + public static func get_board_descr(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> String { + try getStringBoardInfo(board_id: board_id, preset: preset, maxLength: 16_000, function: \.get_board_descr) + } + + public static func get_board_descr(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> String { + try get_board_descr(board_id: board_id.rawValue, preset: preset) + } + + public static func get_device_name(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> String { + try getStringBoardInfo(board_id: board_id, preset: preset, maxLength: 4096, function: \.get_device_name) + } + + public static func get_device_name(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> String { + try get_device_name(board_id: board_id.rawValue, preset: preset) + } + + public static func get_eeg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_eeg_channels) + } + + public static func get_eeg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_eeg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_exg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_exg_channels) + } + + public static func get_exg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_exg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_emg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_emg_channels) + } + + public static func get_emg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_emg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_ecg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_ecg_channels) + } + + public static func get_ecg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_ecg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_eog_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_eog_channels) + } + + public static func get_eog_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_eog_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_ppg_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_ppg_channels) + } + + public static func get_ppg_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_ppg_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_optical_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_optical_channels) + } + + public static func get_optical_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_optical_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_eda_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_eda_channels) + } + + public static func get_eda_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_eda_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_accel_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_accel_channels) + } + + public static func get_accel_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_accel_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_rotation_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_rotation_channels) + } + + public static func get_rotation_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_rotation_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_analog_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_analog_channels) + } + + public static func get_analog_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_analog_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_gyro_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_gyro_channels) + } + + public static func get_gyro_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_gyro_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_other_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_other_channels) + } + + public static func get_other_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_other_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_temperature_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_temperature_channels) + } + + public static func get_temperature_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_temperature_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_resistance_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_resistance_channels) + } + + public static func get_resistance_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_resistance_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func get_magnetometer_channels(board_id: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try getChannels(board_id: board_id, preset: preset, function: \.get_magnetometer_channels) + } + + public static func get_magnetometer_channels(board_id: BoardIds, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [Int] { + try get_magnetometer_channels(board_id: board_id.rawValue, preset: preset) + } + + public static func reshape_data_to_2d(num_rows: Int, num_cols: Int, linear_buffer: [Double]) -> [[Double]] { + BrainFlowArray.reshape_data_to_2d(num_rows: num_rows, num_cols: num_cols, linear_buffer: linear_buffer) + } + + private static func getIntBoardInfo( + board_id: Int, + preset: BrainFlowPresets, + function: KeyPath + ) throws -> Int { + var result: CInt = 0 + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native[keyPath: function](CInt(board_id), CInt(preset.rawValue), &result), "Error in board info getter") + } + return Int(result) + } + + private static func getChannels( + board_id: Int, + preset: BrainFlowPresets, + function: KeyPath + ) throws -> [Int] { + var channels = [CInt](repeating: 0, count: 512) + var length: CInt = 0 + try channels.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native[keyPath: function](CInt(board_id), CInt(preset.rawValue), pointer.baseAddress, &length), "Error in board channel getter") + } + } + return channels.prefix(Int(length)).map(Int.init) + } + + private static func getStringBoardInfo( + board_id: Int, + preset: BrainFlowPresets, + maxLength: Int, + function: KeyPath + ) throws -> String { + var bytes = [CChar](repeating: 0, count: maxLength) + var length: CInt = 0 + try bytes.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native[keyPath: function](CInt(board_id), CInt(preset.rawValue), pointer.baseAddress, &length, CInt(maxLength)), "Error in board string getter") + } + } + return String(bytes: bytes.prefix(Int(length)).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } + + private static func getVersion(function: KeyPath) throws -> String { + let maxLength = 64 + var bytes = [CChar](repeating: 0, count: maxLength) + var length: CInt = 0 + try bytes.withUnsafeMutableBufferPointer { pointer in + try BoardShimNative.withBoard { native in + try checkBrainFlowExitCode(native[keyPath: function](pointer.baseAddress, &length, CInt(maxLength)), "Error in get_version") + } + } + return String(bytes: bytes.prefix(Int(length)).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } +} + +final class BoardShimNative { + typealias BoardInfoIntFunction = @convention(c) (CInt, CInt, UnsafeMutablePointer?) -> CInt + typealias BoardInfoChannelsFunction = @convention(c) (CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + typealias BoardInfoStringFunction = @convention(c) (CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, CInt) -> CInt + typealias VersionFunction = @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt) -> CInt + + let prepare_session: @convention(c) (CInt, UnsafePointer?) -> CInt + let start_stream: @convention(c) (CInt, UnsafePointer?, CInt, UnsafePointer?) -> CInt + let stop_stream: @convention(c) (CInt, UnsafePointer?) -> CInt + let release_session: @convention(c) (CInt, UnsafePointer?) -> CInt + let get_current_board_data: @convention(c) (CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let get_board_data_count: @convention(c) (CInt, UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let get_board_data: @convention(c) (CInt, CInt, UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let get_board_sampling_rate: @convention(c) (CInt, UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let config_board: @convention(c) (UnsafePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, CInt, UnsafePointer?) -> CInt + let config_board_with_bytes: @convention(c) (UnsafePointer?, CInt, CInt, UnsafePointer?) -> CInt + let is_prepared: @convention(c) (UnsafeMutablePointer?, CInt, UnsafePointer?) -> CInt + let insert_marker: @convention(c) (Double, CInt, CInt, UnsafePointer?) -> CInt + let add_streamer: @convention(c) (UnsafePointer?, CInt, CInt, UnsafePointer?) -> CInt + let delete_streamer: @convention(c) (UnsafePointer?, CInt, CInt, UnsafePointer?) -> CInt + let release_all_sessions: @convention(c) () -> CInt + let set_log_level_board_controller: @convention(c) (CInt) -> CInt + let set_log_file_board_controller: @convention(c) (UnsafePointer?) -> CInt + let log_message_board_controller: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt + let get_version_board_controller: VersionFunction + + let get_board_descr: BoardInfoStringFunction + let get_sampling_rate: BoardInfoIntFunction + let get_package_num_channel: BoardInfoIntFunction + let get_timestamp_channel: BoardInfoIntFunction + let get_marker_channel: BoardInfoIntFunction + let get_battery_channel: BoardInfoIntFunction + let get_num_rows: BoardInfoIntFunction + let get_eeg_names: BoardInfoStringFunction + let get_exg_channels: BoardInfoChannelsFunction + let get_eeg_channels: BoardInfoChannelsFunction + let get_emg_channels: BoardInfoChannelsFunction + let get_ecg_channels: BoardInfoChannelsFunction + let get_eog_channels: BoardInfoChannelsFunction + let get_ppg_channels: BoardInfoChannelsFunction + let get_optical_channels: BoardInfoChannelsFunction + let get_eda_channels: BoardInfoChannelsFunction + let get_accel_channels: BoardInfoChannelsFunction + let get_rotation_channels: BoardInfoChannelsFunction + let get_analog_channels: BoardInfoChannelsFunction + let get_gyro_channels: BoardInfoChannelsFunction + let get_other_channels: BoardInfoChannelsFunction + let get_temperature_channels: BoardInfoChannelsFunction + let get_resistance_channels: BoardInfoChannelsFunction + let get_magnetometer_channels: BoardInfoChannelsFunction + let get_device_name: BoardInfoStringFunction + let get_board_presets: @convention(c) (CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + + private static let lock = NSLock() + private static var cached: BoardShimNative? + + static func withBoard(_ body: (BoardShimNative) throws -> T) throws -> T { + try body(load()) + } + + private static func load() throws -> BoardShimNative { + lock.lock() + defer { lock.unlock() } + if let cached { return cached } + let value = try BoardShimNative(library: NativeLibraries.boardController.load()) + cached = value + return value + } + + private init(library: NativeLibrary) throws { + prepare_session = try library.symbol("prepare_session", as: type(of: prepare_session)) + start_stream = try library.symbol("start_stream", as: type(of: start_stream)) + stop_stream = try library.symbol("stop_stream", as: type(of: stop_stream)) + release_session = try library.symbol("release_session", as: type(of: release_session)) + get_current_board_data = try library.symbol("get_current_board_data", as: type(of: get_current_board_data)) + get_board_data_count = try library.symbol("get_board_data_count", as: type(of: get_board_data_count)) + get_board_data = try library.symbol("get_board_data", as: type(of: get_board_data)) + get_board_sampling_rate = try library.symbol("get_board_sampling_rate", as: type(of: get_board_sampling_rate)) + config_board = try library.symbol("config_board", as: type(of: config_board)) + config_board_with_bytes = try library.symbol("config_board_with_bytes", as: type(of: config_board_with_bytes)) + is_prepared = try library.symbol("is_prepared", as: type(of: is_prepared)) + insert_marker = try library.symbol("insert_marker", as: type(of: insert_marker)) + add_streamer = try library.symbol("add_streamer", as: type(of: add_streamer)) + delete_streamer = try library.symbol("delete_streamer", as: type(of: delete_streamer)) + release_all_sessions = try library.symbol("release_all_sessions", as: type(of: release_all_sessions)) + set_log_level_board_controller = try library.symbol("set_log_level_board_controller", as: type(of: set_log_level_board_controller)) + set_log_file_board_controller = try library.symbol("set_log_file_board_controller", as: type(of: set_log_file_board_controller)) + log_message_board_controller = try library.symbol("log_message_board_controller", as: type(of: log_message_board_controller)) + get_version_board_controller = try library.symbol("get_version_board_controller", as: type(of: get_version_board_controller)) + get_board_descr = try library.symbol("get_board_descr", as: type(of: get_board_descr)) + get_sampling_rate = try library.symbol("get_sampling_rate", as: type(of: get_sampling_rate)) + get_package_num_channel = try library.symbol("get_package_num_channel", as: type(of: get_package_num_channel)) + get_timestamp_channel = try library.symbol("get_timestamp_channel", as: type(of: get_timestamp_channel)) + get_marker_channel = try library.symbol("get_marker_channel", as: type(of: get_marker_channel)) + get_battery_channel = try library.symbol("get_battery_channel", as: type(of: get_battery_channel)) + get_num_rows = try library.symbol("get_num_rows", as: type(of: get_num_rows)) + get_eeg_names = try library.symbol("get_eeg_names", as: type(of: get_eeg_names)) + get_exg_channels = try library.symbol("get_exg_channels", as: type(of: get_exg_channels)) + get_eeg_channels = try library.symbol("get_eeg_channels", as: type(of: get_eeg_channels)) + get_emg_channels = try library.symbol("get_emg_channels", as: type(of: get_emg_channels)) + get_ecg_channels = try library.symbol("get_ecg_channels", as: type(of: get_ecg_channels)) + get_eog_channels = try library.symbol("get_eog_channels", as: type(of: get_eog_channels)) + get_ppg_channels = try library.symbol("get_ppg_channels", as: type(of: get_ppg_channels)) + get_optical_channels = try library.symbol("get_optical_channels", as: type(of: get_optical_channels)) + get_eda_channels = try library.symbol("get_eda_channels", as: type(of: get_eda_channels)) + get_accel_channels = try library.symbol("get_accel_channels", as: type(of: get_accel_channels)) + get_rotation_channels = try library.symbol("get_rotation_channels", as: type(of: get_rotation_channels)) + get_analog_channels = try library.symbol("get_analog_channels", as: type(of: get_analog_channels)) + get_gyro_channels = try library.symbol("get_gyro_channels", as: type(of: get_gyro_channels)) + get_other_channels = try library.symbol("get_other_channels", as: type(of: get_other_channels)) + get_temperature_channels = try library.symbol("get_temperature_channels", as: type(of: get_temperature_channels)) + get_resistance_channels = try library.symbol("get_resistance_channels", as: type(of: get_resistance_channels)) + get_magnetometer_channels = try library.symbol("get_magnetometer_channels", as: type(of: get_magnetometer_channels)) + get_device_name = try library.symbol("get_device_name", as: type(of: get_device_name)) + get_board_presets = try library.symbol("get_board_presets", as: type(of: get_board_presets)) + } +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift new file mode 100644 index 000000000..79aa5b0f4 --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift @@ -0,0 +1,236 @@ +public enum BoardIds: Int, CaseIterable, Sendable { + case NO_BOARD = -100 + case PLAYBACK_FILE_BOARD = -3 + case STREAMING_BOARD = -2 + case SYNTHETIC_BOARD = -1 + case CYTON_BOARD = 0 + case GANGLION_BOARD = 1 + case CYTON_DAISY_BOARD = 2 + case GALEA_BOARD = 3 + case GANGLION_WIFI_BOARD = 4 + case CYTON_WIFI_BOARD = 5 + case CYTON_DAISY_WIFI_BOARD = 6 + case BRAINBIT_BOARD = 7 + case UNICORN_BOARD = 8 + case CALLIBRI_EEG_BOARD = 9 + case CALLIBRI_EMG_BOARD = 10 + case CALLIBRI_ECG_BOARD = 11 + case NOTION_1_BOARD = 13 + case NOTION_2_BOARD = 14 + case GFORCE_PRO_BOARD = 16 + case FREEEEG32_BOARD = 17 + case BRAINBIT_BLED_BOARD = 18 + case GFORCE_DUAL_BOARD = 19 + case MUSE_S_BLED_BOARD = 21 + case MUSE_2_BLED_BOARD = 22 + case CROWN_BOARD = 23 + case ANT_NEURO_EE_410_BOARD = 24 + case ANT_NEURO_EE_411_BOARD = 25 + case ANT_NEURO_EE_430_BOARD = 26 + case ANT_NEURO_EE_211_BOARD = 27 + case ANT_NEURO_EE_212_BOARD = 28 + case ANT_NEURO_EE_213_BOARD = 29 + case ANT_NEURO_EE_214_BOARD = 30 + case ANT_NEURO_EE_215_BOARD = 31 + case ANT_NEURO_EE_221_BOARD = 32 + case ANT_NEURO_EE_222_BOARD = 33 + case ANT_NEURO_EE_223_BOARD = 34 + case ANT_NEURO_EE_224_BOARD = 35 + case ANT_NEURO_EE_225_BOARD = 36 + case ENOPHONE_BOARD = 37 + case MUSE_2_BOARD = 38 + case MUSE_S_BOARD = 39 + case BRAINALIVE_BOARD = 40 + case MUSE_2016_BOARD = 41 + case MUSE_2016_BLED_BOARD = 42 + case EXPLORE_4_CHAN_BOARD = 44 + case EXPLORE_8_CHAN_BOARD = 45 + case GANGLION_NATIVE_BOARD = 46 + case EMOTIBIT_BOARD = 47 + case NTL_WIFI_BOARD = 50 + case ANT_NEURO_EE_511_BOARD = 51 + case FREEEEG128_BOARD = 52 + case AAVAA_V3_BOARD = 53 + case EXPLORE_PLUS_8_CHAN_BOARD = 54 + case EXPLORE_PLUS_32_CHAN_BOARD = 55 + case PIEEG_BOARD = 56 + case NEUROPAWN_KNIGHT_BOARD = 57 + case SYNCHRONI_TRIO_3_CHANNELS_BOARD = 58 + case SYNCHRONI_OCTO_8_CHANNELS_BOARD = 59 + case OB5000_8_CHANNELS_BOARD = 60 + case SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61 + case SYNCHRONI_UNO_1_CHANNELS_BOARD = 62 + case OB3000_24_CHANNELS_BOARD = 63 + case BIOLISTENER_BOARD = 64 + case IRONBCI_32_BOARD = 65 + case NEUROPAWN_KNIGHT_BOARD_IMU = 66 + case MUSE_S_ATHENA_BOARD = 67 + + public var code: Int { rawValue } +} + +public enum IpProtocolTypes: Int, CaseIterable, Sendable { + case NO_IP_PROTOCOL = 0 + case UDP = 1 + case TCP = 2 + + public var code: Int { rawValue } +} + +public enum FilterTypes: Int, CaseIterable, Sendable { + case BUTTERWORTH = 0 + case CHEBYSHEV_TYPE_1 = 1 + case BESSEL = 2 + case BUTTERWORTH_ZERO_PHASE = 3 + case CHEBYSHEV_TYPE_1_ZERO_PHASE = 4 + case BESSEL_ZERO_PHASE = 5 + + public var code: Int { rawValue } +} + +public enum AggOperations: Int, CaseIterable, Sendable { + case MEAN = 0 + case MEDIAN = 1 + case EACH = 2 + + public var code: Int { rawValue } +} + +public enum WindowOperations: Int, CaseIterable, Sendable { + case NO_WINDOW = 0 + case HANNING = 1 + case HAMMING = 2 + case BLACKMAN_HARRIS = 3 + + public var code: Int { rawValue } +} + +public enum DetrendOperations: Int, CaseIterable, Sendable { + case NO_DETREND = 0 + case CONSTANT = 1 + case LINEAR = 2 + + public var code: Int { rawValue } +} + +public enum BrainFlowMetrics: Int, CaseIterable, Sendable { + case MINDFULNESS = 0 + case RESTFULNESS = 1 + case USER_DEFINED = 2 + + public var code: Int { rawValue } +} + +public enum BrainFlowClassifiers: Int, CaseIterable, Sendable { + case DEFAULT_CLASSIFIER = 0 + case DYN_LIB_CLASSIFIER = 1 + case ONNX_CLASSIFIER = 2 + + public var code: Int { rawValue } +} + +public enum BrainFlowPresets: Int, CaseIterable, Sendable { + case DEFAULT_PRESET = 0 + case AUXILIARY_PRESET = 1 + case ANCILLARY_PRESET = 2 + + public var code: Int { rawValue } +} + +public enum LogLevels: Int, CaseIterable, Sendable { + case LEVEL_TRACE = 0 + case LEVEL_DEBUG = 1 + case LEVEL_INFO = 2 + case LEVEL_WARN = 3 + case LEVEL_ERROR = 4 + case LEVEL_CRITICAL = 5 + case LEVEL_OFF = 6 + + public var code: Int { rawValue } +} + +public enum NoiseTypes: Int, CaseIterable, Sendable { + case FIFTY = 0 + case SIXTY = 1 + case FIFTY_AND_SIXTY = 2 + + public var code: Int { rawValue } +} + +public enum WaveletDenoisingTypes: Int, CaseIterable, Sendable { + case VISUSHRINK = 0 + case SURESHRINK = 1 + + public var code: Int { rawValue } +} + +public enum ThresholdTypes: Int, CaseIterable, Sendable { + case SOFT = 0 + case HARD = 1 + + public var code: Int { rawValue } +} + +public enum WaveletExtensionTypes: Int, CaseIterable, Sendable { + case SYMMETRIC = 0 + case PERIODIC = 1 + + public var code: Int { rawValue } +} + +public enum NoiseEstimationLevelTypes: Int, CaseIterable, Sendable { + case FIRST_LEVEL = 0 + case ALL_LEVELS = 1 + + public var code: Int { rawValue } +} + +public enum WaveletTypes: Int, CaseIterable, Sendable { + case HAAR = 0 + case DB1 = 1 + case DB2 = 2 + case DB3 = 3 + case DB4 = 4 + case DB5 = 5 + case DB6 = 6 + case DB7 = 7 + case DB8 = 8 + case DB9 = 9 + case DB10 = 10 + case DB11 = 11 + case DB12 = 12 + case DB13 = 13 + case DB14 = 14 + case DB15 = 15 + case BIOR1_1 = 16 + case BIOR1_3 = 17 + case BIOR1_5 = 18 + case BIOR2_2 = 19 + case BIOR2_4 = 20 + case BIOR2_6 = 21 + case BIOR2_8 = 22 + case BIOR3_1 = 23 + case BIOR3_3 = 24 + case BIOR3_5 = 25 + case BIOR3_7 = 26 + case BIOR3_9 = 27 + case BIOR4_4 = 28 + case BIOR5_5 = 29 + case BIOR6_8 = 30 + case COIF1 = 31 + case COIF2 = 32 + case COIF3 = 33 + case COIF4 = 34 + case COIF5 = 35 + case SYM2 = 36 + case SYM3 = 37 + case SYM4 = 38 + case SYM5 = 39 + case SYM6 = 40 + case SYM7 = 41 + case SYM8 = 42 + case SYM9 = 43 + case SYM10 = 44 + + public var code: Int { rawValue } +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowError.swift b/swift_package/Sources/BrainFlow/BrainFlowError.swift new file mode 100644 index 000000000..867eff305 --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowError.swift @@ -0,0 +1,59 @@ +import Foundation + +public enum BrainFlowExitCodes: Int, CaseIterable, Sendable { + case STATUS_OK = 0 + case PORT_ALREADY_OPEN_ERROR = 1 + case UNABLE_TO_OPEN_PORT_ERROR = 2 + case SET_PORT_ERROR = 3 + case BOARD_WRITE_ERROR = 4 + case INCOMMING_MSG_ERROR = 5 + case INITIAL_MSG_ERROR = 6 + case BOARD_NOT_READY_ERROR = 7 + case STREAM_ALREADY_RUN_ERROR = 8 + case INVALID_BUFFER_SIZE_ERROR = 9 + case STREAM_THREAD_ERROR = 10 + case STREAM_THREAD_IS_NOT_RUNNING = 11 + case EMPTY_BUFFER_ERROR = 12 + case INVALID_ARGUMENTS_ERROR = 13 + case UNSUPPORTED_BOARD_ERROR = 14 + case BOARD_NOT_CREATED_ERROR = 15 + case ANOTHER_BOARD_IS_CREATED_ERROR = 16 + case GENERAL_ERROR = 17 + case SYNC_TIMEOUT_ERROR = 18 + case JSON_NOT_FOUND_ERROR = 19 + case NO_SUCH_DATA_IN_JSON_ERROR = 20 + case CLASSIFIER_IS_NOT_PREPARED_ERROR = 21 + case ANOTHER_CLASSIFIER_IS_PREPARED_ERROR = 22 + case UNSUPPORTED_CLASSIFIER_AND_METRIC_COMBINATION_ERROR = 23 + + public var code: Int { rawValue } +} + +public struct BrainFlowError: Error, LocalizedError, CustomStringConvertible, Sendable { + public let message: String + public let exit_code: Int + + public init(_ message: String, _ exit_code: Int) { + self.message = message + self.exit_code = exit_code + } + + public var errorDescription: String? { + "\(message), exit code: \(exit_code)" + } + + public var description: String { + errorDescription ?? message + } +} + +@inline(__always) +func checkBrainFlowExitCode(_ exitCode: CInt, _ message: String) throws { + guard exitCode == CInt(BrainFlowExitCodes.STATUS_OK.rawValue) else { + throw BrainFlowError(message, Int(exitCode)) + } +} + +func invalidArguments(_ message: String) -> BrainFlowError { + BrainFlowError(message, BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.rawValue) +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowNative.swift b/swift_package/Sources/BrainFlow/BrainFlowNative.swift new file mode 100644 index 000000000..6c06e7a4c --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowNative.swift @@ -0,0 +1,143 @@ +import Foundation + +#if os(Linux) +import Glibc +#else +import Darwin +#endif + +final class NativeLibrary { + private let handle: UnsafeMutableRawPointer + + init(names: [String]) throws { + var errors = [String]() + for path in Self.candidatePaths(for: names) { + if let handle = dlopen(path, Self.openFlags) { + self.handle = handle + return + } + if let error = dlerror().map({ String(cString: $0) }) { + errors.append("\(path): \(error)") + } + } + throw BrainFlowError( + "Unable to load BrainFlow native library. Set BRAINFLOW_LIB_DIR or build native libs into installed/lib. Tried: \(errors.joined(separator: "; "))", + BrainFlowExitCodes.GENERAL_ERROR.rawValue + ) + } + + deinit { + dlclose(handle) + } + + func symbol(_ name: String, as type: T.Type) throws -> T { + guard let pointer = dlsym(handle, name) else { + let message = dlerror().map { String(cString: $0) } ?? "symbol not found" + throw BrainFlowError("Unable to load symbol \(name): \(message)", BrainFlowExitCodes.GENERAL_ERROR.rawValue) + } + return unsafeBitCast(pointer, to: type) + } + + private static var openFlags: Int32 { + return RTLD_NOW | RTLD_GLOBAL + } + + private static func candidatePaths(for names: [String]) -> [String] { + var dirs = [String]() + let env = ProcessInfo.processInfo.environment + + if let explicit = env["BRAINFLOW_LIB_DIR"], !explicit.isEmpty { + dirs.append(explicit) + } + dirs.append(contentsOf: splitPathList(env["DYLD_LIBRARY_PATH"])) + dirs.append(contentsOf: splitPathList(env["LD_LIBRARY_PATH"])) + + let cwd = FileManager.default.currentDirectoryPath + dirs.append(cwd) + dirs.append("\(cwd)/installed/lib") + dirs.append("\(cwd)/../installed/lib") + dirs.append("\(cwd)/../../installed/lib") + dirs.append("\(cwd)/lib") + + #if os(macOS) || os(iOS) + if let privateFrameworksPath = Bundle.main.privateFrameworksPath { + dirs.append(privateFrameworksPath) + } + if let resourcePath = Bundle.main.resourcePath { + dirs.append(resourcePath) + dirs.append("\(resourcePath)/lib") + dirs.append("\(resourcePath)/Frameworks") + } + #endif + + var candidates = [String]() + for dir in unique(dirs) { + for name in names { + candidates.append((dir as NSString).appendingPathComponent(name)) + } + } + 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: NativeLibrary? + + init(names: [String]) { + self.names = names + } + + func load() throws -> NativeLibrary { + lock.lock() + defer { lock.unlock() } + if let storage { + return storage + } + let library = try NativeLibrary(names: names) + storage = library + return library + } +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowParams.swift b/swift_package/Sources/BrainFlow/BrainFlowParams.swift new file mode 100644 index 000000000..e33082859 --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowParams.swift @@ -0,0 +1,107 @@ +import Foundation + +public struct BrainFlowInputParams: Codable, Equatable, Sendable { + public var serial_port: String + public var mac_address: String + public var ip_address: String + public var ip_address_aux: String + public var ip_address_anc: String + public var ip_port: Int + public var ip_port_aux: Int + public var ip_port_anc: Int + public var ip_protocol: Int + public var other_info: String + public var timeout: Int + public var serial_number: String + public var file: String + public var file_aux: String + public var file_anc: String + public var master_board: Int + + public init() { + serial_port = "" + mac_address = "" + ip_address = "" + ip_address_aux = "" + ip_address_anc = "" + ip_port = 0 + ip_port_aux = 0 + ip_port_anc = 0 + ip_protocol = IpProtocolTypes.NO_IP_PROTOCOL.rawValue + other_info = "" + timeout = 0 + serial_number = "" + file = "" + file_aux = "" + file_anc = "" + master_board = BoardIds.NO_BOARD.rawValue + } + + public mutating func set_ip_protocol(_ ip_protocol: IpProtocolTypes) { + self.ip_protocol = ip_protocol.rawValue + } + + public mutating func set_master_board(_ board: BoardIds) { + master_board = board.rawValue + } + + public func to_json() throws -> String { + try Self.encoder.encodeString(self) + } + + private static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + return encoder + }() +} + +public struct BrainFlowModelParams: Codable, Equatable, Sendable { + public var metric: Int + public var classifier: Int + public var file: String + public var other_info: String + public var output_name: String + public var max_array_size: Int + + public init(metric: Int, classifier: Int) { + self.metric = metric + self.classifier = classifier + file = "" + other_info = "" + output_name = "" + max_array_size = 8192 + } + + public init(metric: BrainFlowMetrics, classifier: BrainFlowClassifiers) { + self.init(metric: metric.rawValue, classifier: classifier.rawValue) + } + + public mutating func set_metric(_ metric: BrainFlowMetrics) { + self.metric = metric.rawValue + } + + public mutating func set_classifier(_ classifier: BrainFlowClassifiers) { + self.classifier = classifier.rawValue + } + + public func to_json() throws -> String { + try Self.encoder.encodeString(self) + } + + private static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + return encoder + }() +} + +private extension JSONEncoder { + func encodeString(_ value: T) throws -> String { + let data = try encode(value) + guard let string = String(data: data, encoding: .utf8) else { + throw invalidArguments("Unable to encode JSON as UTF-8") + } + return string + } +} diff --git a/swift_package/Sources/BrainFlow/BrainFlowTypes.swift b/swift_package/Sources/BrainFlow/BrainFlowTypes.swift new file mode 100644 index 000000000..0ac58943c --- /dev/null +++ b/swift_package/Sources/BrainFlow/BrainFlowTypes.swift @@ -0,0 +1,106 @@ +import Foundation + +public struct Complex: Equatable, Sendable { + public var real: Double + public var imag: Double + + public init(real: Double, imag: Double) { + self.real = real + self.imag = imag + } +} + +public struct WaveletTransform: Equatable, Sendable { + public var coefficients: [Double] + public var decomposition_lengths: [Int] + + public init(coefficients: [Double], decomposition_lengths: [Int]) { + self.coefficients = coefficients + self.decomposition_lengths = decomposition_lengths + } +} + +public struct PSD: Equatable, Sendable { + public var ampl: [Double] + public var freq: [Double] + + public init(ampl: [Double], freq: [Double]) { + self.ampl = ampl + self.freq = freq + } +} + +public struct BandPowerResult: Equatable, Sendable { + public var average: [Double] + public var stddev: [Double] + + public init(average: [Double], stddev: [Double]) { + self.average = average + self.stddev = stddev + } +} + +public struct CSPResult: Equatable, Sendable { + public var filters: [[Double]] + public var eigenvalues: [Double] + + public init(filters: [[Double]], eigenvalues: [Double]) { + self.filters = filters + self.eigenvalues = eigenvalues + } +} + +public struct ICAResult: Equatable, Sendable { + public var w: [[Double]] + public var k: [[Double]] + public var a: [[Double]] + public var s: [[Double]] + + public init(w: [[Double]], k: [[Double]], a: [[Double]], s: [[Double]]) { + self.w = w + self.k = k + self.a = a + self.s = s + } +} + +public struct FrequencyBand: Equatable, Sendable { + public var start: Double + public var stop: Double + + public init(start: Double, stop: Double) { + self.start = start + self.stop = stop + } +} + +enum BrainFlowArray { + static func reshape_data_to_1d(num_rows: Int, num_cols: Int, buf: [[Double]]) -> [Double] { + var output = [Double](repeating: 0.0, count: num_rows * num_cols) + for col in 0.. [[Double]] { + guard num_rows > 0, num_cols > 0 else { return [] } + return (0.. (rows: Int, cols: Int) { + guard let first = data.first else { + throw invalidArguments("Data array is empty") + } + let cols = first.count + guard cols > 0, data.allSatisfy({ $0.count == cols }) else { + throw invalidArguments("Data array must be rectangular") + } + return (data.count, cols) + } +} diff --git a/swift_package/Sources/BrainFlow/DataFilter.swift b/swift_package/Sources/BrainFlow/DataFilter.swift new file mode 100644 index 000000000..ec21ef5a9 --- /dev/null +++ b/swift_package/Sources/BrainFlow/DataFilter.swift @@ -0,0 +1,816 @@ +import Foundation + +public enum DataFilter { + public static func set_log_level(_ log_level: Int) throws { + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.set_log_level_data_handler(CInt(log_level)), "Error in set_log_level") + } + } + + public static func set_log_level(_ log_level: LogLevels) throws { + try set_log_level(log_level.rawValue) + } + + public static func enable_data_logger() throws { + try set_log_level(LogLevels.LEVEL_INFO) + } + + public static func enable_dev_data_logger() throws { + try set_log_level(LogLevels.LEVEL_TRACE) + } + + public static func disable_data_logger() throws { + try set_log_level(LogLevels.LEVEL_OFF) + } + + public static func set_log_file(_ log_file: String) throws { + try log_file.withCString { path in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.set_log_file_data_handler(path), "Error in set_log_file") + } + } + } + + public static func log_message(_ log_level: Int, message: String) throws { + var mutableMessage = Array(message.utf8CString) + try mutableMessage.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.log_message_data_handler(CInt(log_level), pointer.baseAddress), "Error in log_message") + } + } + } + + public static func get_version() throws -> String { + try getVersion(function: \.get_version_data_handler) + } + + public static func perform_lowpass( + data: inout [Double], + sampling_rate: Int, + cutoff: Double, + order: Int, + filter_type: Int, + ripple: Double + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_lowpass(pointer, CInt(count), CInt(sampling_rate), cutoff, CInt(order), CInt(filter_type), ripple), "Failed to perform lowpass") + } + } + } + + public static func perform_lowpass( + data: inout [Double], + sampling_rate: Int, + cutoff: Double, + order: Int, + filter_type: FilterTypes, + ripple: Double + ) throws { + try perform_lowpass(data: &data, sampling_rate: sampling_rate, cutoff: cutoff, order: order, filter_type: filter_type.rawValue, ripple: ripple) + } + + public static func perform_highpass( + data: inout [Double], + sampling_rate: Int, + cutoff: Double, + order: Int, + filter_type: Int, + ripple: Double + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_highpass(pointer, CInt(count), CInt(sampling_rate), cutoff, CInt(order), CInt(filter_type), ripple), "Failed to perform highpass") + } + } + } + + public static func perform_highpass( + data: inout [Double], + sampling_rate: Int, + cutoff: Double, + order: Int, + filter_type: FilterTypes, + ripple: Double + ) throws { + try perform_highpass(data: &data, sampling_rate: sampling_rate, cutoff: cutoff, order: order, filter_type: filter_type.rawValue, ripple: ripple) + } + + public static func perform_bandpass( + data: inout [Double], + sampling_rate: Int, + start_freq: Double, + stop_freq: Double, + order: Int, + filter_type: Int, + ripple: Double + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_bandpass(pointer, CInt(count), CInt(sampling_rate), start_freq, stop_freq, CInt(order), CInt(filter_type), ripple), "Failed to perform bandpass") + } + } + } + + public static func perform_bandpass( + data: inout [Double], + sampling_rate: Int, + start_freq: Double, + stop_freq: Double, + order: Int, + filter_type: FilterTypes, + ripple: Double + ) throws { + try perform_bandpass(data: &data, sampling_rate: sampling_rate, start_freq: start_freq, stop_freq: stop_freq, order: order, filter_type: filter_type.rawValue, ripple: ripple) + } + + public static func perform_bandstop( + data: inout [Double], + sampling_rate: Int, + start_freq: Double, + stop_freq: Double, + order: Int, + filter_type: Int, + ripple: Double + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_bandstop(pointer, CInt(count), CInt(sampling_rate), start_freq, stop_freq, CInt(order), CInt(filter_type), ripple), "Failed to perform bandstop") + } + } + } + + public static func perform_bandstop( + data: inout [Double], + sampling_rate: Int, + start_freq: Double, + stop_freq: Double, + order: Int, + filter_type: FilterTypes, + ripple: Double + ) throws { + try perform_bandstop(data: &data, sampling_rate: sampling_rate, start_freq: start_freq, stop_freq: stop_freq, order: order, filter_type: filter_type.rawValue, ripple: ripple) + } + + public static func remove_environmental_noise(data: inout [Double], sampling_rate: Int, noise_type: Int) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.remove_environmental_noise(pointer, CInt(count), CInt(sampling_rate), CInt(noise_type)), "Failed to remove environmental noise") + } + } + } + + public static func remove_environmental_noise(data: inout [Double], sampling_rate: Int, noise_type: NoiseTypes) throws { + try remove_environmental_noise(data: &data, sampling_rate: sampling_rate, noise_type: noise_type.rawValue) + } + + public static func perform_rolling_filter(data: inout [Double], period: Int, operation: Int) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_rolling_filter(pointer, CInt(count), CInt(period), CInt(operation)), "Failed to perform rolling filter") + } + } + } + + public static func perform_rolling_filter(data: inout [Double], period: Int, operation: AggOperations) throws { + try perform_rolling_filter(data: &data, period: period, operation: operation.rawValue) + } + + public static func detrend(data: inout [Double], detrend_operation: Int) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.detrend(pointer, CInt(count), CInt(detrend_operation)), "Failed to detrend data") + } + } + } + + public static func detrend(data: inout [Double], detrend_operation: DetrendOperations) throws { + try detrend(data: &data, detrend_operation: detrend_operation.rawValue) + } + + public static func perform_downsampling(data: [Double], period: Int, operation: Int) throws -> [Double] { + guard period > 0, data.count / period > 0 else { throw invalidArguments("Invalid period or data size") } + var input = data + var output = [Double](repeating: 0.0, count: data.count / period) + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_downsampling(inputPtr.baseAddress, CInt(data.count), CInt(period), CInt(operation), outputPtr.baseAddress), "Failed to perform downsampling") + } + } + } + return output + } + + public static func perform_downsampling(data: [Double], period: Int, operation: AggOperations) throws -> [Double] { + try perform_downsampling(data: data, period: period, operation: operation.rawValue) + } + + public static func perform_wavelet_transform( + data: [Double], + wavelet: Int, + decomposition_level: Int, + extension_type: Int + ) throws -> WaveletTransform { + guard decomposition_level > 0 else { throw invalidArguments("Invalid decomposition level") } + var input = data + var output = [Double](repeating: 0.0, count: data.count + 2 * decomposition_level * 41) + var lengths = [CInt](repeating: 0, count: decomposition_level + 1) + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try lengths.withUnsafeMutableBufferPointer { lengthsPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_wavelet_transform(inputPtr.baseAddress, CInt(data.count), CInt(wavelet), CInt(decomposition_level), CInt(extension_type), outputPtr.baseAddress, lengthsPtr.baseAddress), "Failed to perform wavelet transform") + } + } + } + } + let swiftLengths = lengths.map(Int.init) + return WaveletTransform(coefficients: Array(output.prefix(swiftLengths.reduce(0, +))), decomposition_lengths: swiftLengths) + } + + public static func perform_wavelet_transform( + data: [Double], + wavelet: WaveletTypes, + decomposition_level: Int, + extension_type: WaveletExtensionTypes + ) throws -> WaveletTransform { + try perform_wavelet_transform(data: data, wavelet: wavelet.rawValue, decomposition_level: decomposition_level, extension_type: extension_type.rawValue) + } + + public static func perform_inverse_wavelet_transform( + wavelet_output: WaveletTransform, + original_data_len: Int, + wavelet: Int, + decomposition_level: Int, + extension_type: Int + ) throws -> [Double] { + var coeffs = wavelet_output.coefficients + var lengths = wavelet_output.decomposition_lengths.map(CInt.init) + var output = [Double](repeating: 0.0, count: original_data_len) + try coeffs.withUnsafeMutableBufferPointer { coeffsPtr in + try lengths.withUnsafeMutableBufferPointer { lengthsPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_inverse_wavelet_transform(coeffsPtr.baseAddress, CInt(original_data_len), CInt(wavelet), CInt(decomposition_level), CInt(extension_type), lengthsPtr.baseAddress, outputPtr.baseAddress), "Failed to perform inverse wavelet transform") + } + } + } + } + return output + } + + public static func perform_inverse_wavelet_transform( + wavelet_output: WaveletTransform, + original_data_len: Int, + wavelet: WaveletTypes, + decomposition_level: Int, + extension_type: WaveletExtensionTypes + ) throws -> [Double] { + try perform_inverse_wavelet_transform(wavelet_output: wavelet_output, original_data_len: original_data_len, wavelet: wavelet.rawValue, decomposition_level: decomposition_level, extension_type: extension_type.rawValue) + } + + public static func perform_wavelet_denoising( + data: inout [Double], + wavelet: Int, + decomposition_level: Int, + wavelet_denoising: Int, + threshold: Int, + extension_type: Int, + noise_level: Int + ) throws { + try withMutableData(&data) { pointer, count in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_wavelet_denoising(pointer, CInt(count), CInt(wavelet), CInt(decomposition_level), CInt(wavelet_denoising), CInt(threshold), CInt(extension_type), CInt(noise_level)), "Failed to perform wavelet denoising") + } + } + } + + public static func perform_wavelet_denoising( + data: inout [Double], + wavelet: WaveletTypes, + decomposition_level: Int, + wavelet_denoising: WaveletDenoisingTypes, + threshold: ThresholdTypes, + extension_type: WaveletExtensionTypes, + noise_level: NoiseEstimationLevelTypes + ) throws { + try perform_wavelet_denoising(data: &data, wavelet: wavelet.rawValue, decomposition_level: decomposition_level, wavelet_denoising: wavelet_denoising.rawValue, threshold: threshold.rawValue, extension_type: extension_type.rawValue, noise_level: noise_level.rawValue) + } + + public static func restore_data_from_wavelet_detailed_coeffs( + data: [Double], + wavelet: Int, + decomposition_level: Int, + level_to_restore: Int + ) throws -> [Double] { + var input = data + var output = [Double](repeating: 0.0, count: data.count) + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.restore_data_from_wavelet_detailed_coeffs(inputPtr.baseAddress, CInt(data.count), CInt(wavelet), CInt(decomposition_level), CInt(level_to_restore), outputPtr.baseAddress), "Failed to restore wavelet detailed coeffs") + } + } + } + return output + } + + public static func restore_data_from_wavelet_detailed_coeffs( + data: [Double], + wavelet: WaveletTypes, + decomposition_level: Int, + level_to_restore: Int + ) throws -> [Double] { + try restore_data_from_wavelet_detailed_coeffs(data: data, wavelet: wavelet.rawValue, decomposition_level: decomposition_level, level_to_restore: level_to_restore) + } + + public static func detect_peaks_z_score(data: [Double], lag: Int = 5, threshold: Double = 3.5, influence: Double = 0.1) throws -> [Double] { + var input = data + var output = [Double](repeating: 0.0, count: data.count) + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.detect_peaks_z_score(inputPtr.baseAddress, CInt(data.count), CInt(lag), threshold, influence, outputPtr.baseAddress), "Failed to detect peaks") + } + } + } + return output + } + + public static func get_csp(data: [[[Double]]], labels: [Double]) throws -> CSPResult { + guard let firstEpoch = data.first, let firstChannel = firstEpoch.first, !firstChannel.isEmpty else { throw invalidArguments("Invalid CSP data") } + let nEpochs = data.count + let nChannels = firstEpoch.count + let nTimes = firstChannel.count + guard labels.count == nEpochs else { throw invalidArguments("labels count must match epoch count") } + guard data.allSatisfy({ epoch in + epoch.count == nChannels && epoch.allSatisfy { $0.count == nTimes } + }) else { + throw invalidArguments("CSP data must be rectangular") + } + var flattened = [Double]() + flattened.reserveCapacity(nEpochs * nChannels * nTimes) + for epoch in data { + for channel in epoch { + flattened.append(contentsOf: channel) + } + } + var mutableLabels = labels + var filters = [Double](repeating: 0.0, count: nChannels * nChannels) + var eigenvalues = [Double](repeating: 0.0, count: nChannels) + try flattened.withUnsafeMutableBufferPointer { dataPtr in + try mutableLabels.withUnsafeMutableBufferPointer { labelsPtr in + try filters.withUnsafeMutableBufferPointer { filtersPtr in + try eigenvalues.withUnsafeMutableBufferPointer { eigenPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_csp(dataPtr.baseAddress, labelsPtr.baseAddress, CInt(nEpochs), CInt(nChannels), CInt(nTimes), filtersPtr.baseAddress, eigenPtr.baseAddress), "Failed to get CSP") + } + } + } + } + } + return CSPResult(filters: BrainFlowArray.reshape_data_to_2d(num_rows: nChannels, num_cols: nChannels, linear_buffer: filters), eigenvalues: eigenvalues) + } + + public static func get_window(window_function: Int, window_len: Int) throws -> [Double] { + guard window_len > 0 else { throw invalidArguments("window_len must be positive") } + var output = [Double](repeating: 0.0, count: window_len) + try output.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_window(CInt(window_function), CInt(window_len), pointer.baseAddress), "Failed to get window") + } + } + return output + } + + public static func get_window(window_function: WindowOperations, window_len: Int) throws -> [Double] { + try get_window(window_function: window_function.rawValue, window_len: window_len) + } + + public static func perform_fft(data: [Double], start_pos: Int, end_pos: Int, window: Int) throws -> [Complex] { + guard start_pos >= 0, end_pos <= data.count, start_pos < end_pos else { throw invalidArguments("Invalid position arguments") } + var input = Array(data[start_pos.. [Complex] { + try perform_fft(data: data, start_pos: start_pos, end_pos: end_pos, window: window.rawValue) + } + + public static func perform_fft(data: [Double], window: Int) throws -> [Complex] { + try perform_fft(data: data, start_pos: 0, end_pos: data.count, window: window) + } + + public static func perform_fft(data: [Double], window: WindowOperations) throws -> [Complex] { + try perform_fft(data: data, start_pos: 0, end_pos: data.count, window: window.rawValue) + } + + public static func perform_ifft(data: [Complex]) throws -> [Double] { + guard data.count >= 2 else { throw invalidArguments("FFT data must contain at least two bins") } + var real = data.map(\.real) + var imag = data.map(\.imag) + let restoredLength = (data.count - 1) * 2 + var output = [Double](repeating: 0.0, count: restoredLength) + try real.withUnsafeMutableBufferPointer { realPtr in + try imag.withUnsafeMutableBufferPointer { imagPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_ifft(realPtr.baseAddress, imagPtr.baseAddress, CInt(restoredLength), outputPtr.baseAddress), "Failed to perform IFFT") + } + } + } + } + return output + } + + public static func get_psd(data: [Double], start_pos: Int, end_pos: Int, sampling_rate: Int, window: Int) throws -> PSD { + guard start_pos >= 0, end_pos <= data.count, start_pos < end_pos else { throw invalidArguments("Invalid position arguments") } + var input = Array(data[start_pos.. PSD { + try get_psd(data: data, start_pos: start_pos, end_pos: end_pos, sampling_rate: sampling_rate, window: window.rawValue) + } + + public static func get_psd(data: [Double], sampling_rate: Int, window: Int) throws -> PSD { + try get_psd(data: data, start_pos: 0, end_pos: data.count, sampling_rate: sampling_rate, window: window) + } + + public static func get_psd(data: [Double], sampling_rate: Int, window: WindowOperations) throws -> PSD { + try get_psd(data: data, start_pos: 0, end_pos: data.count, sampling_rate: sampling_rate, window: window.rawValue) + } + + public static func get_psd_welch(data: [Double], nfft: Int, overlap: Int, sampling_rate: Int, window: Int) throws -> PSD { + guard nfft > 0, nfft & (nfft - 1) == 0 else { throw invalidArguments("nfft must be a positive power of two") } + guard data.count >= nfft else { throw invalidArguments("nfft must be less than or equal to data count") } + guard overlap >= 0, overlap < nfft else { throw invalidArguments("overlap must be non-negative and less than nfft") } + guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") } + var input = data + var ampl = [Double](repeating: 0.0, count: nfft / 2 + 1) + var freq = [Double](repeating: 0.0, count: nfft / 2 + 1) + try input.withUnsafeMutableBufferPointer { inputPtr in + try ampl.withUnsafeMutableBufferPointer { amplPtr in + try freq.withUnsafeMutableBufferPointer { freqPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_psd_welch(inputPtr.baseAddress, CInt(data.count), CInt(nfft), CInt(overlap), CInt(sampling_rate), CInt(window), amplPtr.baseAddress, freqPtr.baseAddress), "Failed to get PSD Welch") + } + } + } + } + return PSD(ampl: ampl, freq: freq) + } + + public static func get_psd_welch(data: [Double], nfft: Int, overlap: Int, sampling_rate: Int, window: WindowOperations) throws -> PSD { + try get_psd_welch(data: data, nfft: nfft, overlap: overlap, sampling_rate: sampling_rate, window: window.rawValue) + } + + public static func get_band_power(psd: PSD, freq_start: Double, freq_end: Double) throws -> Double { + guard !psd.ampl.isEmpty, psd.ampl.count == psd.freq.count else { throw invalidArguments("PSD arrays must be non-empty and have equal lengths") } + guard freq_start < freq_end else { throw invalidArguments("freq_start must be less than freq_end") } + var ampl = psd.ampl + var freq = psd.freq + var output = 0.0 + try ampl.withUnsafeMutableBufferPointer { amplPtr in + try freq.withUnsafeMutableBufferPointer { freqPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_band_power(amplPtr.baseAddress, freqPtr.baseAddress, CInt(psd.ampl.count), freq_start, freq_end, &output), "Failed to get band power") + } + } + } + return output + } + + public static func get_avg_band_powers(data: [[Double]], channels: [Int], sampling_rate: Int, apply_filter: Bool) throws -> BandPowerResult { + let defaultBands = [ + FrequencyBand(start: 2.0, stop: 4.0), + FrequencyBand(start: 4.0, stop: 8.0), + FrequencyBand(start: 8.0, stop: 13.0), + FrequencyBand(start: 13.0, stop: 30.0), + FrequencyBand(start: 30.0, stop: 45.0) + ] + return try get_custom_band_powers(data: data, bands: defaultBands, channels: channels, sampling_rate: sampling_rate, apply_filter: apply_filter) + } + + public static func get_custom_band_powers( + data: [[Double]], + bands: [FrequencyBand], + channels: [Int], + sampling_rate: Int, + apply_filter: Bool + ) throws -> BandPowerResult { + let (rows, cols) = try BrainFlowArray.validateRectangular(data) + guard !channels.isEmpty, !bands.isEmpty else { throw invalidArguments("Channels and bands must be non-empty") } + guard channels.allSatisfy({ $0 >= 0 && $0 < rows }) else { throw invalidArguments("Channel index is out of range") } + guard bands.allSatisfy({ $0.start < $0.stop }) else { throw invalidArguments("Band start frequency must be less than stop frequency") } + guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") } + var selected = [Double]() + selected.reserveCapacity(channels.count * cols) + for channel in channels { + selected.append(contentsOf: data[channel]) + } + var starts = bands.map(\.start) + var stops = bands.map(\.stop) + var avg = [Double](repeating: 0.0, count: bands.count) + var stddev = [Double](repeating: 0.0, count: bands.count) + try selected.withUnsafeMutableBufferPointer { dataPtr in + try starts.withUnsafeMutableBufferPointer { startsPtr in + try stops.withUnsafeMutableBufferPointer { stopsPtr in + try avg.withUnsafeMutableBufferPointer { avgPtr in + try stddev.withUnsafeMutableBufferPointer { stddevPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_custom_band_powers(dataPtr.baseAddress, CInt(channels.count), CInt(cols), startsPtr.baseAddress, stopsPtr.baseAddress, CInt(bands.count), CInt(sampling_rate), apply_filter ? 1 : 0, avgPtr.baseAddress, stddevPtr.baseAddress), "Failed to get custom band powers") + } + } + } + } + } + } + return BandPowerResult(average: avg, stddev: stddev) + } + + public static func perform_ica(data: [[Double]], num_components: Int, channels: [Int]? = nil) throws -> ICAResult { + let (rows, cols) = try BrainFlowArray.validateRectangular(data) + let selectedChannels = channels ?? Array(0..= 0 && $0 < rows }) else { throw invalidArguments("Channel index is out of range") } + guard cols >= 2 else { throw invalidArguments("ICA data must contain at least two samples") } + guard selectedChannels.count >= 2 else { throw invalidArguments("ICA requires at least two channels") } + guard num_components >= 2, num_components <= selectedChannels.count else { throw invalidArguments("num_components must be between 2 and the selected channel count") } + var selected = [Double]() + selected.reserveCapacity(selectedChannels.count * cols) + for channel in selectedChannels { + selected.append(contentsOf: data[channel]) + } + var w = [Double](repeating: 0.0, count: num_components * num_components) + var k = [Double](repeating: 0.0, count: selectedChannels.count * num_components) + var a = [Double](repeating: 0.0, count: num_components * selectedChannels.count) + var s = [Double](repeating: 0.0, count: cols * num_components) + try selected.withUnsafeMutableBufferPointer { dataPtr in + try w.withUnsafeMutableBufferPointer { wPtr in + try k.withUnsafeMutableBufferPointer { kPtr in + try a.withUnsafeMutableBufferPointer { aPtr in + try s.withUnsafeMutableBufferPointer { sPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.perform_ica(dataPtr.baseAddress, CInt(selectedChannels.count), CInt(cols), CInt(num_components), wPtr.baseAddress, kPtr.baseAddress, aPtr.baseAddress, sPtr.baseAddress), "Failed to perform ICA") + } + } + } + } + } + } + return ICAResult( + w: BrainFlowArray.reshape_data_to_2d(num_rows: num_components, num_cols: num_components, linear_buffer: w), + k: BrainFlowArray.reshape_data_to_2d(num_rows: num_components, num_cols: selectedChannels.count, linear_buffer: k), + a: BrainFlowArray.reshape_data_to_2d(num_rows: selectedChannels.count, num_cols: num_components, linear_buffer: a), + s: BrainFlowArray.reshape_data_to_2d(num_rows: num_components, num_cols: cols, linear_buffer: s) + ) + } + + public static func calc_stddev(data: [Double], start_pos: Int? = nil, end_pos: Int? = nil) throws -> Double { + var input = data + let start = start_pos ?? 0 + let end = end_pos ?? data.count + guard start >= 0, end <= data.count, start < end else { throw invalidArguments("Invalid position arguments") } + var output = 0.0 + try input.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.calc_stddev(pointer.baseAddress, CInt(start), CInt(end), &output), "Failed to calc stddev") + } + } + return output + } + + public static func get_railed_percentage(data: [Double], gain: Int) throws -> Double { + guard !data.isEmpty else { throw invalidArguments("data must be non-empty") } + var input = data + var output = 0.0 + try input.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_railed_percentage(pointer.baseAddress, CInt(data.count), CInt(gain), &output), "Failed to get railed percentage") + } + } + return output + } + + public static func get_oxygen_level(ppg_ir: [Double], ppg_red: [Double], sampling_rate: Int, coef1: Double = 1.5958422, coef2: Double = -34.6596622, coef3: Double = 112.6898759) throws -> Double { + guard !ppg_ir.isEmpty, ppg_ir.count == ppg_red.count else { throw invalidArguments("PPG arrays must be non-empty and have equal lengths") } + guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") } + var ir = ppg_ir + var red = ppg_red + var output = 0.0 + try ir.withUnsafeMutableBufferPointer { irPtr in + try red.withUnsafeMutableBufferPointer { redPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_oxygen_level(irPtr.baseAddress, redPtr.baseAddress, CInt(ppg_ir.count), CInt(sampling_rate), coef1, coef2, coef3, &output), "Failed to get oxygen level") + } + } + } + return output + } + + public static func get_heart_rate(ppg_ir: [Double], ppg_red: [Double], sampling_rate: Int, fft_size: Int) throws -> Double { + guard !ppg_ir.isEmpty, ppg_ir.count == ppg_red.count else { throw invalidArguments("PPG arrays must be non-empty and have equal lengths") } + guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") } + guard fft_size >= 1024, fft_size % 2 == 0 else { throw invalidArguments("fft_size must be even and at least 1024") } + var ir = ppg_ir + var red = ppg_red + var output = 0.0 + try ir.withUnsafeMutableBufferPointer { irPtr in + try red.withUnsafeMutableBufferPointer { redPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_heart_rate(irPtr.baseAddress, redPtr.baseAddress, CInt(ppg_ir.count), CInt(sampling_rate), CInt(fft_size), &output), "Failed to get heart rate") + } + } + } + return output + } + + public static func get_nearest_power_of_two(_ value: Int) throws -> Int { + var output: CInt = 0 + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_nearest_power_of_two(CInt(value), &output), "Failed to get nearest power of two") + } + return Int(output) + } + + public static func write_file(data: [[Double]], file_name: String, file_mode: String) throws { + let (rows, cols) = try BrainFlowArray.validateRectangular(data) + var linear = reshape_data_to_1d(num_rows: rows, num_cols: cols, buf: data) + try file_name.withCString { fileNamePtr in + try file_mode.withCString { fileModePtr in + try linear.withUnsafeMutableBufferPointer { linearPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.write_file(linearPtr.baseAddress, CInt(rows), CInt(cols), fileNamePtr, fileModePtr), "Failed to write file") + } + } + } + } + } + + public static func read_file(_ file_name: String) throws -> [[Double]] { + var elements: CInt = 0 + try file_name.withCString { fileNamePtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.get_num_elements_in_file(fileNamePtr, &elements), "Failed to determine number of file elements") + } + } + guard elements >= 0 else { throw invalidArguments("File element count must be non-negative") } + var data = [Double](repeating: 0.0, count: Int(elements)) + var rows: CInt = 0 + var cols: CInt = 0 + try file_name.withCString { fileNamePtr in + try data.withUnsafeMutableBufferPointer { dataPtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native.read_file(dataPtr.baseAddress, &rows, &cols, fileNamePtr, elements), "Failed to read file") + } + } + } + return BrainFlowArray.reshape_data_to_2d(num_rows: Int(rows), num_cols: Int(cols), linear_buffer: data) + } + + public static func reshape_data_to_1d(num_rows: Int, num_cols: Int, buf: [[Double]]) -> [Double] { + BrainFlowArray.reshape_data_to_1d(num_rows: num_rows, num_cols: num_cols, buf: buf) + } + + public static func reshape_data_to_2d(num_rows: Int, num_cols: Int, linear_buffer: [Double]) -> [[Double]] { + BrainFlowArray.reshape_data_to_2d(num_rows: num_rows, num_cols: num_cols, linear_buffer: linear_buffer) + } + + private static func withMutableData(_ data: inout [Double], _ body: (UnsafeMutablePointer?, Int) throws -> T) throws -> T { + try data.withUnsafeMutableBufferPointer { pointer in + try body(pointer.baseAddress, pointer.count) + } + } + + private static func getVersion(function: KeyPath) throws -> String { + let maxLength = 64 + var bytes = [CChar](repeating: 0, count: maxLength) + var length: CInt = 0 + try bytes.withUnsafeMutableBufferPointer { pointer in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode(native[keyPath: function](pointer.baseAddress, &length, CInt(maxLength)), "Error in get_version") + } + } + return String(bytes: bytes.prefix(Int(length)).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } +} + +final class DataFilterNative { + typealias VersionFunction = @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt) -> CInt + + let perform_lowpass: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, CInt, CInt, Double) -> CInt + let perform_highpass: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, CInt, CInt, Double) -> CInt + let perform_bandpass: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, Double, CInt, CInt, Double) -> CInt + let perform_bandstop: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, Double, CInt, CInt, Double) -> CInt + let remove_environmental_noise: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt) -> CInt + let perform_rolling_filter: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt) -> CInt + let perform_downsampling: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?) -> CInt + let perform_wavelet_transform: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let perform_inverse_wavelet_transform: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let perform_wavelet_denoising: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, CInt, CInt, CInt) -> CInt + let get_csp: @convention(c) (UnsafePointer?, UnsafePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let get_window: @convention(c) (CInt, CInt, UnsafeMutablePointer?) -> CInt + let perform_fft: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let perform_ifft: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, UnsafeMutablePointer?) -> CInt + let get_nearest_power_of_two: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt + let get_psd: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let detrend: @convention(c) (UnsafeMutablePointer?, CInt, CInt) -> CInt + let calc_stddev: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?) -> CInt + let get_psd_welch: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let get_band_power: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, Double, Double, UnsafeMutablePointer?) -> CInt + let get_custom_band_powers: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let get_railed_percentage: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?) -> CInt + let get_oxygen_level: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, CInt, Double, Double, Double, UnsafeMutablePointer?) -> CInt + let get_heart_rate: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?) -> CInt + let restore_data_from_wavelet_detailed_coeffs: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, CInt, UnsafeMutablePointer?) -> CInt + let detect_peaks_z_score: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, Double, UnsafeMutablePointer?) -> CInt + let perform_ica: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt + let set_log_level_data_handler: @convention(c) (CInt) -> CInt + let set_log_file_data_handler: @convention(c) (UnsafePointer?) -> CInt + let log_message_data_handler: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt + let write_file: @convention(c) (UnsafePointer?, CInt, CInt, UnsafePointer?, UnsafePointer?) -> CInt + let read_file: @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafePointer?, CInt) -> CInt + let get_num_elements_in_file: @convention(c) (UnsafePointer?, UnsafeMutablePointer?) -> CInt + let get_version_data_handler: VersionFunction + + private static let lock = NSLock() + private static var cached: DataFilterNative? + + static func withData(_ body: (DataFilterNative) throws -> T) throws -> T { + try body(load()) + } + + private static func load() throws -> DataFilterNative { + lock.lock() + defer { lock.unlock() } + if let cached { return cached } + let value = try DataFilterNative(library: NativeLibraries.dataHandler.load()) + cached = value + return value + } + + private init(library: NativeLibrary) throws { + perform_lowpass = try library.symbol("perform_lowpass", as: type(of: perform_lowpass)) + perform_highpass = try library.symbol("perform_highpass", as: type(of: perform_highpass)) + perform_bandpass = try library.symbol("perform_bandpass", as: type(of: perform_bandpass)) + perform_bandstop = try library.symbol("perform_bandstop", as: type(of: perform_bandstop)) + remove_environmental_noise = try library.symbol("remove_environmental_noise", as: type(of: remove_environmental_noise)) + perform_rolling_filter = try library.symbol("perform_rolling_filter", as: type(of: perform_rolling_filter)) + perform_downsampling = try library.symbol("perform_downsampling", as: type(of: perform_downsampling)) + perform_wavelet_transform = try library.symbol("perform_wavelet_transform", as: type(of: perform_wavelet_transform)) + perform_inverse_wavelet_transform = try library.symbol("perform_inverse_wavelet_transform", as: type(of: perform_inverse_wavelet_transform)) + perform_wavelet_denoising = try library.symbol("perform_wavelet_denoising", as: type(of: perform_wavelet_denoising)) + get_csp = try library.symbol("get_csp", as: type(of: get_csp)) + get_window = try library.symbol("get_window", as: type(of: get_window)) + perform_fft = try library.symbol("perform_fft", as: type(of: perform_fft)) + perform_ifft = try library.symbol("perform_ifft", as: type(of: perform_ifft)) + get_nearest_power_of_two = try library.symbol("get_nearest_power_of_two", as: type(of: get_nearest_power_of_two)) + get_psd = try library.symbol("get_psd", as: type(of: get_psd)) + detrend = try library.symbol("detrend", as: type(of: detrend)) + calc_stddev = try library.symbol("calc_stddev", as: type(of: calc_stddev)) + get_psd_welch = try library.symbol("get_psd_welch", as: type(of: get_psd_welch)) + get_band_power = try library.symbol("get_band_power", as: type(of: get_band_power)) + get_custom_band_powers = try library.symbol("get_custom_band_powers", as: type(of: get_custom_band_powers)) + get_railed_percentage = try library.symbol("get_railed_percentage", as: type(of: get_railed_percentage)) + get_oxygen_level = try library.symbol("get_oxygen_level", as: type(of: get_oxygen_level)) + get_heart_rate = try library.symbol("get_heart_rate", as: type(of: get_heart_rate)) + restore_data_from_wavelet_detailed_coeffs = try library.symbol("restore_data_from_wavelet_detailed_coeffs", as: type(of: restore_data_from_wavelet_detailed_coeffs)) + detect_peaks_z_score = try library.symbol("detect_peaks_z_score", as: type(of: detect_peaks_z_score)) + perform_ica = try library.symbol("perform_ica", as: type(of: perform_ica)) + set_log_level_data_handler = try library.symbol("set_log_level_data_handler", as: type(of: set_log_level_data_handler)) + set_log_file_data_handler = try library.symbol("set_log_file_data_handler", as: type(of: set_log_file_data_handler)) + log_message_data_handler = try library.symbol("log_message_data_handler", as: type(of: log_message_data_handler)) + write_file = try library.symbol("write_file", as: type(of: write_file)) + read_file = try library.symbol("read_file", as: type(of: read_file)) + get_num_elements_in_file = try library.symbol("get_num_elements_in_file", as: type(of: get_num_elements_in_file)) + get_version_data_handler = try library.symbol("get_version_data_handler", as: type(of: get_version_data_handler)) + } +} diff --git a/swift_package/Sources/BrainFlow/MLModel.swift b/swift_package/Sources/BrainFlow/MLModel.swift new file mode 100644 index 000000000..59ce3a8d3 --- /dev/null +++ b/swift_package/Sources/BrainFlow/MLModel.swift @@ -0,0 +1,140 @@ +import Foundation + +public final class MLModel { + private let params: BrainFlowModelParams + private let serialized_params: String + + public init(params: BrainFlowModelParams) throws { + self.params = params + serialized_params = try params.to_json() + } + + public static func set_log_level(_ log_level: Int) throws { + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.set_log_level_ml_module(CInt(log_level)), "Error in set_log_level") + } + } + + public static func set_log_level(_ log_level: LogLevels) throws { + try set_log_level(log_level.rawValue) + } + + public static func enable_ml_logger() throws { + try set_log_level(LogLevels.LEVEL_INFO) + } + + public static func enable_dev_ml_logger() throws { + try set_log_level(LogLevels.LEVEL_TRACE) + } + + public static func disable_ml_logger() throws { + try set_log_level(LogLevels.LEVEL_OFF) + } + + public static func set_log_file(_ log_file: String) throws { + try log_file.withCString { path in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.set_log_file_ml_module(path), "Error in set_log_file") + } + } + } + + public static func log_message(_ log_level: Int, message: String) throws { + var mutableMessage = Array(message.utf8CString) + try mutableMessage.withUnsafeMutableBufferPointer { pointer in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.log_message_ml_module(CInt(log_level), pointer.baseAddress), "Error in log_message") + } + } + } + + public static func release_all() throws { + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.release_all(), "Error in release_all") + } + } + + public static func get_version() throws -> String { + let maxLength = 64 + var bytes = [CChar](repeating: 0, count: maxLength) + var length: CInt = 0 + try bytes.withUnsafeMutableBufferPointer { pointer in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.get_version_ml_module(pointer.baseAddress, &length, CInt(maxLength)), "Error in get_version") + } + } + return String(bytes: bytes.prefix(Int(length)).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? "" + } + + public func prepare() throws { + try serialized_params.withCString { paramsPtr in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.prepare(paramsPtr), "Error in prepare") + } + } + } + + public func release() throws { + try serialized_params.withCString { paramsPtr in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.release(paramsPtr), "Error in release") + } + } + } + + public func predict(input_data: [Double]) throws -> [Double] { + var input = input_data + var output = [Double](repeating: 0.0, count: params.max_array_size) + var outputLen: CInt = 0 + try serialized_params.withCString { paramsPtr in + try input.withUnsafeMutableBufferPointer { inputPtr in + try output.withUnsafeMutableBufferPointer { outputPtr in + try MLModelNative.withML { native in + try checkBrainFlowExitCode(native.predict(inputPtr.baseAddress, CInt(input_data.count), outputPtr.baseAddress, &outputLen, paramsPtr), "Error in predict") + } + } + } + } + return Array(output.prefix(Int(outputLen))) + } +} + +final class MLModelNative { + typealias VersionFunction = @convention(c) (UnsafeMutablePointer?, UnsafeMutablePointer?, CInt) -> CInt + + let prepare: @convention(c) (UnsafePointer?) -> CInt + let predict: @convention(c) (UnsafeMutablePointer?, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafePointer?) -> CInt + let release: @convention(c) (UnsafePointer?) -> CInt + let release_all: @convention(c) () -> CInt + let set_log_level_ml_module: @convention(c) (CInt) -> CInt + let set_log_file_ml_module: @convention(c) (UnsafePointer?) -> CInt + let log_message_ml_module: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt + let get_version_ml_module: VersionFunction + + private static let lock = NSLock() + private static var cached: MLModelNative? + + static func withML(_ body: (MLModelNative) throws -> T) throws -> T { + try body(load()) + } + + private static func load() throws -> MLModelNative { + lock.lock() + defer { lock.unlock() } + if let cached { return cached } + let value = try MLModelNative(library: NativeLibraries.mlModule.load()) + cached = value + return value + } + + private init(library: NativeLibrary) throws { + prepare = try library.symbol("prepare", as: type(of: prepare)) + predict = try library.symbol("predict", as: type(of: predict)) + release = try library.symbol("release", as: type(of: release)) + release_all = try library.symbol("release_all", as: type(of: release_all)) + set_log_level_ml_module = try library.symbol("set_log_level_ml_module", as: type(of: set_log_level_ml_module)) + set_log_file_ml_module = try library.symbol("set_log_file_ml_module", as: type(of: set_log_file_ml_module)) + log_message_ml_module = try library.symbol("log_message_ml_module", as: type(of: log_message_ml_module)) + get_version_ml_module = try library.symbol("get_version_ml_module", as: type(of: get_version_ml_module)) + } +} diff --git a/swift_package/Sources/BrainFlowCLI/main.swift b/swift_package/Sources/BrainFlowCLI/main.swift new file mode 100644 index 000000000..c098c9aa8 --- /dev/null +++ b/swift_package/Sources/BrainFlowCLI/main.swift @@ -0,0 +1,30 @@ +import BrainFlow +import Foundation + +let boardId = BoardIds.SYNTHETIC_BOARD +var params = BrainFlowInputParams() + +do { + try BoardShim.enable_board_logger() + let board = try BoardShim(board_id: boardId, input_params: params) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + Thread.sleep(forTimeInterval: 2.0) + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + let rows = data.count + let cols = data.first?.count ?? 0 + let samplingRate = try BoardShim.get_sampling_rate(board_id: boardId.rawValue) + let eegChannels = try BoardShim.get_eeg_channels(board_id: boardId.rawValue) + + print("BrainFlow Swift synthetic board sample") + print("board_id=\(boardId.rawValue)") + print("sampling_rate=\(samplingRate)") + print("rows=\(rows) cols=\(cols)") + print("eeg_channels=\(eegChannels)") +} catch { + fputs("BrainFlow CLI failed: \(error)\n", stderr) + exit(1) +} diff --git a/swift_package/Sources/BrainFlowMacDemo/main.swift b/swift_package/Sources/BrainFlowMacDemo/main.swift new file mode 100644 index 000000000..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..54ccae074 --- /dev/null +++ b/swift_package/Tests/BrainFlowTests/BrainFlowTests.swift @@ -0,0 +1,182 @@ +import XCTest +@testable import BrainFlow + +final class BrainFlowTests: XCTestCase { + private func requireNativeLibraries() throws { + do { + _ = try BoardShim.get_version() + } catch { + throw XCTSkip("BrainFlow native libraries are not available; build BrainFlow into installed/lib or set BRAINFLOW_LIB_DIR.") + } + } + + private func assertInvalidArguments( + _ expression: @autoclosure () throws -> T, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertThrowsError(try expression(), file: file, line: line) { error in + guard let brainFlowError = error as? BrainFlowError else { + return XCTFail("Expected BrainFlowError, got \(error)", file: file, line: line) + } + XCTAssertEqual( + brainFlowError.exit_code, + BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.rawValue, + file: file, + line: line + ) + } + } + + func testInputParamsJSON() throws { + var params = BrainFlowInputParams() + params.serial_port = "/dev/ttyUSB0" + params.set_master_board(.SYNTHETIC_BOARD) + let json = try params.to_json() + + XCTAssertTrue(json.contains("serial_port")) + XCTAssertTrue(json.contains("ttyUSB0")) + XCTAssertTrue(json.contains("master_board")) + } + + func testBoardShimRejectsInvalidArgumentsBeforeNativeCalls() throws { + let board = try BoardShim(board_id: .SYNTHETIC_BOARD) + + assertInvalidArguments(try board.start_stream(buffer_size: 0)) + assertInvalidArguments(try board.config_board_with_bytes([])) + assertInvalidArguments(try board.get_current_board_data(num_samples: 0)) + } + + func testDataFilterRejectsInvalidArgumentsBeforeNativeCalls() throws { + assertInvalidArguments(try DataFilter.get_csp(data: [[[1.0, 2.0]], [[3.0]]], labels: [0.0, 1.0])) + assertInvalidArguments(try DataFilter.get_csp(data: [[[1.0, 2.0]]], labels: [])) + assertInvalidArguments(try DataFilter.get_window(window_function: WindowOperations.HANNING.rawValue, window_len: 0)) + assertInvalidArguments(try DataFilter.perform_ifft(data: [Complex(real: 1.0, imag: 0.0)])) + assertInvalidArguments(try DataFilter.get_psd_welch(data: [1.0, 2.0, 3.0], nfft: 4, overlap: 0, sampling_rate: 250, window: WindowOperations.NO_WINDOW.rawValue)) + assertInvalidArguments(try DataFilter.get_band_power(psd: PSD(ampl: [1.0], freq: []), freq_start: 1.0, freq_end: 2.0)) + assertInvalidArguments(try DataFilter.get_custom_band_powers( + data: [[1.0, 2.0]], + bands: [FrequencyBand(start: 1.0, stop: 2.0)], + channels: [1], + sampling_rate: 250, + apply_filter: false + )) + assertInvalidArguments(try DataFilter.perform_ica(data: [[1.0, 2.0], [3.0, 4.0]], num_components: 3)) + assertInvalidArguments(try DataFilter.calc_stddev(data: [1.0, 2.0], start_pos: 1, end_pos: 3)) + assertInvalidArguments(try DataFilter.get_railed_percentage(data: [], gain: 24)) + assertInvalidArguments(try DataFilter.get_oxygen_level(ppg_ir: [1.0], ppg_red: [1.0, 2.0], sampling_rate: 25)) + assertInvalidArguments(try DataFilter.get_heart_rate(ppg_ir: [1.0, 2.0], ppg_red: [1.0, 2.0], sampling_rate: 25, fft_size: 1023)) + } + + func testBrainFlowGetDataSyntheticBoard() throws { + try requireNativeLibraries() + let board = try BoardShim(board_id: .SYNTHETIC_BOARD) + try board.prepare_session() + XCTAssertTrue(try board.is_prepared()) + + try board.start_stream(buffer_size: 45000) + Thread.sleep(forTimeInterval: 1.0) + XCTAssertGreaterThan(try board.get_board_data_count(), 0) + + let currentData = try board.get_current_board_data(num_samples: 16) + XCTAssertEqual(currentData.count, try BoardShim.get_num_rows(board_id: BoardIds.SYNTHETIC_BOARD.rawValue)) + XCTAssertLessThanOrEqual(currentData.first?.count ?? 0, 16) + + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + XCTAssertEqual(data.count, try BoardShim.get_num_rows(board_id: BoardIds.SYNTHETIC_BOARD.rawValue)) + XCTAssertGreaterThan(data.first?.count ?? 0, 0) + } + + func testMarkersSyntheticBoard() throws { + try requireNativeLibraries() + let board = try BoardShim(board_id: .SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + try board.insert_marker(1.0) + Thread.sleep(forTimeInterval: 0.5) + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + let markerChannel = try BoardShim.get_marker_channel(board_id: BoardIds.SYNTHETIC_BOARD.rawValue) + XCTAssertTrue(data[markerChannel].contains { abs($0 - 1.0) < 0.0001 }) + } + + func testReadWriteFile() throws { + try requireNativeLibraries() + let data = [ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0] + ] + let fileName = NSTemporaryDirectory() + "/brainflow_swift_read_write.csv" + try DataFilter.write_file(data: data, file_name: fileName, file_mode: "w") + let restored = try DataFilter.read_file(fileName) + + XCTAssertEqual(restored.count, data.count) + XCTAssertEqual(restored.first?.count, data.first?.count) + } + + func testDownsamplingAndTransforms() throws { + try requireNativeLibraries() + let data = Array(0..<128).map(Double.init) + let downsampled = try DataFilter.perform_downsampling(data: data, period: 4, operation: .MEAN) + XCTAssertEqual(downsampled.count, 32) + + let fft = try DataFilter.perform_fft(data: data, start_pos: 0, end_pos: 128, window: .NO_WINDOW) + XCTAssertEqual(fft.count, 65) + + let restored = try DataFilter.perform_ifft(data: fft) + XCTAssertEqual(restored.count, 128) + } + + func testSignalFilteringDenoisingAndBandPower() throws { + try requireNativeLibraries() + var data = (0..<256).map { index in sin(Double(index) / 10.0) } + try DataFilter.perform_lowpass(data: &data, sampling_rate: 250, cutoff: 30.0, order: 4, filter_type: .BUTTERWORTH, ripple: 0.0) + try DataFilter.perform_highpass(data: &data, sampling_rate: 250, cutoff: 1.0, order: 4, filter_type: .BUTTERWORTH, ripple: 0.0) + try DataFilter.perform_wavelet_denoising( + data: &data, + wavelet: .DB5, + decomposition_level: 3, + wavelet_denoising: .SURESHRINK, + threshold: .HARD, + extension_type: .SYMMETRIC, + noise_level: .FIRST_LEVEL + ) + + let psd = try DataFilter.get_psd(data: data, start_pos: 0, end_pos: data.count, sampling_rate: 250, window: WindowOperations.HANNING.rawValue) + let power = try DataFilter.get_band_power(psd: psd, freq_start: 4.0, freq_end: 30.0) + XCTAssertTrue(power.isFinite) + } + + func testICA() throws { + try requireNativeLibraries() + let rows = 4 + let cols = 128 + let data = (0..&2\n exit 1\n fi\n 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 "../../../.." */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../..; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A10000000000000000000051 /* BrainFlow */ = { + isa = XCSwiftPackageProductDependency; + package = A10000000000000000000050 /* XCLocalSwiftPackageReference "../../../.." */; + productName = BrainFlow; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = A10000000000000000000001 /* Project object */; +} diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme new file mode 100644 index 000000000..2abd7e3dd --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemo.xcodeproj/xcshareddata/xcschemes/BrainFlowiOSDemo.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift b/swift_package/examples/apps/ios/BrainFlowiOSDemo/BrainFlowiOSDemoApp.swift new file mode 100644 index 000000000..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/swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist b/swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist new file mode 100644 index 000000000..30e89aafd --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDisplayName + BrainFlow Demo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSBluetoothAlwaysUsageDescription + BrainFlow uses Bluetooth to connect to supported Muse boards. + UILaunchScreen + + + diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy b/swift_package/examples/apps/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..196836a1f --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + + diff --git a/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md b/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md new file mode 100644 index 000000000..3ae60c676 --- /dev/null +++ b/swift_package/examples/apps/ios/BrainFlowiOSDemo/README.md @@ -0,0 +1,46 @@ +# BrainFlow iOS Demo + +This sample is a normal Xcode iOS application that exercises the BrainFlow Swift package with the synthetic board, so it does not need external hardware for simulator, TestFlight, or App Review smoke testing. + +## Run In Simulator + +From the repository root, 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 `../../../..` 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 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: + +- `libBoardController.dylib` +- `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: + +- Bundle ID: `org.brainflow.demo.ios` +- Display name and app icon +- Signing team and provisioning profile +- Screenshots for required iPhone/iPad sizes +- App privacy answers matching the final native libraries and any real-board permissions diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements b/swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements new file mode 100644 index 000000000..13cb114cf --- /dev/null +++ b/swift_package/examples/apps/macos/BrainFlowMacDemo/BrainFlowMacDemo.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/Info.plist b/swift_package/examples/apps/macos/BrainFlowMacDemo/Info.plist new file mode 100644 index 000000000..3be9880f3 --- /dev/null +++ b/swift_package/examples/apps/macos/BrainFlowMacDemo/Info.plist @@ -0,0 +1,18 @@ + + + + + CFBundleDisplayName + BrainFlow Demo + CFBundleIdentifier + org.brainflow.demo.macos + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 12.0 + NSHighResolutionCapable + + + diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy b/swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..196836a1f --- /dev/null +++ b/swift_package/examples/apps/macos/BrainFlowMacDemo/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + + diff --git a/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md b/swift_package/examples/apps/macos/BrainFlowMacDemo/README.md new file mode 100644 index 000000000..ebcc92537 --- /dev/null +++ b/swift_package/examples/apps/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/examples/tests/band_power/band_power.swift b/swift_package/examples/tests/band_power/band_power.swift new file mode 100644 index 000000000..7bbc8ec73 --- /dev/null +++ b/swift_package/examples/tests/band_power/band_power.swift @@ -0,0 +1,19 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum BandPowerExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + let data = try sample.firstEEGChannel() + let psd = try DataFilter.get_psd( + data: data, + start_pos: 0, + end_pos: data.count, + sampling_rate: sample.samplingRate, + window: WindowOperations.HANNING.rawValue + ) + let alpha = try DataFilter.get_band_power(psd: psd, freq_start: 8.0, freq_end: 13.0) + print("Alpha power: \(alpha)") + } +} diff --git a/swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift b/swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift new file mode 100644 index 000000000..5f28a1d36 --- /dev/null +++ b/swift_package/examples/tests/brainflow_get_data/brainflow_get_data.swift @@ -0,0 +1,18 @@ +import BrainFlow +import Foundation + +@main +enum BrainFlowGetDataExample { + static func main() throws { + let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + Thread.sleep(forTimeInterval: 5.0) + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + print("Rows: \(data.count)") + print("Samples: \(data.first?.count ?? 0)") + } +} diff --git a/swift_package/examples/tests/denoising/denoising.swift b/swift_package/examples/tests/denoising/denoising.swift new file mode 100644 index 000000000..a4488b966 --- /dev/null +++ b/swift_package/examples/tests/denoising/denoising.swift @@ -0,0 +1,20 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum DenoisingExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + var data = try sample.firstEEGChannel() + try DataFilter.perform_wavelet_denoising( + data: &data, + wavelet: WaveletTypes.DB5, + decomposition_level: 3, + wavelet_denoising: WaveletDenoisingTypes.SURESHRINK, + threshold: ThresholdTypes.HARD, + extension_type: WaveletExtensionTypes.SYMMETRIC, + noise_level: NoiseEstimationLevelTypes.FIRST_LEVEL + ) + print(data.prefix(10)) + } +} diff --git a/swift_package/examples/tests/downsampling/downsampling.swift b/swift_package/examples/tests/downsampling/downsampling.swift new file mode 100644 index 000000000..feeceddba --- /dev/null +++ b/swift_package/examples/tests/downsampling/downsampling.swift @@ -0,0 +1,12 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum DownsamplingExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + let data = try sample.firstEEGChannel() + let downsampled = try DataFilter.perform_downsampling(data: data, period: 4, operation: AggOperations.MEAN) + print(downsampled) + } +} diff --git a/swift_package/examples/tests/eeg_metrics/eeg_metrics.swift b/swift_package/examples/tests/eeg_metrics/eeg_metrics.swift new file mode 100644 index 000000000..bfbe2d8ec --- /dev/null +++ b/swift_package/examples/tests/eeg_metrics/eeg_metrics.swift @@ -0,0 +1,22 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum EEGMetricsExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read() + let bandPowers = try DataFilter.get_avg_band_powers( + data: sample.data, + channels: sample.eegChannels, + sampling_rate: sample.samplingRate, + apply_filter: true + ) + + let params = BrainFlowModelParams(metric: BrainFlowMetrics.MINDFULNESS, classifier: BrainFlowClassifiers.DEFAULT_CLASSIFIER) + let model = try MLModel(params: params) + try model.prepare() + let prediction = try model.predict(input_data: bandPowers.average) + try model.release() + print(prediction) + } +} diff --git a/swift_package/examples/tests/ica/ica.swift b/swift_package/examples/tests/ica/ica.swift new file mode 100644 index 000000000..92840e65f --- /dev/null +++ b/swift_package/examples/tests/ica/ica.swift @@ -0,0 +1,14 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum ICAExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 500) + let channels = Array(sample.eegChannels.prefix(4)) + let ica = try DataFilter.perform_ica(data: sample.data, num_components: 2, channels: channels) + + print("W: \(ica.w.count)x\(ica.w.first?.count ?? 0)") + print("S: \(ica.s.count)x\(ica.s.first?.count ?? 0)") + } +} diff --git a/swift_package/examples/tests/markers/markers.swift b/swift_package/examples/tests/markers/markers.swift new file mode 100644 index 000000000..4e614afd8 --- /dev/null +++ b/swift_package/examples/tests/markers/markers.swift @@ -0,0 +1,20 @@ +import BrainFlow +import Foundation + +@main +enum MarkersExample { + static func main() throws { + let board = try BoardShim(board_id: BoardIds.SYNTHETIC_BOARD) + try board.prepare_session() + try board.start_stream(buffer_size: 45000) + try board.insert_marker(1.0) + Thread.sleep(forTimeInterval: 1.0) + try board.stop_stream() + let data = try board.get_board_data() + try board.release_session() + + let markerChannel = try BoardShim.get_marker_channel(board_id: BoardIds.SYNTHETIC_BOARD.rawValue) + print("Marker channel: \(markerChannel)") + print("Samples: \(data[markerChannel].count)") + } +} diff --git a/swift_package/examples/tests/read_write_file/read_write_file.swift b/swift_package/examples/tests/read_write_file/read_write_file.swift new file mode 100644 index 000000000..f4f7afa86 --- /dev/null +++ b/swift_package/examples/tests/read_write_file/read_write_file.swift @@ -0,0 +1,16 @@ +import BrainFlow +import BrainFlowExampleSupport +import Foundation + +@main +enum ReadWriteFileExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(seconds: 2.0) + + let fileName = NSTemporaryDirectory() + "/brainflow_swift.csv" + try DataFilter.write_file(data: sample.data, file_name: fileName, file_mode: "w") + let restored = try DataFilter.read_file(fileName) + print("Original: \(sample.data.count)x\(sample.data.first?.count ?? 0)") + print("Restored: \(restored.count)x\(restored.first?.count ?? 0)") + } +} diff --git a/swift_package/examples/tests/signal_filtering/signal_filtering.swift b/swift_package/examples/tests/signal_filtering/signal_filtering.swift new file mode 100644 index 000000000..1572cf3be --- /dev/null +++ b/swift_package/examples/tests/signal_filtering/signal_filtering.swift @@ -0,0 +1,13 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum SignalFilteringExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + var data = try sample.firstEEGChannel() + try DataFilter.perform_lowpass(data: &data, sampling_rate: sample.samplingRate, cutoff: 30.0, order: 4, filter_type: FilterTypes.BUTTERWORTH, ripple: 0.0) + try DataFilter.perform_highpass(data: &data, sampling_rate: sample.samplingRate, cutoff: 1.0, order: 4, filter_type: FilterTypes.BUTTERWORTH, ripple: 0.0) + print(data.prefix(10)) + } +} diff --git a/swift_package/examples/tests/support/SyntheticBoardData.swift b/swift_package/examples/tests/support/SyntheticBoardData.swift new file mode 100644 index 000000000..29d051feb --- /dev/null +++ b/swift_package/examples/tests/support/SyntheticBoardData.swift @@ -0,0 +1,42 @@ +import BrainFlow +import Foundation + +public struct SyntheticBoardData { + public let boardId: BoardIds + public let data: [[Double]] + public let eegChannels: [Int] + public let samplingRate: Int + + public func firstEEGChannel() throws -> [Double] { + guard let channel = eegChannels.first, channel >= 0, channel < data.count else { + throw BrainFlowError("No EEG channel found in synthetic board data", BrainFlowExitCodes.GENERAL_ERROR.rawValue) + } + return data[channel] + } +} + +public enum SyntheticBoardDataReader { + public static func read(seconds: TimeInterval = 5.0, maxSamples: Int? = nil) throws -> SyntheticBoardData { + let boardId = BoardIds.SYNTHETIC_BOARD + let board = try BoardShim(board_id: boardId) + try board.prepare_session() + + do { + try board.start_stream(buffer_size: 45_000) + Thread.sleep(forTimeInterval: seconds) + try board.stop_stream() + let data = try board.get_board_data(maxSamples) + try board.release_session() + + return SyntheticBoardData( + boardId: boardId, + data: data, + eegChannels: try BoardShim.get_eeg_channels(board_id: boardId), + samplingRate: try BoardShim.get_sampling_rate(board_id: boardId) + ) + } catch { + try? board.release_session() + throw error + } + } +} diff --git a/swift_package/examples/tests/transforms/transforms.swift b/swift_package/examples/tests/transforms/transforms.swift new file mode 100644 index 000000000..a14f32c9a --- /dev/null +++ b/swift_package/examples/tests/transforms/transforms.swift @@ -0,0 +1,14 @@ +import BrainFlow +import BrainFlowExampleSupport + +@main +enum TransformsExample { + static func main() throws { + let sample = try SyntheticBoardDataReader.read(maxSamples: 256) + let data = try sample.firstEEGChannel() + let fft = try DataFilter.perform_fft(data: data, start_pos: 0, end_pos: data.count, window: WindowOperations.HANNING) + let restored = try DataFilter.perform_ifft(data: fft) + print("FFT bins: \(fft.count)") + print("Restored samples: \(restored.count)") + } +}