From d70e9b5851d5626f4422fd14331b564fbbf18857 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sun, 8 Dec 2024 01:05:16 +0200 Subject: [PATCH 01/26] initial version of BioListener communication over tcp --- .../brainflow/board_controller_library.cs | 3 +- .../src/main/java/brainflow/BoardIds.java | 3 +- julia_package/brainflow/src/board_shim.jl | 1 + matlab_package/brainflow/BoardIds.m | 1 + nodejs_package/brainflow/brainflow.types.ts | 3 +- python_package/brainflow/board_shim.py | 1 + rust_package/brainflow/src/ffi/constants.rs | 3 +- .../biolistener/biolistener.cpp | 329 ++++++++++++++++++ .../biolistener/inc/biolistener.h | 49 +++ src/board_controller/board_controller.cpp | 4 + src/board_controller/brainflow_boards.cpp | 14 +- src/board_controller/build.cmake | 2 + src/utils/inc/brainflow_constants.h | 3 +- 13 files changed, 410 insertions(+), 6 deletions(-) create mode 100644 src/board_controller/biolistener/biolistener.cpp create mode 100644 src/board_controller/biolistener/inc/biolistener.h diff --git a/csharp_package/brainflow/brainflow/board_controller_library.cs b/csharp_package/brainflow/brainflow/board_controller_library.cs index 5e0ce5c14..68114775a 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -116,7 +116,8 @@ public enum BoardIds EXPLORE_PLUS_8_CHAN_BOARD = 54, EXPLORE_PLUS_32_CHAN_BOARD = 55, PIEEG_BOARD = 56, - NEUROPAWN_KNIGHT_BOARD = 57 + NEUROPAWN_KNIGHT_BOARD = 57, + BIOLISTENER_BOARD = 58, }; diff --git a/java_package/brainflow/src/main/java/brainflow/BoardIds.java b/java_package/brainflow/src/main/java/brainflow/BoardIds.java index a460d98bc..af0363dc2 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardIds.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardIds.java @@ -66,7 +66,8 @@ public enum BoardIds EXPLORE_PLUS_8_CHAN_BOARD(54), EXPLORE_PLUS_32_CHAN_BOARD(55), PIEEG_BOARD(56), - NEUROPAWN_KNIGHT_BOARD(57); + NEUROPAWN_KNIGHT_BOARD(57), + BIOLISTENER_BOARD(58); private final int board_id; private static final Map bi_map = new HashMap (); diff --git a/julia_package/brainflow/src/board_shim.jl b/julia_package/brainflow/src/board_shim.jl index c455c2fbd..62243355f 100644 --- a/julia_package/brainflow/src/board_shim.jl +++ b/julia_package/brainflow/src/board_shim.jl @@ -62,6 +62,7 @@ export BrainFlowInputParams EXPLORE_PLUS_32_CHAN_BOARD = 55 PIEEG_BOARD = 56 NEUROPAWN_KNIGHT_BOARD = 57 + BIOLISTENER_BOARD = 58 end diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m index 4c4d69a90..665cf6f74 100644 --- a/matlab_package/brainflow/BoardIds.m +++ b/matlab_package/brainflow/BoardIds.m @@ -60,5 +60,6 @@ EXPLORE_PLUS_32_CHAN_BOARD(55) PIEEG_BOARD(56) NEUROPAWN_KNIGHT_BOARD(57) + BIOLISTENER_BOARD(58) end end \ No newline at end of file diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index b0730b561..d76fc2194 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -69,7 +69,8 @@ export enum BoardIds { EXPLORE_PLUS_8_CHAN_BOARD = 54, EXPLORE_PLUS_32_CHAN_BOARD = 55, PIEEG_BOARD = 56, - NEUROPAWN_KNIGHT_BOARD = 57 + NEUROPAWN_KNIGHT_BOARD = 57, + BIOLISTENER_BOARD = 58 } export enum IpProtocolTypes { diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index 0b8f5707b..aceb271a6 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -75,6 +75,7 @@ class BoardIds(enum.IntEnum): EXPLORE_PLUS_32_CHAN_BOARD = 55 #: PIEEG_BOARD = 56 #: NEUROPAWN_KNIGHT_BOARD = 57 #: + BIOLISTENER_BOARD = 58 #: class IpProtocolTypes(enum.IntEnum): diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index d856f1ab3..26ed86fee 100644 --- a/rust_package/brainflow/src/ffi/constants.rs +++ b/rust_package/brainflow/src/ffi/constants.rs @@ -32,7 +32,7 @@ impl BoardIds { pub const FIRST: BoardIds = BoardIds::PlaybackFileBoard; } impl BoardIds { - pub const LAST: BoardIds = BoardIds::NeuropawnKnightBoard; + pub const LAST: BoardIds = BoardIds::BiolistenerBoard; } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] @@ -96,6 +96,7 @@ pub enum BoardIds { ExplorePlus32ChanBoard = 55, PieegBoard = 56, NeuropawnKnightBoard = 57, + BiolistenerBoard = 58, } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp new file mode 100644 index 000000000..fa4554875 --- /dev/null +++ b/src/board_controller/biolistener/biolistener.cpp @@ -0,0 +1,329 @@ +#include "biolistener.h" + +#include +#include +#include +#include + +#include "custom_cast.h" +#include "json.hpp" +#include "network_interfaces.h" +#include "timestamp.h" + +using json = nlohmann::json; + + +BioListener::BioListener(int board_id, struct BrainFlowInputParams params) : Board (board_id, params) +{ + control_socket = NULL; + keep_alive = false; + initialized = false; + control_port = -1; + data_port = -1; +} + +BioListener::~BioListener () +{ + skip_logs = true; + release_session (); +} + +int BioListener::prepare_session () +{ + if (initialized) + { + safe_logger (spdlog::level::info, "Session is already prepared"); + return (int)BrainFlowExitCodes::STATUS_OK; + } + + if (params.timeout < 2) + { + params.timeout = 4; + } + + int res = create_control_connection (); + + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + res = wait_for_connection (); + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + res = send_control_msg (R"({"command":"send_control_msg"})"); + } + + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + if (control_socket != NULL) + { + delete control_socket; + control_socket = NULL; + } + } + else + { + initialized = true; + } + + return res; +} + +int BioListener::config_board (std::string conf, std::string &response) +{ + return send_control_msg (conf.c_str ()); +} + +int BioListener::start_stream (int buffer_size, const char *streamer_params) +{ + if (!initialized) + { + safe_logger (spdlog::level::err, "You need to call prepare_session before config_board"); + return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR; + } + if (keep_alive) + { + safe_logger (spdlog::level::err, "Streaming thread already running"); + return (int)BrainFlowExitCodes::STREAM_ALREADY_RUN_ERROR; + } + int res = prepare_for_acquisition (buffer_size, streamer_params); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + res = send_control_msg (R"({"command":"start_stream"})"); + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + keep_alive = true; + streaming_thread = std::thread ([this] { this->read_thread (); }); + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int BioListener::stop_stream () +{ + if (keep_alive) + { + if (send_control_msg (R"({"command":"stop_stream")") != (int)BrainFlowExitCodes::STATUS_OK) + { + safe_logger (spdlog::level::warn, "failed to set low power mode"); + } + keep_alive = false; + streaming_thread.join (); + return (int)BrainFlowExitCodes::STATUS_OK; + } + return (int)BrainFlowExitCodes::STREAM_THREAD_IS_NOT_RUNNING; +} + +int BioListener::release_session () +{ + if (initialized) + { + if (keep_alive) + { + stop_stream (); + } + initialized = false; + free_packages (); + send_control_msg (R"({"command":"release_session"})"); + if (control_socket) + { + control_socket->close (); + delete control_socket; + control_socket = NULL; + } + control_port = -1; + data_port = -1; + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +void BioListener::read_thread () +{ + const int max_size = 1024; + char message[max_size]; + int num_rows = board_descr["default"]["num_rows"]; + double *package = new double[num_rows]; + for (int i = 0; i < num_rows; i++) + { + package[i] = 0.0; + } + + std::vector eeg_channels = board_descr["default"]["eeg_channels"]; + std::vector other_channels = board_descr["default"]["other_channels"]; + + while (keep_alive) + { + int bytes_recv = control_socket->recv (message, max_size); + if (bytes_recv < 1) + { + safe_logger (spdlog::level::trace, "no data received"); + continue; + } + std::string message_received = std::string (message, bytes_recv); + std::vector splitted_packages = + split_string (message_received, PACKET_DELIMITER_CSV); + + safe_logger (spdlog::level::trace, "Received message: >>>{}<<<", message_received.c_str ()); + + + for (std::string recv_package : splitted_packages) + { + safe_logger (spdlog::level::trace, "Received package: >>>{}<<<", recv_package.c_str ()); + try { + json j = json::parse (recv_package); +// json j = json::from_msgpack(recv_package); + + safe_logger (spdlog::level::trace, "Parsed json: {}", j.dump ()); + + + // parse from_msgpack + + + if (j["type"] == 1) + { + package[board_descr["default"]["timestamp_channel"].get ()] = j["ts"]; + package[board_descr["default"]["package_num_channel"].get ()] = j["n"]; +// int sensor_id = j["s_id"]; + + // in data is array of 8 values, copy them to package + for (int i = 0; i < 8; i++) + { + package[eeg_channels[i]] = j["data"][i]; + } + + + push_package (package); + } + + + } catch (json::parse_error &e) { + safe_logger (spdlog::level::err, "Failed to parse json: {}", e.what ()); + } + + + } + } + + delete[] package; +} + +std::vector BioListener::split_string (const std::string &package, char delim) +{ + std::vector result; + size_t start; + size_t end = 0; + while ((start = package.find_first_not_of (delim, end)) != std::string::npos) + { + end = package.find (delim, start); + std::string cur_str = package.substr (start, end - start); + result.push_back (cur_str); + } + return result; +} + + +int BioListener::create_control_connection () +{ + char local_ip[80]; + + strncpy(local_ip, params.ip_address.c_str(), sizeof(local_ip) - 1); + local_ip[sizeof(local_ip) - 1] = '\0'; +// safe_logger (spdlog::level::info, "Local ip address is {}", params.ip_address.c_str ()); +// int local_ip_res = +// SocketClientUDP::get_local_ip_addr (params.ip_address.c_str (), DEFAULT_CONTROL_PORT, local_ip); +// if (local_ip_res != (int)SocketClientUDPReturnCodes::STATUS_OK) +// { +// safe_logger (spdlog::level::err, "failed to get local ip addr: {}", local_ip_res); +// return (int)BrainFlowExitCodes::GENERAL_ERROR; +// } + safe_logger (spdlog::level::info, "local ip addr is {}", local_ip); + + int res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + for (int i = 0; i < 39; i += 2) + { + control_port = DEFAULT_CONTROL_PORT + i; + control_socket = new SocketServerTCP (local_ip, control_port, false); + if (control_socket->bind () == ((int)SocketServerTCPReturnCodes::STATUS_OK)) + { + safe_logger (spdlog::level::info, "use port {} for control", control_port); + res = (int)BrainFlowExitCodes::STATUS_OK; + break; + } + else + { + safe_logger (spdlog::level::warn, "failed to connect to {}", control_port); + } + control_socket->close (); + delete control_socket; + control_socket = NULL; + } + return res; +} + +int BioListener::send_control_msg (const char *msg) +{ + // FIXME: hardcoded ports +// // should never happen +// if ((control_port < 0) || (data_port < 0)) +// { +// safe_logger (spdlog::level::info, "ports for data or control are not set"); +// return (int)BrainFlowExitCodes::GENERAL_ERROR; +// } + + // convert msg to string just by copying + std::string package = msg; + + int res = (int)BrainFlowExitCodes::STATUS_OK; + int bytes_send = control_socket->send (package.c_str (), (int)package.size ()); + if (bytes_send != (int)package.size ()) + { + safe_logger (spdlog::level::err, "failed to send control msg package: {}, res is {}", msg, + bytes_send); + res = (int)BrainFlowExitCodes::GENERAL_ERROR; + } + else + { + safe_logger (spdlog::level::info, "Message: {} sent", msg); + } + return res; +} + +int BioListener::wait_for_connection () +{ + int res = (int)BrainFlowExitCodes::STATUS_OK; + int accept_res = control_socket->accept (); + if (accept_res != (int)SocketServerTCPReturnCodes::STATUS_OK) + { + safe_logger (spdlog::level::err, "error in accept"); + res = (int)BrainFlowExitCodes::GENERAL_ERROR; + } + else + { + int max_attempts = 1500; + for (int i = 0; i < max_attempts; i++) + { + safe_logger (spdlog::level::trace, "waiting for accept {}/{}", i, max_attempts); + if (control_socket->client_connected) + { + safe_logger (spdlog::level::trace, "emotibit connected"); + break; + } + else + { +#ifdef _WIN32 + Sleep (300); +#else + usleep (300000); +#endif + } + } + if (!control_socket->client_connected) + { + safe_logger (spdlog::level::trace, "failed to establish connection"); + res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + } + return res; +} + + diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h new file mode 100644 index 000000000..051db52a0 --- /dev/null +++ b/src/board_controller/biolistener/inc/biolistener.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#include "board.h" +#include "board_controller.h" + +#include "broadcast_server.h" +#include "socket_client_udp.h" +#include "socket_server_tcp.h" + +#define DEFAULT_CONTROL_PORT 12345 +#define PACKET_DELIMITER_CSV '\n' + + +class BioListener : public Board +{ + +private: + volatile bool keep_alive; + volatile bool initialized; + + std::string ip_address; + std::thread streaming_thread; + SocketServerTCP *control_socket; + std::mutex m; + std::condition_variable cv; + int control_port; + int data_port; + + void read_thread (); + std::vector split_string (const std::string &package, char delim); + + int create_control_connection (); + int send_control_msg (const char *msg); + int wait_for_connection (); + +public: + BioListener (int board_id, struct BrainFlowInputParams params); + ~BioListener (); + + int prepare_session (); + int start_stream (int buffer_size, const char *streamer_params); + int stop_stream (); + int release_session (); + int config_board (std::string config, std::string &response); +}; diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index cb55e2e88..452a0fa3f 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -16,6 +16,7 @@ #include "aavaa_v3.h" #include "ant_neuro.h" +#include "biolistener.h" #include "board.h" #include "board_controller.h" #include "board_info_getter.h" @@ -285,6 +286,9 @@ int prepare_session (int board_id, const char *json_brainflow_input_params) board = std::shared_ptr (new Knight ((int)BoardIds::NEUROPAWN_KNIGHT_BOARD, params)); break; + case BoardIds::BIOLISTENER_BOARD: + board = std::shared_ptr (new BioListener (board_id, params)); + break; default: return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; } diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index 884e2efb5..f3e150eff 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -75,7 +75,8 @@ BrainFlowBoards::BrainFlowBoards() {"54", json::object()}, {"55", json::object()}, {"56", json::object()}, - {"57", json::object()} + {"57", json::object()}, + {"58", json::object()} } }}; @@ -1107,6 +1108,17 @@ BrainFlowBoards::BrainFlowBoards() {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, {"other_channels", {9, 10}} }; + brainflow_boards_json["boards"]["58"]["default"] = + { + {"name", "BioListener"}, + {"sampling_rate", 500}, + {"timestamp_channel", 11}, + {"marker_channel",12}, + {"package_num_channel", 0}, + {"num_rows", 13}, + {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, + {"other_channels", {9, 10}} + }; } BrainFlowBoards boards_struct; diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake index 9cb994390..0a11c3188 100644 --- a/src/board_controller/build.cmake +++ b/src/board_controller/build.cmake @@ -86,6 +86,7 @@ SET (BOARD_CONTROLLER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/aavaa/aavaa_v3.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/pieeg/pieeg_board.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/knight.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/biolistener.cpp ) include (${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro/build.cmake) @@ -144,6 +145,7 @@ target_include_directories ( ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/aavaa/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/pieeg/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/inc + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/inc ) target_compile_definitions(${BOARD_CONTROLLER_NAME} PRIVATE NOMINMAX BRAINFLOW_VERSION=${BRAINFLOW_VERSION}) diff --git a/src/utils/inc/brainflow_constants.h b/src/utils/inc/brainflow_constants.h index d115fce1a..abb1b6e5d 100644 --- a/src/utils/inc/brainflow_constants.h +++ b/src/utils/inc/brainflow_constants.h @@ -89,9 +89,10 @@ enum class BoardIds : int EXPLORE_PLUS_32_CHAN_BOARD = 55, PIEEG_BOARD = 56, NEUROPAWN_KNIGHT_BOARD = 57, + BIOLISTENER_BOARD = 58, // use it to iterate FIRST = PLAYBACK_FILE_BOARD, - LAST = NEUROPAWN_KNIGHT_BOARD + LAST = BIOLISTENER_BOARD }; enum class IpProtocolTypes : int From f02871c98cd14087157df9fafebe817f599ca143 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sun, 15 Dec 2024 17:57:08 +0200 Subject: [PATCH 02/26] enhanced BioListener communication over tcp - custom packet, automatic ADC type detection, conversion to microvolts --- .../biolistener/biolistener.cpp | 246 ++++++++++++------ .../biolistener/inc/biolistener.h | 37 ++- 2 files changed, 207 insertions(+), 76 deletions(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index fa4554875..18582c780 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -13,13 +13,20 @@ using json = nlohmann::json; -BioListener::BioListener(int board_id, struct BrainFlowInputParams params) : Board (board_id, params) +BioListener::BioListener (int board_id, struct BrainFlowInputParams params) + : Board (board_id, params) { control_socket = NULL; keep_alive = false; initialized = false; control_port = -1; data_port = -1; + + // Data channels gain to default value + for (int i = 0; i < BIOLISTENER_DATA_CHANNELS_COUNT; i++) + { + channels_gain[i] = BIOLISTENER_DEFAULT_PGA_GAIN; + } } BioListener::~BioListener () @@ -49,7 +56,12 @@ int BioListener::prepare_session () } if (res == (int)BrainFlowExitCodes::STATUS_OK) { - res = send_control_msg (R"({"command":"send_control_msg"})"); + auto json_command = json {{"command", BIOLISTENER_COMMAND_RESET_ADC}}; + + res = send_control_msg ((json_command.dump () + PACKET_DELIMITER_CSV).c_str ()); + + // FIXME: sleep for 500ms to wait for reset + std::this_thread::sleep_for (std::chrono::milliseconds (500)); } if (res != (int)BrainFlowExitCodes::STATUS_OK) @@ -90,7 +102,9 @@ int BioListener::start_stream (int buffer_size, const char *streamer_params) { return res; } - res = send_control_msg (R"({"command":"start_stream"})"); + + auto json_command = json {{"command", BIOLISTENER_COMMAND_START_SAMPLING}}; + res = send_control_msg ((json_command.dump () + PACKET_DELIMITER_CSV).c_str ()); if (res == (int)BrainFlowExitCodes::STATUS_OK) { keep_alive = true; @@ -103,10 +117,13 @@ int BioListener::stop_stream () { if (keep_alive) { - if (send_control_msg (R"({"command":"stop_stream")") != (int)BrainFlowExitCodes::STATUS_OK) + auto json_command = json {{"command", BIOLISTENER_COMMAND_STOP_SAMPLING}}; + if (send_control_msg ((json_command.dump () + PACKET_DELIMITER_CSV).c_str ()) != + (int)BrainFlowExitCodes::STATUS_OK) { - safe_logger (spdlog::level::warn, "failed to set low power mode"); + safe_logger (spdlog::level::warn, "failed to stop stream"); } + keep_alive = false; streaming_thread.join (); return (int)BrainFlowExitCodes::STATUS_OK; @@ -124,7 +141,6 @@ int BioListener::release_session () } initialized = false; free_packages (); - send_control_msg (R"({"command":"release_session"})"); if (control_socket) { control_socket->close (); @@ -137,9 +153,64 @@ int BioListener::release_session () return (int)BrainFlowExitCodes::STATUS_OK; } +bool BioListener::parse_tcp_buffer ( + const char *buffer, size_t buffer_size, data_packet &parsed_packet) +{ + // Ensure the buffer size matches the packet size + if (buffer_size != PACKET_SIZE) + { + safe_logger (spdlog::level::trace, "Buffer size mismatch!"); + + return false; + } + + // Copy the raw buffer into the struct + std::memcpy (&parsed_packet, buffer, PACKET_SIZE); + + // Validate the header and footer + if (parsed_packet.header != 0xA0) + { + safe_logger (spdlog::level::trace, "Invalid header! Expected: 0xA0"); + return false; + } + + if (parsed_packet.footer != 0xC0) + { + safe_logger (spdlog::level::trace, "Invalid footer! Expected: 0xC0"); + return false; + } + + return true; +} + + +// Convert raw ADC code (two's complement) to voltage +// ref: reference voltage (X if milli volts output is needed, X * 1000 if micro volts output) +// 1.2V for ADS131M08, 2.5V for AD7771 +// raw_code: raw ADC code +// pga_gain: gain of the PGA +// adc_resolution: resolution of the ADC (2^23 for ADS131M08, 2^24 for AD7771) +double BioListener::data_to_volts ( + double ref, uint32_t raw_code, double pga_gain, double adc_resolution) +{ + // Calculate the resolution in millivolts + double resolution = ref / (adc_resolution * pga_gain); + + // Compute the voltage in millivolts based on the raw ADC code + if (raw_code <= 0x7FFFFF) + { // Positive range + return resolution * (double)raw_code; + } + else + { // Negative range (two's complement) + return (resolution * (double)raw_code) - (ref / pga_gain); + } +} + + void BioListener::read_thread () { - const int max_size = 1024; + const int max_size = PACKET_SIZE; char message[max_size]; int num_rows = board_descr["default"]["num_rows"]; double *package = new double[num_rows]; @@ -159,90 +230,85 @@ void BioListener::read_thread () safe_logger (spdlog::level::trace, "no data received"); continue; } - std::string message_received = std::string (message, bytes_recv); - std::vector splitted_packages = - split_string (message_received, PACKET_DELIMITER_CSV); - - safe_logger (spdlog::level::trace, "Received message: >>>{}<<<", message_received.c_str ()); - - for (std::string recv_package : splitted_packages) + try { - safe_logger (spdlog::level::trace, "Received package: >>>{}<<<", recv_package.c_str ()); - try { - json j = json::parse (recv_package); -// json j = json::from_msgpack(recv_package); - - safe_logger (spdlog::level::trace, "Parsed json: {}", j.dump ()); - - - // parse from_msgpack + data_packet parsed_packet; + if (!parse_tcp_buffer (message, bytes_recv, parsed_packet)) + { + safe_logger (spdlog::level::err, "Failed to parse data packet"); + continue; + } + if (parsed_packet.type == 1) + { + package[board_descr["default"]["timestamp_channel"].get ()] = parsed_packet.ts; + package[board_descr["default"]["package_num_channel"].get ()] = + parsed_packet.n; + int sensor_id = parsed_packet.s_id; - if (j["type"] == 1) + for (int i = 0; i < BIOLISTENER_DATA_CHANNELS_COUNT; i++) { - package[board_descr["default"]["timestamp_channel"].get ()] = j["ts"]; - package[board_descr["default"]["package_num_channel"].get ()] = j["n"]; -// int sensor_id = j["s_id"]; - - // in data is array of 8 values, copy them to package - for (int i = 0; i < 8; i++) + double pga_gain; { - package[eeg_channels[i]] = j["data"][i]; + std::lock_guard lock (m_channels_gain); + pga_gain = channels_gain[i]; } - - push_package (package); + if (sensor_id == BIOLISTENER_ADC_AD7771) + { + static const float ref_microV = 2500000.0; + static const float adc_resolution = 16777216.0; + package[eeg_channels[i]] = data_to_volts ( + ref_microV, parsed_packet.data[i], pga_gain, adc_resolution); + } + else if (sensor_id == BIOLISTENER_ADC_ADS131M08) + { + static const double ref_microV = 1200000.0; + static const double adc_resolution = 8388608.0; + package[eeg_channels[i]] = data_to_volts ( + ref_microV, parsed_packet.data[i], pga_gain, adc_resolution); + } + else + { + safe_logger (spdlog::level::err, "Unknown sensor id: {}", sensor_id); + return; + } } - - - } catch (json::parse_error &e) { - safe_logger (spdlog::level::err, "Failed to parse json: {}", e.what ()); + push_package (package); } - - + } + catch (json::parse_error &e) + { + safe_logger (spdlog::level::err, "Failed to parse json: {}", e.what ()); } } delete[] package; } -std::vector BioListener::split_string (const std::string &package, char delim) -{ - std::vector result; - size_t start; - size_t end = 0; - while ((start = package.find_first_not_of (delim, end)) != std::string::npos) - { - end = package.find (delim, start); - std::string cur_str = package.substr (start, end - start); - result.push_back (cur_str); - } - return result; -} - - int BioListener::create_control_connection () { char local_ip[80]; - strncpy(local_ip, params.ip_address.c_str(), sizeof(local_ip) - 1); - local_ip[sizeof(local_ip) - 1] = '\0'; -// safe_logger (spdlog::level::info, "Local ip address is {}", params.ip_address.c_str ()); -// int local_ip_res = -// SocketClientUDP::get_local_ip_addr (params.ip_address.c_str (), DEFAULT_CONTROL_PORT, local_ip); -// if (local_ip_res != (int)SocketClientUDPReturnCodes::STATUS_OK) -// { -// safe_logger (spdlog::level::err, "failed to get local ip addr: {}", local_ip_res); -// return (int)BrainFlowExitCodes::GENERAL_ERROR; -// } + strncpy (local_ip, params.ip_address.c_str (), sizeof (local_ip) - 1); + local_ip[sizeof (local_ip) - 1] = '\0'; + // safe_logger (spdlog::level::info, "Local ip address is {}", params.ip_address.c_str ()); + // int local_ip_res = + // SocketClientUDP::get_local_ip_addr (params.ip_address.c_str (), DEFAULT_CONTROL_PORT, + // local_ip); + // if (local_ip_res != (int)SocketClientUDPReturnCodes::STATUS_OK) + // { + // safe_logger (spdlog::level::err, "failed to get local ip addr: {}", local_ip_res); + // return (int)BrainFlowExitCodes::GENERAL_ERROR; + // } safe_logger (spdlog::level::info, "local ip addr is {}", local_ip); int res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; for (int i = 0; i < 39; i += 2) { control_port = DEFAULT_CONTROL_PORT + i; - control_socket = new SocketServerTCP (local_ip, control_port, false); + control_socket = new SocketServerTCP (local_ip, control_port, true); if (control_socket->bind () == ((int)SocketServerTCPReturnCodes::STATUS_OK)) { safe_logger (spdlog::level::info, "use port {} for control", control_port); @@ -262,13 +328,45 @@ int BioListener::create_control_connection () int BioListener::send_control_msg (const char *msg) { - // FIXME: hardcoded ports -// // should never happen -// if ((control_port < 0) || (data_port < 0)) -// { -// safe_logger (spdlog::level::info, "ports for data or control are not set"); -// return (int)BrainFlowExitCodes::GENERAL_ERROR; -// } + // should never happen + if (control_port < 0) + { + safe_logger (spdlog::level::info, "ports for control are not set"); + return (int)BrainFlowExitCodes::GENERAL_ERROR; + } + + // try to convert msg from json to string + try + { + auto j = json::parse (msg); + if (j.contains ("command") && j["command"] == BIOLISTENER_COMMAND_SET_ADC_CHANNEL_PGA) + { + int channel = j["channel"]; + double pga = j["pga"]; + if (channel >= 0 && channel < BIOLISTENER_DATA_CHANNELS_COUNT) + { + std::lock_guard lock (m_channels_gain); + channels_gain[channel] = pga; + safe_logger (spdlog::level::info, "Set gain for channel: {}", channel); + safe_logger (spdlog::level::info, "Gain: {}", pga); + } + } + } + catch (json::parse_error &e) + { + safe_logger (spdlog::level::err, "Failed to parse json: {}", e.what ()); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + catch (json::exception &e) + { + safe_logger (spdlog::level::err, "Failed to parse json: {}", e.what ()); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + catch (...) + { + safe_logger (spdlog::level::err, "Failed to parse json"); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } // convert msg to string just by copying std::string package = msg; @@ -305,7 +403,7 @@ int BioListener::wait_for_connection () safe_logger (spdlog::level::trace, "waiting for accept {}/{}", i, max_attempts); if (control_socket->client_connected) { - safe_logger (spdlog::level::trace, "emotibit connected"); + safe_logger (spdlog::level::trace, "BioListener connected"); break; } else @@ -325,5 +423,3 @@ int BioListener::wait_for_connection () } return res; } - - diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h index 051db52a0..8e61714ec 100644 --- a/src/board_controller/biolistener/inc/biolistener.h +++ b/src/board_controller/biolistener/inc/biolistener.h @@ -10,10 +10,38 @@ #include "broadcast_server.h" #include "socket_client_udp.h" #include "socket_server_tcp.h" +#include "socket_server_udp.h" #define DEFAULT_CONTROL_PORT 12345 #define PACKET_DELIMITER_CSV '\n' +#define BIOLISTENER_COMMAND_UNDEFINED 0 +#define BIOLISTENER_COMMAND_SET_ADC_DATA_RATE 1 +#define BIOLISTENER_COMMAND_SET_ADC_CHANNEL_ENABLE 2 +#define BIOLISTENER_COMMAND_SET_ADC_CHANNEL_PGA 3 +#define BIOLISTENER_COMMAND_RESET_ADC 4 +#define BIOLISTENER_COMMAND_START_SAMPLING 5 +#define BIOLISTENER_COMMAND_STOP_SAMPLING 6 + +#define BIOLISTENER_ADC_ADS131M08 0 +#define BIOLISTENER_ADC_AD7771 1 + +#define BIOLISTENER_DATA_CHANNELS_COUNT 8 +#define BIOLISTENER_DEFAULT_PGA_GAIN 8 + +typedef struct data_packet +{ + uint8_t header; + uint32_t ts; + uint8_t type; + uint32_t n; + uint8_t s_id; + uint32_t data[BIOLISTENER_DATA_CHANNELS_COUNT]; + uint8_t footer; +} __attribute__ ((packed)) data_packet; + +const size_t PACKET_SIZE = sizeof (data_packet); + class BioListener : public Board { @@ -30,13 +58,18 @@ class BioListener : public Board int control_port; int data_port; + std::mutex m_channels_gain; + double channels_gain[BIOLISTENER_DATA_CHANNELS_COUNT] {0}; + void read_thread (); - std::vector split_string (const std::string &package, char delim); int create_control_connection (); int send_control_msg (const char *msg); int wait_for_connection (); + static double data_to_volts ( + double ref, uint32_t raw_code, double pga_gain, double adc_resolution); + public: BioListener (int board_id, struct BrainFlowInputParams params); ~BioListener (); @@ -46,4 +79,6 @@ class BioListener : public Board int stop_stream (); int release_session (); int config_board (std::string config, std::string &response); + + bool parse_tcp_buffer (const char *buffer, size_t buffer_size, data_packet &parsed_packet); }; From 5d99063ddc962e2e2f998bb8e78e2ae19f5965ac Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Mon, 16 Dec 2024 19:28:32 +0200 Subject: [PATCH 03/26] BioListener - corrected conversion to micro volts --- src/board_controller/biolistener/biolistener.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index 18582c780..ac493198c 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -257,17 +257,17 @@ void BioListener::read_thread () if (sensor_id == BIOLISTENER_ADC_AD7771) { - static const float ref_microV = 2500000.0; - static const float adc_resolution = 16777216.0; + static const double ref_microV = 2500000.0; + static const double adc_resolution = 16777216.0; package[eeg_channels[i]] = data_to_volts ( - ref_microV, parsed_packet.data[i], pga_gain, adc_resolution); + ref_microV, parsed_packet.data[i], pga_gain, adc_resolution) * 2.0; } else if (sensor_id == BIOLISTENER_ADC_ADS131M08) { static const double ref_microV = 1200000.0; - static const double adc_resolution = 8388608.0; + static const double adc_resolution = 16777216.0; package[eeg_channels[i]] = data_to_volts ( - ref_microV, parsed_packet.data[i], pga_gain, adc_resolution); + ref_microV, parsed_packet.data[i], pga_gain, adc_resolution) * 2.0; } else { From 7a1b0cef617453a73fa98e80ea6ad4b56239d3e9 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Tue, 17 Dec 2024 01:17:54 +0200 Subject: [PATCH 04/26] BioListener - refactored code --- src/board_controller/biolistener/biolistener.cpp | 6 +++--- src/board_controller/biolistener/inc/biolistener.h | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index ac493198c..25ca1a20c 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -168,13 +168,13 @@ bool BioListener::parse_tcp_buffer ( std::memcpy (&parsed_packet, buffer, PACKET_SIZE); // Validate the header and footer - if (parsed_packet.header != 0xA0) + if (parsed_packet.header != BIOLISTENER_DATA_PACKET_HEADER) { safe_logger (spdlog::level::trace, "Invalid header! Expected: 0xA0"); return false; } - if (parsed_packet.footer != 0xC0) + if (parsed_packet.footer != BIOLISTENER_DATA_PACKET_FOOTER) { safe_logger (spdlog::level::trace, "Invalid footer! Expected: 0xC0"); return false; @@ -240,7 +240,7 @@ void BioListener::read_thread () continue; } - if (parsed_packet.type == 1) + if (parsed_packet.type == BIOLISTENER_DATA_PACKET_BIOSIGNALS) { package[board_descr["default"]["timestamp_channel"].get ()] = parsed_packet.ts; package[board_descr["default"]["package_num_channel"].get ()] = diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h index 8e61714ec..edf102f37 100644 --- a/src/board_controller/biolistener/inc/biolistener.h +++ b/src/board_controller/biolistener/inc/biolistener.h @@ -23,6 +23,12 @@ #define BIOLISTENER_COMMAND_START_SAMPLING 5 #define BIOLISTENER_COMMAND_STOP_SAMPLING 6 +#define BIOLISTENER_DATA_PACKET_DEBUG 0 +#define BIOLISTENER_DATA_PACKET_BIOSIGNALS 1 + +#define BIOLISTENER_DATA_PACKET_HEADER 0xA0 +#define BIOLISTENER_DATA_PACKET_FOOTER 0xC0 + #define BIOLISTENER_ADC_ADS131M08 0 #define BIOLISTENER_ADC_AD7771 1 From 1f56f0ddb497d835bed585d5ef1b5b9da9c650b0 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Thu, 19 Dec 2024 01:12:05 +0200 Subject: [PATCH 05/26] BioListener - corrected timestamp, AUXILIARY_PRESET with IMU and battery voltage --- .../biolistener/biolistener.cpp | 36 +++++++++++++++++-- .../biolistener/inc/biolistener.h | 6 ++++ src/board_controller/brainflow_boards.cpp | 34 ++++++++++++------ 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index 25ca1a20c..d93fdf8ea 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -219,10 +219,15 @@ void BioListener::read_thread () package[i] = 0.0; } + bool first_data_packet_received = false; + std::vector eeg_channels = board_descr["default"]["eeg_channels"]; std::vector other_channels = board_descr["default"]["other_channels"]; + std::vector accel_channels = board_descr["auxiliary"]["accel_channels"]; + std::vector gyro_channels = board_descr["auxiliary"]["gyro_channels"]; + int temp_channel = board_descr["auxiliary"]["temperature_channels"][0]; - while (keep_alive) + while (keep_alive) { int bytes_recv = control_socket->recv (message, max_size); if (bytes_recv < 1) @@ -240,9 +245,16 @@ void BioListener::read_thread () continue; } + if (!first_data_packet_received) + { + // ENHANCEMENT: can be replaced with more accurate timestamp based on ntp or similar + timestamp_offset = get_timestamp () - ((double)parsed_packet.ts / 1000.0); + first_data_packet_received = true; + } + if (parsed_packet.type == BIOLISTENER_DATA_PACKET_BIOSIGNALS) { - package[board_descr["default"]["timestamp_channel"].get ()] = parsed_packet.ts; + package[board_descr["default"]["timestamp_channel"].get ()] = timestamp_offset + ((double)parsed_packet.ts / 1000.0); package[board_descr["default"]["package_num_channel"].get ()] = parsed_packet.n; int sensor_id = parsed_packet.s_id; @@ -277,6 +289,26 @@ void BioListener::read_thread () } push_package (package); } + else if (parsed_packet.type == BIOLISTENER_DATA_PACKET_IMU) + { + package[board_descr["default"]["timestamp_channel"].get ()] = timestamp_offset + ((double)parsed_packet.ts / 1000.0); + package[board_descr["default"]["package_num_channel"].get ()] = parsed_packet.n; + + for (int i = 0; i < 3; i++) + { + package[accel_channels[i]] = UINT32_TO_FLOAT(parsed_packet.data[i]); + package[gyro_channels[i]] = UINT32_TO_FLOAT(parsed_packet.data[i + 3]); + } + + package[temp_channel] = UINT32_TO_FLOAT(parsed_packet.data[6]); + package[board_descr["auxiliary"]["battery_channel"].get ()] = UINT32_TO_FLOAT(parsed_packet.data[7]); + + push_package (package, (int)BrainFlowPresets::AUXILIARY_PRESET); + } + else + { + safe_logger (spdlog::level::err, "Unknown data packet type: {}", parsed_packet.type); + } } catch (json::parse_error &e) { diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h index edf102f37..e4d758e0a 100644 --- a/src/board_controller/biolistener/inc/biolistener.h +++ b/src/board_controller/biolistener/inc/biolistener.h @@ -25,6 +25,7 @@ #define BIOLISTENER_DATA_PACKET_DEBUG 0 #define BIOLISTENER_DATA_PACKET_BIOSIGNALS 1 +#define BIOLISTENER_DATA_PACKET_IMU 2 #define BIOLISTENER_DATA_PACKET_HEADER 0xA0 #define BIOLISTENER_DATA_PACKET_FOOTER 0xC0 @@ -35,6 +36,9 @@ #define BIOLISTENER_DATA_CHANNELS_COUNT 8 #define BIOLISTENER_DEFAULT_PGA_GAIN 8 +#define FLOAT_TO_UINT32(x) (*((uint32_t *)&(x))) +#define UINT32_TO_FLOAT(x) (*((float *)&(x))) + typedef struct data_packet { uint8_t header; @@ -64,6 +68,8 @@ class BioListener : public Board int control_port; int data_port; + double timestamp_offset; + std::mutex m_channels_gain; double channels_gain[BIOLISTENER_DATA_CHANNELS_COUNT] {0}; diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index f3e150eff..dcbc7b74b 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -1109,16 +1109,30 @@ BrainFlowBoards::BrainFlowBoards() {"other_channels", {9, 10}} }; brainflow_boards_json["boards"]["58"]["default"] = - { - {"name", "BioListener"}, - {"sampling_rate", 500}, - {"timestamp_channel", 11}, - {"marker_channel",12}, - {"package_num_channel", 0}, - {"num_rows", 13}, - {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, - {"other_channels", {9, 10}} - }; + { + {"name", "BioListener"}, + {"sampling_rate", 500}, + {"timestamp_channel", 11}, + {"marker_channel", 12}, + {"package_num_channel", 0}, + {"num_rows", 13}, + {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, + {"other_channels", {9, 10}} + }; + brainflow_boards_json["boards"]["58"]["auxiliary"] = + { + {"name", "BioListener"}, + {"sampling_rate", 50}, + {"timestamp_channel", 11}, + {"marker_channel", 12}, + {"package_num_channel", 0}, + {"num_rows", 13}, + {"accel_channels", {1, 2, 3}}, + {"gyro_channels", {4, 5, 6}}, + {"temperature_channels", {7}}, + {"battery_channel", 8}, + {"other_channels", {9, 10}} + }; } BrainFlowBoards boards_struct; From 03375b402111f4168b4ff7a81304081116ed392b Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sat, 28 Dec 2024 21:13:07 +0200 Subject: [PATCH 06/26] BioListener - ip_port can be modified, package num mismatch (packet lost) detection --- .../biolistener/biolistener.cpp | 21 ++++++++++++++++++- .../biolistener/inc/biolistener.h | 1 - 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index d93fdf8ea..f641cff3c 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -257,6 +257,16 @@ void BioListener::read_thread () package[board_descr["default"]["timestamp_channel"].get ()] = timestamp_offset + ((double)parsed_packet.ts / 1000.0); package[board_descr["default"]["package_num_channel"].get ()] = parsed_packet.n; + + static uint32_t package_num_channel_last = parsed_packet.n - 1; + + if (package_num_channel_last + 1 != parsed_packet.n) + { + safe_logger (spdlog::level::err, "Package num mismatch BIOLISTENER_DATA_PACKET_BIOSIGNALS! Lost: {} packets", parsed_packet.n - package_num_channel_last + 1); + } + + package_num_channel_last = parsed_packet.n; + int sensor_id = parsed_packet.s_id; for (int i = 0; i < BIOLISTENER_DATA_CHANNELS_COUNT; i++) @@ -294,6 +304,15 @@ void BioListener::read_thread () package[board_descr["default"]["timestamp_channel"].get ()] = timestamp_offset + ((double)parsed_packet.ts / 1000.0); package[board_descr["default"]["package_num_channel"].get ()] = parsed_packet.n; + static uint32_t package_num_channel_last = parsed_packet.n - 1; + + if (package_num_channel_last + 1 != parsed_packet.n) + { + safe_logger (spdlog::level::err, "Package num mismatch BIOLISTENER_DATA_PACKET_IMU! Lost: {} packets", parsed_packet.n - package_num_channel_last + 1); + } + + package_num_channel_last = parsed_packet.n; + for (int i = 0; i < 3; i++) { package[accel_channels[i]] = UINT32_TO_FLOAT(parsed_packet.data[i]); @@ -339,7 +358,7 @@ int BioListener::create_control_connection () int res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; for (int i = 0; i < 39; i += 2) { - control_port = DEFAULT_CONTROL_PORT + i; + control_port = params.ip_port + i; control_socket = new SocketServerTCP (local_ip, control_port, true); if (control_socket->bind () == ((int)SocketServerTCPReturnCodes::STATUS_OK)) { diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h index e4d758e0a..b1a5d5d8d 100644 --- a/src/board_controller/biolistener/inc/biolistener.h +++ b/src/board_controller/biolistener/inc/biolistener.h @@ -12,7 +12,6 @@ #include "socket_server_tcp.h" #include "socket_server_udp.h" -#define DEFAULT_CONTROL_PORT 12345 #define PACKET_DELIMITER_CSV '\n' #define BIOLISTENER_COMMAND_UNDEFINED 0 From b7d2dcb3c9f0dec4bf2ff52724a0ab99c1f5a64b Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Fri, 3 Jan 2025 20:37:10 +0200 Subject: [PATCH 07/26] BioListener - simplified channel number change in packet, now template-based --- .../biolistener/biolistener.cpp | 151 ++++++++++++------ .../biolistener/inc/biolistener.h | 14 +- src/board_controller/board_controller.cpp | 2 +- 3 files changed, 112 insertions(+), 55 deletions(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index f641cff3c..0e5e1873d 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -2,10 +2,8 @@ #include #include -#include #include -#include "custom_cast.h" #include "json.hpp" #include "network_interfaces.h" #include "timestamp.h" @@ -13,7 +11,9 @@ using json = nlohmann::json; -BioListener::BioListener (int board_id, struct BrainFlowInputParams params) +template +BioListener::BioListener ( + int board_id, struct BrainFlowInputParams params) : Board (board_id, params) { control_socket = NULL; @@ -21,6 +21,9 @@ BioListener::BioListener (int board_id, struct BrainFlowInputParams params) initialized = false; control_port = -1; data_port = -1; + timestamp_offset = -1; + + packet_size = sizeof (data_packet); // Data channels gain to default value for (int i = 0; i < BIOLISTENER_DATA_CHANNELS_COUNT; i++) @@ -29,13 +32,15 @@ BioListener::BioListener (int board_id, struct BrainFlowInputParams params) } } -BioListener::~BioListener () +template +BioListener::~BioListener () { skip_logs = true; release_session (); } -int BioListener::prepare_session () +template +int BioListener::prepare_session () { if (initialized) { @@ -60,7 +65,6 @@ int BioListener::prepare_session () res = send_control_msg ((json_command.dump () + PACKET_DELIMITER_CSV).c_str ()); - // FIXME: sleep for 500ms to wait for reset std::this_thread::sleep_for (std::chrono::milliseconds (500)); } @@ -80,12 +84,16 @@ int BioListener::prepare_session () return res; } -int BioListener::config_board (std::string conf, std::string &response) +template +int BioListener::config_board ( + std::string conf, std::string &response) { return send_control_msg (conf.c_str ()); } -int BioListener::start_stream (int buffer_size, const char *streamer_params) +template +int BioListener::start_stream ( + int buffer_size, const char *streamer_params) { if (!initialized) { @@ -108,12 +116,13 @@ int BioListener::start_stream (int buffer_size, const char *streamer_params) if (res == (int)BrainFlowExitCodes::STATUS_OK) { keep_alive = true; - streaming_thread = std::thread ([this] { this->read_thread (); }); + streaming_thread = std::thread ([this] { read_thread (); }); } return (int)BrainFlowExitCodes::STATUS_OK; } -int BioListener::stop_stream () +template +int BioListener::stop_stream () { if (keep_alive) { @@ -131,7 +140,8 @@ int BioListener::stop_stream () return (int)BrainFlowExitCodes::STREAM_THREAD_IS_NOT_RUNNING; } -int BioListener::release_session () +template +int BioListener::release_session () { if (initialized) { @@ -153,11 +163,12 @@ int BioListener::release_session () return (int)BrainFlowExitCodes::STATUS_OK; } -bool BioListener::parse_tcp_buffer ( - const char *buffer, size_t buffer_size, data_packet &parsed_packet) +template +bool BioListener::parse_tcp_buffer (const char *buffer, + size_t buffer_size, data_packet &parsed_packet) { // Ensure the buffer size matches the packet size - if (buffer_size != PACKET_SIZE) + if (buffer_size != packet_size) { safe_logger (spdlog::level::trace, "Buffer size mismatch!"); @@ -165,7 +176,7 @@ bool BioListener::parse_tcp_buffer ( } // Copy the raw buffer into the struct - std::memcpy (&parsed_packet, buffer, PACKET_SIZE); + std::memcpy (&parsed_packet, buffer, packet_size); // Validate the header and footer if (parsed_packet.header != BIOLISTENER_DATA_PACKET_HEADER) @@ -190,7 +201,8 @@ bool BioListener::parse_tcp_buffer ( // raw_code: raw ADC code // pga_gain: gain of the PGA // adc_resolution: resolution of the ADC (2^23 for ADS131M08, 2^24 for AD7771) -double BioListener::data_to_volts ( +template +double BioListener::data_to_volts ( double ref, uint32_t raw_code, double pga_gain, double adc_resolution) { // Calculate the resolution in millivolts @@ -208,9 +220,10 @@ double BioListener::data_to_volts ( } -void BioListener::read_thread () +template +void BioListener::read_thread () { - const int max_size = PACKET_SIZE; + const int max_size = packet_size; char message[max_size]; int num_rows = board_descr["default"]["num_rows"]; double *package = new double[num_rows]; @@ -227,7 +240,7 @@ void BioListener::read_thread () std::vector gyro_channels = board_descr["auxiliary"]["gyro_channels"]; int temp_channel = board_descr["auxiliary"]["temperature_channels"][0]; - while (keep_alive) + while (keep_alive) { int bytes_recv = control_socket->recv (message, max_size); if (bytes_recv < 1) @@ -238,7 +251,7 @@ void BioListener::read_thread () try { - data_packet parsed_packet; + data_packet parsed_packet {}; if (!parse_tcp_buffer (message, bytes_recv, parsed_packet)) { safe_logger (spdlog::level::err, "Failed to parse data packet"); @@ -249,20 +262,57 @@ void BioListener::read_thread () { // ENHANCEMENT: can be replaced with more accurate timestamp based on ntp or similar timestamp_offset = get_timestamp () - ((double)parsed_packet.ts / 1000.0); + + // Check gain + for (int i = 0; i < BIOLISTENER_DATA_CHANNELS_COUNT; i++) + { + int sensor_id = parsed_packet.s_id; + + double pga_gain; + { + std::lock_guard lock (m_channels_gain); + pga_gain = channels_gain[i]; + } + + if (parsed_packet.s_id == BIOLISTENER_ADC_AD7771) + { + // 1-8 OK + if (pga_gain < 1 || pga_gain > 8) + { + safe_logger (spdlog::level::critical, "Invalid pga gain for AD7771: {}", + pga_gain); + break; + } + } + else if (parsed_packet.s_id == BIOLISTENER_ADC_ADS131M08) + { + // 1-128 OK + if (pga_gain < 1 || pga_gain > 128) + { + safe_logger (spdlog::level::critical, + "Invalid pga gain for ADS131M08: {}", pga_gain); + break; + } + } + } + first_data_packet_received = true; } if (parsed_packet.type == BIOLISTENER_DATA_PACKET_BIOSIGNALS) { - package[board_descr["default"]["timestamp_channel"].get ()] = timestamp_offset + ((double)parsed_packet.ts / 1000.0); - package[board_descr["default"]["package_num_channel"].get ()] = + package[board_descr["default"]["timestamp_channel"].template get ()] = + timestamp_offset + ((double)parsed_packet.ts / 1000.0); + package[board_descr["default"]["package_num_channel"].template get ()] = parsed_packet.n; static uint32_t package_num_channel_last = parsed_packet.n - 1; if (package_num_channel_last + 1 != parsed_packet.n) { - safe_logger (spdlog::level::err, "Package num mismatch BIOLISTENER_DATA_PACKET_BIOSIGNALS! Lost: {} packets", parsed_packet.n - package_num_channel_last + 1); + safe_logger (spdlog::level::err, + "Package num mismatch BIOLISTENER_DATA_PACKET_BIOSIGNALS! Lost: {} packets", + parsed_packet.n - package_num_channel_last + 1); } package_num_channel_last = parsed_packet.n; @@ -281,15 +331,17 @@ void BioListener::read_thread () { static const double ref_microV = 2500000.0; static const double adc_resolution = 16777216.0; - package[eeg_channels[i]] = data_to_volts ( - ref_microV, parsed_packet.data[i], pga_gain, adc_resolution) * 2.0; + package[eeg_channels[i]] = data_to_volts (ref_microV, parsed_packet.data[i], + pga_gain, adc_resolution) * + 2.0; } else if (sensor_id == BIOLISTENER_ADC_ADS131M08) { static const double ref_microV = 1200000.0; static const double adc_resolution = 16777216.0; - package[eeg_channels[i]] = data_to_volts ( - ref_microV, parsed_packet.data[i], pga_gain, adc_resolution) * 2.0; + package[eeg_channels[i]] = data_to_volts (ref_microV, parsed_packet.data[i], + pga_gain, adc_resolution) * + 2.0; } else { @@ -301,32 +353,38 @@ void BioListener::read_thread () } else if (parsed_packet.type == BIOLISTENER_DATA_PACKET_IMU) { - package[board_descr["default"]["timestamp_channel"].get ()] = timestamp_offset + ((double)parsed_packet.ts / 1000.0); - package[board_descr["default"]["package_num_channel"].get ()] = parsed_packet.n; + package[board_descr["default"]["timestamp_channel"].template get ()] = + timestamp_offset + ((double)parsed_packet.ts / 1000.0); + package[board_descr["default"]["package_num_channel"].template get ()] = + parsed_packet.n; static uint32_t package_num_channel_last = parsed_packet.n - 1; if (package_num_channel_last + 1 != parsed_packet.n) { - safe_logger (spdlog::level::err, "Package num mismatch BIOLISTENER_DATA_PACKET_IMU! Lost: {} packets", parsed_packet.n - package_num_channel_last + 1); + safe_logger (spdlog::level::err, + "Package num mismatch BIOLISTENER_DATA_PACKET_IMU! Lost: {} packets", + parsed_packet.n - package_num_channel_last + 1); } package_num_channel_last = parsed_packet.n; for (int i = 0; i < 3; i++) { - package[accel_channels[i]] = UINT32_TO_FLOAT(parsed_packet.data[i]); - package[gyro_channels[i]] = UINT32_TO_FLOAT(parsed_packet.data[i + 3]); + package[accel_channels[i]] = UINT32_TO_FLOAT (parsed_packet.data[i]); + package[gyro_channels[i]] = UINT32_TO_FLOAT (parsed_packet.data[i + 3]); } - package[temp_channel] = UINT32_TO_FLOAT(parsed_packet.data[6]); - package[board_descr["auxiliary"]["battery_channel"].get ()] = UINT32_TO_FLOAT(parsed_packet.data[7]); + package[temp_channel] = UINT32_TO_FLOAT (parsed_packet.data[6]); + package[board_descr["auxiliary"]["battery_channel"].template get ()] = + UINT32_TO_FLOAT (parsed_packet.data[7]); push_package (package, (int)BrainFlowPresets::AUXILIARY_PRESET); } else { - safe_logger (spdlog::level::err, "Unknown data packet type: {}", parsed_packet.type); + safe_logger ( + spdlog::level::err, "Unknown data packet type: {}", parsed_packet.type); } } catch (json::parse_error &e) @@ -338,21 +396,13 @@ void BioListener::read_thread () delete[] package; } -int BioListener::create_control_connection () +template +int BioListener::create_control_connection () { char local_ip[80]; strncpy (local_ip, params.ip_address.c_str (), sizeof (local_ip) - 1); local_ip[sizeof (local_ip) - 1] = '\0'; - // safe_logger (spdlog::level::info, "Local ip address is {}", params.ip_address.c_str ()); - // int local_ip_res = - // SocketClientUDP::get_local_ip_addr (params.ip_address.c_str (), DEFAULT_CONTROL_PORT, - // local_ip); - // if (local_ip_res != (int)SocketClientUDPReturnCodes::STATUS_OK) - // { - // safe_logger (spdlog::level::err, "failed to get local ip addr: {}", local_ip_res); - // return (int)BrainFlowExitCodes::GENERAL_ERROR; - // } safe_logger (spdlog::level::info, "local ip addr is {}", local_ip); int res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; @@ -377,7 +427,8 @@ int BioListener::create_control_connection () return res; } -int BioListener::send_control_msg (const char *msg) +template +int BioListener::send_control_msg (const char *msg) { // should never happen if (control_port < 0) @@ -437,7 +488,8 @@ int BioListener::send_control_msg (const char *msg) return res; } -int BioListener::wait_for_connection () +template +int BioListener::wait_for_connection () { int res = (int)BrainFlowExitCodes::STATUS_OK; int accept_res = control_socket->accept (); @@ -448,7 +500,7 @@ int BioListener::wait_for_connection () } else { - int max_attempts = 1500; + int max_attempts = 10; for (int i = 0; i < max_attempts; i++) { safe_logger (spdlog::level::trace, "waiting for accept {}/{}", i, max_attempts); @@ -474,3 +526,6 @@ int BioListener::wait_for_connection () } return res; } + +template class BioListener<8>; +template class BioListener<16>; diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h index b1a5d5d8d..0dbf8c7d7 100644 --- a/src/board_controller/biolistener/inc/biolistener.h +++ b/src/board_controller/biolistener/inc/biolistener.h @@ -32,13 +32,13 @@ #define BIOLISTENER_ADC_ADS131M08 0 #define BIOLISTENER_ADC_AD7771 1 -#define BIOLISTENER_DATA_CHANNELS_COUNT 8 #define BIOLISTENER_DEFAULT_PGA_GAIN 8 #define FLOAT_TO_UINT32(x) (*((uint32_t *)&(x))) #define UINT32_TO_FLOAT(x) (*((float *)&(x))) -typedef struct data_packet +template +struct data_packet { uint8_t header; uint32_t ts; @@ -47,11 +47,10 @@ typedef struct data_packet uint8_t s_id; uint32_t data[BIOLISTENER_DATA_CHANNELS_COUNT]; uint8_t footer; -} __attribute__ ((packed)) data_packet; - -const size_t PACKET_SIZE = sizeof (data_packet); +} __attribute__ ((packed)); +template class BioListener : public Board { @@ -67,6 +66,8 @@ class BioListener : public Board int control_port; int data_port; + size_t packet_size; + double timestamp_offset; std::mutex m_channels_gain; @@ -91,5 +92,6 @@ class BioListener : public Board int release_session (); int config_board (std::string config, std::string &response); - bool parse_tcp_buffer (const char *buffer, size_t buffer_size, data_packet &parsed_packet); + bool parse_tcp_buffer (const char *buffer, size_t buffer_size, + data_packet &parsed_packet); }; diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index 452a0fa3f..5512e338d 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -287,7 +287,7 @@ int prepare_session (int board_id, const char *json_brainflow_input_params) std::shared_ptr (new Knight ((int)BoardIds::NEUROPAWN_KNIGHT_BOARD, params)); break; case BoardIds::BIOLISTENER_BOARD: - board = std::shared_ptr (new BioListener (board_id, params)); + board = std::shared_ptr (new BioListener<8> (board_id, params)); break; default: return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; From 400784b927249107dae438df4fece3c6b39533b5 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Fri, 3 Jan 2025 20:58:17 +0200 Subject: [PATCH 08/26] BioListener - emulator --- .github/workflows/run_unix.yml | 2 + .github/workflows/run_windows.yml | 9 + .../biolistener_emulator.py | 166 ++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 emulator/brainflow_emulator/biolistener_emulator.py diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index 8458078b1..4ba9eb061 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -251,6 +251,8 @@ jobs: run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1 - name: Streaming Python Markers run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/markers.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1 + - name: BioListener Python + run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/biolistener_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 - name: Denoising Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/denoising.py - name: Serialization Python diff --git a/.github/workflows/run_windows.yml b/.github/workflows/run_windows.yml index 1fa6b4174..161db57c3 100644 --- a/.github/workflows/run_windows.yml +++ b/.github/workflows/run_windows.yml @@ -186,6 +186,15 @@ jobs: - name: KnightBoard Windows Python Test run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\knightboard_windows.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 57 --serial-port shell: cmd + - name: BioListener Windows Python Test + run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 + shell: cmd + - name: BioListener Windows Cpp Test + run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\cpp_package\examples\get_data\build\Release\brainflow_get_data.exe --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 + shell: cmd + - name: BioListener Windows C# Test + run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\csharp_package\brainflow\examples\brainflow_get_data\bin\Release\net7.0\brainflow_get_data.exe --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 + shell: cmd # Signal Processing Testing - name: Serialization Rust Test run: | diff --git a/emulator/brainflow_emulator/biolistener_emulator.py b/emulator/brainflow_emulator/biolistener_emulator.py new file mode 100644 index 000000000..cac977f2b --- /dev/null +++ b/emulator/brainflow_emulator/biolistener_emulator.py @@ -0,0 +1,166 @@ +import datetime +import enum +import json +import logging +import random +import socket +import struct +import subprocess +import sys +import threading +import time + +from brainflow_emulator.emulate_common import TestFailureError, log_multilines + +BIOLISTENER_DATA_CHANNELS_COUNT = 8 + +BIOLISTENER_DATA_PACKET_DEBUG = 0 +BIOLISTENER_DATA_PACKET_BIOSIGNALS = 1 +BIOLISTENER_DATA_PACKET_IMU = 2 + +ADC_USED = 0 # ADS131M08 + + +class DataPacket: + FORMAT_STRING = f'=1B 1I 1B 1I 1B {BIOLISTENER_DATA_CHANNELS_COUNT}I 1B' + + def __init__(self, ts, tp, n, s_id, data): + self.header = 0xA0 + self.ts = ts + self.type = tp + self.n = n + self.s_id = s_id + self.data = data + self.footer = 0xC0 + + def pack(self): + return struct.pack(self.FORMAT_STRING, self.header, self.ts, self.type, self.n, self.s_id, *self.data, self.footer) + + @classmethod + def unpack(cls, packed_data): + format_string = cls.FORMAT_STRING + unpacked_data = struct.unpack(format_string, packed_data) + return cls(*unpacked_data) + + def __repr__(self): + return (f'DataPacket(header={self.header}, ts={self.ts}, type={self.type}, ' + f'n={self.n}, s_id={self.s_id}, data={self.data}, footer={self.footer})') + + +class State(enum.Enum): + wait = 'wait' + stream = 'stream' + + +def test_socket(cmd_list): + logging.info('Running %s' % ' '.join([str(x) for x in cmd_list])) + process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + log_multilines(logging.info, stdout) + log_multilines(logging.info, stderr) + + if process.returncode != 0: + raise TestFailureError('Test failed with exit code %s' % str(process.returncode), process.returncode) + + return stdout, stderr + + +def run_socket_server(): + thread = BioListenerEmulator() + thread.start() + return thread + + +class BioListenerEmulator(threading.Thread): + + def __init__(self): + threading.Thread.__init__(self) + self.local_ip = '127.0.0.1' + self.local_port = 12345 + self.server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) + self.server_socket.settimeout(0.01) + self.state = State.stream.value + self.package_num = 0 + self.keep_alive = True + + @staticmethod + def volts_to_data(ref, voltage, pga_gain, adc_resolution): + resolution = ref / (adc_resolution * pga_gain) + + if voltage >= 0: # Positive range + raw_code = voltage / resolution + else: # Negative range + raw_code = (voltage + (ref / pga_gain)) / resolution + + raw_code = int(raw_code) + raw_code = max(0, min(0xFFFFFF, raw_code)) # Ensure within 24 bit range + + return raw_code + + def run(self): + while self.keep_alive: + try: + self.server_socket.connect((self.local_ip, self.local_port)) + break + except: + time.sleep(0.1) + + started_at = time.time() + while self.keep_alive: + new_data_packet = DataPacket( + ts=int((time.time() - started_at) * 1000), + tp=BIOLISTENER_DATA_PACKET_BIOSIGNALS, + n=self.package_num, + s_id=ADC_USED, + data=[ + self.volts_to_data( + ref=2500000.0, + voltage=random.uniform(-1000, 1000), + pga_gain=8, + adc_resolution=16777216.0 + ) for _ in range(BIOLISTENER_DATA_CHANNELS_COUNT) + ] + ) + self.package_num += 1 + + try: + data = self.server_socket.recv(1024) + message = data.decode('utf-8').strip() + + if message: + for message_part in message.split("\n"): + logging.info(f"BioListener received command: {message_part}") + json_str = json.loads(message_part) + if json_str["command"] in (1, 2, 3, 4): + logging.info("Command ignored - simulator supports only start and stop stream command") + elif json_str["command"] == 5: + self.state = State.stream.value + elif json_str["command"] == 6: + self.state = State.wait.value + except TimeoutError: + pass + except Exception as err: + logging.debug("Error in recv thread") + + try: + if self.state == State.stream.value: + self.server_socket.sendall(new_data_packet.pack()) + except ConnectionResetError: + print(f"Connection lost") + except Exception as e: + print(f"Error: {e}") + + +def main(cmd_list): + if not cmd_list: + raise Exception('No command to execute') + server_thread = run_socket_server() + test_socket(cmd_list) + server_thread.keep_alive = False + server_thread.join() + + +if __name__ == '__main__': + logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=logging.INFO) + main(sys.argv[1:]) From 1a79c291d21406acfb163c5465eb06201b18ccd1 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Fri, 3 Jan 2025 22:06:37 +0200 Subject: [PATCH 09/26] BioListener - run workflows --- src/board_controller/biolistener/biolistener.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index 0e5e1873d..b1ce8dc0b 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -520,7 +520,7 @@ int BioListener::wait_for_connection () } if (!control_socket->client_connected) { - safe_logger (spdlog::level::trace, "failed to establish connection"); + safe_logger (spdlog::level::trace, "BioListener - failed to establish connection"); res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; } } From a9bae1e698e0ee998942eaf2754ded77d47da83f Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Fri, 3 Jan 2025 22:33:52 +0200 Subject: [PATCH 10/26] BioListener - compiler independent --- src/board_controller/biolistener/inc/biolistener.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h index 0dbf8c7d7..caa6ac419 100644 --- a/src/board_controller/biolistener/inc/biolistener.h +++ b/src/board_controller/biolistener/inc/biolistener.h @@ -37,6 +37,7 @@ #define FLOAT_TO_UINT32(x) (*((uint32_t *)&(x))) #define UINT32_TO_FLOAT(x) (*((float *)&(x))) +#pragma pack(push, 1) template struct data_packet { @@ -47,7 +48,8 @@ struct data_packet uint8_t s_id; uint32_t data[BIOLISTENER_DATA_CHANNELS_COUNT]; uint8_t footer; -} __attribute__ ((packed)); +}; +#pragma pack(pop) template From 1db4ef3fe5e61fd5bcbb6f76289f96c40164f319 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Fri, 3 Jan 2025 22:57:40 +0200 Subject: [PATCH 11/26] BioListener - compiler independent --- src/board_controller/biolistener/biolistener.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index b1ce8dc0b..3ea72a562 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -223,8 +223,7 @@ double BioListener::data_to_volts ( template void BioListener::read_thread () { - const int max_size = packet_size; - char message[max_size]; + char message[sizeof (data_packet)]; int num_rows = board_descr["default"]["num_rows"]; double *package = new double[num_rows]; for (int i = 0; i < num_rows; i++) @@ -242,7 +241,8 @@ void BioListener::read_thread () while (keep_alive) { - int bytes_recv = control_socket->recv (message, max_size); + int bytes_recv = + control_socket->recv (message, sizeof (data_packet)); if (bytes_recv < 1) { safe_logger (spdlog::level::trace, "no data received"); From 4be29ed201746e93e1ba4a4a798742be9cce94dc Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Fri, 3 Jan 2025 23:44:15 +0200 Subject: [PATCH 12/26] BioListener - run workflows --- emulator/brainflow_emulator/biolistener_emulator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/emulator/brainflow_emulator/biolistener_emulator.py b/emulator/brainflow_emulator/biolistener_emulator.py index cac977f2b..2f6085d29 100644 --- a/emulator/brainflow_emulator/biolistener_emulator.py +++ b/emulator/brainflow_emulator/biolistener_emulator.py @@ -147,9 +147,9 @@ def run(self): if self.state == State.stream.value: self.server_socket.sendall(new_data_packet.pack()) except ConnectionResetError: - print(f"Connection lost") + logging.debug(f"Connection lost") except Exception as e: - print(f"Error: {e}") + logging.debug(f"Error: {e}") def main(cmd_list): From 243ac3b43882c82616512ca6113c3aa02a602030 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sat, 4 Jan 2025 00:24:35 +0200 Subject: [PATCH 13/26] BioListener - emulator --- .github/workflows/run_unix.yml | 2 -- .github/workflows/run_windows.yml | 6 ------ 2 files changed, 8 deletions(-) diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index b1184b904..dc5ea949e 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -251,8 +251,6 @@ jobs: run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1 - name: Streaming Python Markers run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/markers.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1 - - name: BioListener Python - run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/biolistener_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 - name: Denoising Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/denoising.py - name: Serialization Python diff --git a/.github/workflows/run_windows.yml b/.github/workflows/run_windows.yml index 161db57c3..7d6cadbad 100644 --- a/.github/workflows/run_windows.yml +++ b/.github/workflows/run_windows.yml @@ -189,12 +189,6 @@ jobs: - name: BioListener Windows Python Test run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 shell: cmd - - name: BioListener Windows Cpp Test - run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\cpp_package\examples\get_data\build\Release\brainflow_get_data.exe --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 - shell: cmd - - name: BioListener Windows C# Test - run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\csharp_package\brainflow\examples\brainflow_get_data\bin\Release\net7.0\brainflow_get_data.exe --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 - shell: cmd # Signal Processing Testing - name: Serialization Rust Test run: | From 721356f1956a7b62fe00fb69e0340f73d92120bb Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sat, 18 Jan 2025 13:29:25 +0200 Subject: [PATCH 14/26] BioListener - docs --- docs/SupportedBoards.rst | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst index 8ede7cbd6..01d661150 100644 --- a/docs/SupportedBoards.rst +++ b/docs/SupportedBoards.rst @@ -1342,3 +1342,44 @@ Supported platforms: - Linux - MacOS - Devices like Raspberry Pi + + +BioListener +-------- + +BioListener +~~~~~~~~~~~~~ + +.. image:: https://live.staticflickr.com/65535/54273076343_6a7eb99697_k.jpg + :width: 519px + :height: 389px + +`BioListener website `_ + +To create such board you need to specify the following board ID and fields of BrainFlowInputParams object: + +- :code:`BoardIds.BIOLISTENER_BOARD` +- :code:`ip_port`, any local port which is currently free (and is selected on BioListener board), e.g. 12345 +- :code:`ip_address`, ip address of the current device, BrainFlow acts as a server. Use 0.0.0.0 to make the server listen on all network interfaces. + +Initialization Example: + +.. code-block:: python + + params = BrainFlowInputParams() + params.ip_port = 12345 + params.ip_address = "0.0.0.0" + board = BoardShim(BoardIds.BIOLISTENER_BOARD, params) + +Supported platforms: + +- Windows +- Linux +- MacOS +- Devices like Raspberry Pi +- Android + +Available :ref:`presets-label`: + +- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG (EMG, ECG, EOG) data +- :code:`BrainFlowPresets.AUXILIARY_PRESET`, it contains Gyro, Accel, battery and ESP32 chip temperature data From bf2afcb752f2314d902a605d2fb999198b167a3b Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sat, 18 Jan 2025 13:34:44 +0200 Subject: [PATCH 15/26] BioListener - all channel types --- src/board_controller/brainflow_boards.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index dcbc7b74b..667939b11 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -1117,6 +1117,9 @@ BrainFlowBoards::BrainFlowBoards() {"package_num_channel", 0}, {"num_rows", 13}, {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, + {"emg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, + {"ecg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, + {"eog_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, {"other_channels", {9, 10}} }; brainflow_boards_json["boards"]["58"]["auxiliary"] = From 72b328c837d60b0a27af1e348d6fd29b1c9bc4fb Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sun, 2 Mar 2025 22:52:52 +0200 Subject: [PATCH 16/26] BioListener - merge fixes --- .github/workflows/run_windows.yml | 2 +- csharp_package/brainflow/brainflow/board_controller_library.cs | 3 ++- java_package/brainflow/src/main/java/brainflow/BoardIds.java | 1 + julia_package/brainflow/src/board_shim.jl | 3 ++- matlab_package/brainflow/BoardIds.m | 3 ++- nodejs_package/brainflow/brainflow.types.ts | 3 ++- python_package/brainflow/board_shim.py | 3 ++- rust_package/brainflow/src/ffi/constants.rs | 3 ++- 8 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run_windows.yml b/.github/workflows/run_windows.yml index 7d6cadbad..9db171151 100644 --- a/.github/workflows/run_windows.yml +++ b/.github/workflows/run_windows.yml @@ -187,7 +187,7 @@ jobs: run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\knightboard_windows.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 57 --serial-port shell: cmd - name: BioListener Windows Python Test - run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 58 --ip-address 127.0.0.1 --ip-port 12345 + run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 64 --ip-address 127.0.0.1 --ip-port 12345 shell: cmd # Signal Processing Testing - name: Serialization Rust Test diff --git a/csharp_package/brainflow/brainflow/board_controller_library.cs b/csharp_package/brainflow/brainflow/board_controller_library.cs index aa34c508a..10a910621 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -122,7 +122,8 @@ public enum BoardIds OB5000_8_CHANNELS_BOARD = 60, SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61, SYNCHRONI_UNO_1_CHANNELS_BOARD = 62, - BIOLISTENER_BOARD = 63, + OB3000_24_CHANNELS_BOARD = 63, + BIOLISTENER_BOARD = 64, }; diff --git a/java_package/brainflow/src/main/java/brainflow/BoardIds.java b/java_package/brainflow/src/main/java/brainflow/BoardIds.java index e8c2acd65..487d8fc5a 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardIds.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardIds.java @@ -72,6 +72,7 @@ public enum BoardIds OB5000_8_CHANNELS_BOARD(60), SYNCHRONI_PENTO_8_CHANNELS_BOARD(61), SYNCHRONI_UNO_1_CHANNELS_BOARD(62), + OB3000_24_CHANNELS_BOARD(63), BIOLISTENER_BOARD(64); private final int board_id; diff --git a/julia_package/brainflow/src/board_shim.jl b/julia_package/brainflow/src/board_shim.jl index 8c72292f6..3e7086093 100644 --- a/julia_package/brainflow/src/board_shim.jl +++ b/julia_package/brainflow/src/board_shim.jl @@ -67,7 +67,8 @@ export BrainFlowInputParams OB5000_8_CHANNELS_BOARD = 60 SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61 SYNCHRONI_UNO_1_CHANNELS_BOARD = 62 - BIOLISTENER_BOARD = 63 + OB3000_24_CHANNELS_BOARD = 63 + BIOLISTENER_BOARD = 64 end diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m index ea465b821..cc242952d 100644 --- a/matlab_package/brainflow/BoardIds.m +++ b/matlab_package/brainflow/BoardIds.m @@ -65,6 +65,7 @@ OB5000_8_CHANNELS_BOARD(60) SYNCHRONI_PENTO_8_CHANNELS_BOARD(61) SYNCHRONI_UNO_1_CHANNELS_BOARD(62) - BIOLISTENER_BOARD(63) + OB3000_24_CHANNELS_BOARD(63) + BIOLISTENER_BOARD(64) end end \ No newline at end of file diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index 1b138fc8a..f55f10187 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -75,7 +75,8 @@ export enum BoardIds { OB5000_8_CHANNELS_BOARD = 60, SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61, SYNCHRONI_UNO_1_CHANNELS_BOARD = 62, - BIOLISTENER_BOARD = 63 + OB3000_24_CHANNELS_BOARD = 63, + BIOLISTENER_BOARD = 64 } export enum IpProtocolTypes { diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index 29e58441c..0f98d001f 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -80,7 +80,8 @@ class BoardIds(enum.IntEnum): OB5000_8_CHANNELS_BOARD = 60 #: SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61 #: SYNCHRONI_UNO_1_CHANNELS_BOARD = 62 #: - BIOLISTENER_BOARD = 63 #: + OB3000_24_CHANNELS_BOARD = 63 #: + BIOLISTENER_BOARD = 64 #: class IpProtocolTypes(enum.IntEnum): diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index 977590759..49453de73 100644 --- a/rust_package/brainflow/src/ffi/constants.rs +++ b/rust_package/brainflow/src/ffi/constants.rs @@ -101,7 +101,8 @@ pub enum BoardIds { OB50008CHannelsBoard= 60 , SynchroniPento8ChannelsBoard = 61, SynchroniUno1ChannelsBoard = 62, - BiolistenerBoard = 63 + OB300024ChannelsBoard = 63, + BiolistenerBoard = 64 } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] From 4553cc8bfc7a3c2dc51e08a59bbbe95eb9047a38 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sun, 2 Mar 2025 23:19:26 +0200 Subject: [PATCH 17/26] BioListener - merge fixes --- src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp index 362fa1ad3..5901f436c 100644 --- a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp +++ b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp @@ -1,3 +1,4 @@ +#include #include #include #include From 1006bad09386d7aa34989a3b6f149ed160bb6acc Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Sun, 2 Mar 2025 23:21:28 +0200 Subject: [PATCH 18/26] BioListener - merge fixes --- src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp index 5901f436c..943d4f18a 100644 --- a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp +++ b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include #include From 56fd1806b12a7f288df6e5682bfd6f5768c6a899 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Mon, 3 Mar 2025 00:20:10 +0200 Subject: [PATCH 19/26] BioListener - emulator more logs added --- emulator/brainflow_emulator/biolistener_emulator.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) mode change 100644 => 100755 emulator/brainflow_emulator/biolistener_emulator.py diff --git a/emulator/brainflow_emulator/biolistener_emulator.py b/emulator/brainflow_emulator/biolistener_emulator.py old mode 100644 new mode 100755 index 2f6085d29..5c1be7df0 --- a/emulator/brainflow_emulator/biolistener_emulator.py +++ b/emulator/brainflow_emulator/biolistener_emulator.py @@ -79,10 +79,11 @@ def __init__(self): self.local_ip = '127.0.0.1' self.local_port = 12345 self.server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) - self.server_socket.settimeout(0.01) + self.server_socket.settimeout(1) self.state = State.stream.value self.package_num = 0 self.keep_alive = True + logging.info(f"BioListener emulator started") @staticmethod def volts_to_data(ref, voltage, pga_gain, adc_resolution): @@ -106,6 +107,8 @@ def run(self): except: time.sleep(0.1) + logging.info(f"BioListener emulator connected to {self.local_ip}:{self.local_port}") + started_at = time.time() while self.keep_alive: new_data_packet = DataPacket( @@ -141,15 +144,15 @@ def run(self): except TimeoutError: pass except Exception as err: - logging.debug("Error in recv thread") + logging.error("Error in recv thread") try: if self.state == State.stream.value: self.server_socket.sendall(new_data_packet.pack()) except ConnectionResetError: - logging.debug(f"Connection lost") + logging.error(f"Connection lost") except Exception as e: - logging.debug(f"Error: {e}") + logging.error(f"Error: {e}") def main(cmd_list): From b341a2574c021b530691fa88a9e1bc15d45c844c Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Mon, 3 Mar 2025 01:02:01 +0200 Subject: [PATCH 20/26] BioListener - emulator more logs added --- emulator/brainflow_emulator/biolistener_emulator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/emulator/brainflow_emulator/biolistener_emulator.py b/emulator/brainflow_emulator/biolistener_emulator.py index 5c1be7df0..89eb8a9d0 100755 --- a/emulator/brainflow_emulator/biolistener_emulator.py +++ b/emulator/brainflow_emulator/biolistener_emulator.py @@ -138,13 +138,17 @@ def run(self): if json_str["command"] in (1, 2, 3, 4): logging.info("Command ignored - simulator supports only start and stop stream command") elif json_str["command"] == 5: + logging.info("Start stream command received") self.state = State.stream.value elif json_str["command"] == 6: + logging.info("Stop stream command received") self.state = State.wait.value + else: + logging.warning(f"Unknown command: {json_str['command']}") except TimeoutError: pass except Exception as err: - logging.error("Error in recv thread") + logging.error(f"Error in recv thread: {err}") try: if self.state == State.stream.value: From 532a3f671d219d4c180c7830dc00e9a85f6abeb0 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Mon, 3 Mar 2025 01:04:38 +0200 Subject: [PATCH 21/26] BioListener - emulator --- emulator/brainflow_emulator/biolistener_emulator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emulator/brainflow_emulator/biolistener_emulator.py b/emulator/brainflow_emulator/biolistener_emulator.py index 89eb8a9d0..0f94aa19f 100755 --- a/emulator/brainflow_emulator/biolistener_emulator.py +++ b/emulator/brainflow_emulator/biolistener_emulator.py @@ -80,7 +80,7 @@ def __init__(self): self.local_port = 12345 self.server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) self.server_socket.settimeout(1) - self.state = State.stream.value + self.state = State.wait.value self.package_num = 0 self.keep_alive = True logging.info(f"BioListener emulator started") From 9f0c72982bea7f1873c4d769cb85127cbd579d6e Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Mon, 3 Mar 2025 01:08:49 +0200 Subject: [PATCH 22/26] BioListener - emulator --- .github/workflows/run_unix.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index dc5ea949e..1c7a3d336 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -251,6 +251,8 @@ jobs: run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1 - name: Streaming Python Markers run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/markers.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1 + - name: BioListener Python + run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/biolistener_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id 64 --ip-address 127.0.0.1 --ip-port 12345 - name: Denoising Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/denoising.py - name: Serialization Python From 17f7e3f7543d58cf1eba662f42029d8d45031c60 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Mon, 3 Mar 2025 02:12:25 +0200 Subject: [PATCH 23/26] BioListener - emulator --- .../biolistener_emulator.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/emulator/brainflow_emulator/biolistener_emulator.py b/emulator/brainflow_emulator/biolistener_emulator.py index 0f94aa19f..128cbfc0b 100755 --- a/emulator/brainflow_emulator/biolistener_emulator.py +++ b/emulator/brainflow_emulator/biolistener_emulator.py @@ -83,6 +83,7 @@ def __init__(self): self.state = State.wait.value self.package_num = 0 self.keep_alive = True + self.connection_established = False logging.info(f"BioListener emulator started") @staticmethod @@ -100,14 +101,21 @@ def volts_to_data(ref, voltage, pga_gain, adc_resolution): return raw_code def run(self): + logging.info(f"BioListener emulator connecting to {self.local_ip}:{self.local_port}...") while self.keep_alive: try: self.server_socket.connect((self.local_ip, self.local_port)) + self.connection_established = True break - except: + except Exception as err: + logging.warning(f"Error connecting to {self.local_ip}:{self.local_port}: {err}") time.sleep(0.1) - logging.info(f"BioListener emulator connected to {self.local_ip}:{self.local_port}") + if self.connection_established: + logging.info(f"BioListener emulator connected to {self.local_ip}:{self.local_port}") + else: + logging.error(f"BioListener emulator failed to connect to {self.local_ip}:{self.local_port}") + return started_at = time.time() while self.keep_alive: @@ -147,6 +155,8 @@ def run(self): logging.warning(f"Unknown command: {json_str['command']}") except TimeoutError: pass + except socket.timeout: + pass except Exception as err: logging.error(f"Error in recv thread: {err}") @@ -163,9 +173,12 @@ def main(cmd_list): if not cmd_list: raise Exception('No command to execute') server_thread = run_socket_server() - test_socket(cmd_list) - server_thread.keep_alive = False - server_thread.join() + + try: + test_socket(cmd_list) + finally: + server_thread.keep_alive = False + server_thread.join() if __name__ == '__main__': From f3ef60ae31a8b685969b5722ce93fa6e5595a9b2 Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Mon, 3 Mar 2025 02:44:35 +0200 Subject: [PATCH 24/26] BioListener - emulator --- emulator/brainflow_emulator/biolistener_emulator.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/emulator/brainflow_emulator/biolistener_emulator.py b/emulator/brainflow_emulator/biolistener_emulator.py index 128cbfc0b..310744374 100755 --- a/emulator/brainflow_emulator/biolistener_emulator.py +++ b/emulator/brainflow_emulator/biolistener_emulator.py @@ -79,6 +79,7 @@ def __init__(self): self.local_ip = '127.0.0.1' self.local_port = 12345 self.server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_socket.settimeout(1) self.state = State.wait.value self.package_num = 0 @@ -102,13 +103,22 @@ def volts_to_data(ref, voltage, pga_gain, adc_resolution): def run(self): logging.info(f"BioListener emulator connecting to {self.local_ip}:{self.local_port}...") - while self.keep_alive: + while self.keep_alive and not self.connection_established: try: self.server_socket.connect((self.local_ip, self.local_port)) self.connection_established = True break except Exception as err: logging.warning(f"Error connecting to {self.local_ip}:{self.local_port}: {err}") + # A failed connect may leave the socket unusable. + try: + self.server_socket.close() + except Exception: + pass + # Recreate the socket with the same options. + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server_socket.settimeout(1) time.sleep(0.1) if self.connection_established: From 5450bd80e6491fa9c944ab87d527397b1176839a Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Fri, 7 Mar 2025 21:30:43 +0200 Subject: [PATCH 25/26] BioListener - PR corrections - defines moved to the separate file, added default values, minor fixes, docs updates --- docs/SupportedBoards.rst | 7 ++-- .../biolistener/biolistener.cpp | 34 +++++++++++++------ .../biolistener/inc/biolistener.h | 30 ++-------------- .../biolistener/inc/biolistener_defines.h | 29 ++++++++++++++++ 4 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 src/board_controller/biolistener/inc/biolistener_defines.h diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst index 16918b89c..3368ac24a 100644 --- a/docs/SupportedBoards.rst +++ b/docs/SupportedBoards.rst @@ -1364,8 +1364,11 @@ BioListener To create such board you need to specify the following board ID and fields of BrainFlowInputParams object: - :code:`BoardIds.BIOLISTENER_BOARD` -- :code:`ip_port`, any local port which is currently free (and is selected on BioListener board), e.g. 12345 -- :code:`ip_address`, ip address of the current device, BrainFlow acts as a server. Use 0.0.0.0 to make the server listen on all network interfaces. +- *optional:* :code:`ip_address`, ip address of the machine running the BrainFlow server (not the end device). If not provided, the server will listen on all network interfaces (at `0.0.0.0`) +- *optional:* :code:`ip_port`, any free local port. If the chosen port is in use, the next available free port will be used. If not provided, the search for a free port starts at `12345` +- *optional:* :code:`timeout`, timeout for device discovery, default is 3sec + +Make sure to configure the BioListener board as stated in the `BioListener documentation `_ to connect to the BrainFlow server. Initialization Example: diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index 3ea72a562..22da26095 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -8,6 +8,8 @@ #include "network_interfaces.h" #include "timestamp.h" +#include "biolistener_defines.h" + using json = nlohmann::json; @@ -50,7 +52,20 @@ int BioListener::prepare_session () if (params.timeout < 2) { - params.timeout = 4; + params.timeout = 3; + safe_logger (spdlog::level::warn, "Timeout is too low, setting to 3 sec"); + } + + if (params.ip_address.empty ()) + { + params.ip_address = "0.0.0.0"; + safe_logger (spdlog::level::warn, "IP address is not set, listening on all network interfaces"); + } + + if (params.ip_port <= 0) + { + params.ip_port = 12345; + safe_logger (spdlog::level::warn, "IP port is not set, using default value 12345"); } int res = create_control_connection (); @@ -171,7 +186,6 @@ bool BioListener::parse_tcp_buffer (const char if (buffer_size != packet_size) { safe_logger (spdlog::level::trace, "Buffer size mismatch!"); - return false; } @@ -237,7 +251,7 @@ void BioListener::read_thread () std::vector other_channels = board_descr["default"]["other_channels"]; std::vector accel_channels = board_descr["auxiliary"]["accel_channels"]; std::vector gyro_channels = board_descr["auxiliary"]["gyro_channels"]; - int temp_channel = board_descr["auxiliary"]["temperature_channels"][0]; + int temperature_channel = board_descr["auxiliary"]["temperature_channels"][0]; while (keep_alive) { @@ -353,9 +367,9 @@ void BioListener::read_thread () } else if (parsed_packet.type == BIOLISTENER_DATA_PACKET_IMU) { - package[board_descr["default"]["timestamp_channel"].template get ()] = + package[board_descr["auxiliary"]["timestamp_channel"].template get ()] = timestamp_offset + ((double)parsed_packet.ts / 1000.0); - package[board_descr["default"]["package_num_channel"].template get ()] = + package[board_descr["auxiliary"]["package_num_channel"].template get ()] = parsed_packet.n; static uint32_t package_num_channel_last = parsed_packet.n - 1; @@ -375,7 +389,7 @@ void BioListener::read_thread () package[gyro_channels[i]] = UINT32_TO_FLOAT (parsed_packet.data[i + 3]); } - package[temp_channel] = UINT32_TO_FLOAT (parsed_packet.data[6]); + package[temperature_channel] = UINT32_TO_FLOAT (parsed_packet.data[6]); package[board_descr["auxiliary"]["battery_channel"].template get ()] = UINT32_TO_FLOAT (parsed_packet.data[7]); @@ -406,7 +420,7 @@ int BioListener::create_control_connection () safe_logger (spdlog::level::info, "local ip addr is {}", local_ip); int res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; - for (int i = 0; i < 39; i += 2) + for (int i = 0; i < BIOLISTENER_MAX_PORTS_TO_TRY_TILL_FREE_FOUND; i += 1) { control_port = params.ip_port + i; control_socket = new SocketServerTCP (local_ip, control_port, true); @@ -500,7 +514,7 @@ int BioListener::wait_for_connection () } else { - int max_attempts = 10; + int max_attempts = (params.timeout * 1000) / BIOLISTENER_SLEEP_TIME_BETWEEN_SOCKET_TRIES_MS; for (int i = 0; i < max_attempts; i++) { safe_logger (spdlog::level::trace, "waiting for accept {}/{}", i, max_attempts); @@ -512,9 +526,9 @@ int BioListener::wait_for_connection () else { #ifdef _WIN32 - Sleep (300); + Sleep (BIOLISTENER_SLEEP_TIME_BETWEEN_SOCKET_TRIES_MS); #else - usleep (300000); + usleep (BIOLISTENER_SLEEP_TIME_BETWEEN_SOCKET_TRIES_MS * 1000); #endif } } diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h index caa6ac419..704503fd8 100644 --- a/src/board_controller/biolistener/inc/biolistener.h +++ b/src/board_controller/biolistener/inc/biolistener.h @@ -12,30 +12,6 @@ #include "socket_server_tcp.h" #include "socket_server_udp.h" -#define PACKET_DELIMITER_CSV '\n' - -#define BIOLISTENER_COMMAND_UNDEFINED 0 -#define BIOLISTENER_COMMAND_SET_ADC_DATA_RATE 1 -#define BIOLISTENER_COMMAND_SET_ADC_CHANNEL_ENABLE 2 -#define BIOLISTENER_COMMAND_SET_ADC_CHANNEL_PGA 3 -#define BIOLISTENER_COMMAND_RESET_ADC 4 -#define BIOLISTENER_COMMAND_START_SAMPLING 5 -#define BIOLISTENER_COMMAND_STOP_SAMPLING 6 - -#define BIOLISTENER_DATA_PACKET_DEBUG 0 -#define BIOLISTENER_DATA_PACKET_BIOSIGNALS 1 -#define BIOLISTENER_DATA_PACKET_IMU 2 - -#define BIOLISTENER_DATA_PACKET_HEADER 0xA0 -#define BIOLISTENER_DATA_PACKET_FOOTER 0xC0 - -#define BIOLISTENER_ADC_ADS131M08 0 -#define BIOLISTENER_ADC_AD7771 1 - -#define BIOLISTENER_DEFAULT_PGA_GAIN 8 - -#define FLOAT_TO_UINT32(x) (*((uint32_t *)&(x))) -#define UINT32_TO_FLOAT(x) (*((float *)&(x))) #pragma pack(push, 1) template @@ -84,6 +60,9 @@ class BioListener : public Board static double data_to_volts ( double ref, uint32_t raw_code, double pga_gain, double adc_resolution); + bool parse_tcp_buffer (const char *buffer, size_t buffer_size, + data_packet &parsed_packet); + public: BioListener (int board_id, struct BrainFlowInputParams params); ~BioListener (); @@ -93,7 +72,4 @@ class BioListener : public Board int stop_stream (); int release_session (); int config_board (std::string config, std::string &response); - - bool parse_tcp_buffer (const char *buffer, size_t buffer_size, - data_packet &parsed_packet); }; diff --git a/src/board_controller/biolistener/inc/biolistener_defines.h b/src/board_controller/biolistener/inc/biolistener_defines.h new file mode 100644 index 000000000..a3765ba0e --- /dev/null +++ b/src/board_controller/biolistener/inc/biolistener_defines.h @@ -0,0 +1,29 @@ +#pragma once + +#define PACKET_DELIMITER_CSV '\n' + +#define BIOLISTENER_COMMAND_UNDEFINED 0 +#define BIOLISTENER_COMMAND_SET_ADC_DATA_RATE 1 +#define BIOLISTENER_COMMAND_SET_ADC_CHANNEL_ENABLE 2 +#define BIOLISTENER_COMMAND_SET_ADC_CHANNEL_PGA 3 +#define BIOLISTENER_COMMAND_RESET_ADC 4 +#define BIOLISTENER_COMMAND_START_SAMPLING 5 +#define BIOLISTENER_COMMAND_STOP_SAMPLING 6 + +#define BIOLISTENER_DATA_PACKET_DEBUG 0 +#define BIOLISTENER_DATA_PACKET_BIOSIGNALS 1 +#define BIOLISTENER_DATA_PACKET_IMU 2 + +#define BIOLISTENER_DATA_PACKET_HEADER 0xA0 +#define BIOLISTENER_DATA_PACKET_FOOTER 0xC0 + +#define BIOLISTENER_ADC_ADS131M08 0 +#define BIOLISTENER_ADC_AD7771 1 + +#define BIOLISTENER_DEFAULT_PGA_GAIN 8 + +#define BIOLISTENER_MAX_PORTS_TO_TRY_TILL_FREE_FOUND 200 +#define BIOLISTENER_SLEEP_TIME_BETWEEN_SOCKET_TRIES_MS 300 + +#define FLOAT_TO_UINT32(x) (*((uint32_t *)&(x))) +#define UINT32_TO_FLOAT(x) (*((float *)&(x))) From 3c6ea6a069887ce9ac54e7e732261c1e95d2cc6d Mon Sep 17 00:00:00 2001 From: Serhii Matsyshyn Date: Fri, 7 Mar 2025 21:32:17 +0200 Subject: [PATCH 26/26] BioListener - PR corrections - defines moved to the separate file, added default values, minor fixes, docs updates --- src/board_controller/biolistener/biolistener.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp index 22da26095..8b24c6a70 100644 --- a/src/board_controller/biolistener/biolistener.cpp +++ b/src/board_controller/biolistener/biolistener.cpp @@ -59,7 +59,8 @@ int BioListener::prepare_session () if (params.ip_address.empty ()) { params.ip_address = "0.0.0.0"; - safe_logger (spdlog::level::warn, "IP address is not set, listening on all network interfaces"); + safe_logger ( + spdlog::level::warn, "IP address is not set, listening on all network interfaces"); } if (params.ip_port <= 0)