Repository: brainflow-dev/brainflow (language: python)

Issue:
Add configuration option for start index in playback boards
**Use case:**
There are several configuration options for the Playback File Board that can be set via the `config_board` method. It would be useful to have another option that sets the index in the playback file to start playback from. This would enable scrubbing through the file and starting the file a specific locations (e.g. an start index found using a marker for alignment with other modalities).

**Describe the solution you'd like:**
Add an option to the `config_board` method that supports starting the playback board at a specified sample index in the file.

**Describe alternatives you've considered:**
This can be done outside of Brainflow by reading the file and sending data to the DataFilter interfaces, but doing so replicates much of the functionality already implemented in the Playback File Board. Alternatively, a version of the file could be edited and saved so that it only contains data starting from the desired index, but this is inefficient.



Discussion / hints:

Fix patch:
diff --git a/src/board_controller/inc/playback_file_board.h b/src/board_controller/inc/playback_file_board.h
index 825f43e9c..cb59ba2d6 100644
--- a/src/board_controller/inc/playback_file_board.h
+++ b/src/board_controller/inc/playback_file_board.h
@@ -3,6 +3,7 @@
 #include <condition_variable>
 #include <mutex>
 #include <thread>
+#include <vector>
 
 #include "board.h"
 #include "board_controller.h"
@@ -15,13 +16,13 @@ class PlaybackFileBoard : public Board
     volatile bool keep_alive;
     volatile bool loopback;
     volatile bool use_new_timestamps;
+    std::vector<double> pos_percentage;
     std::vector<std::thread> streaming_threads;
     bool initialized;
-    std::mutex m;
-    std::condition_variable cv;
-    volatile int state;
+    std::vector<std::vector<long int>> file_offsets;
 
     void read_thread (int preset, std::string filename);
+    int get_file_offsets (std::string filename, std::vector<long int> &offsets);
 
 public:
     PlaybackFileBoard (struct BrainFlowInputParams params);
diff --git a/src/board_controller/playback_file_board.cpp b/src/board_controller/playback_file_board.cpp
index 5d7179514..6a951e546 100644
--- a/src/board_controller/playback_file_board.cpp
+++ b/src/board_controller/playback_file_board.cpp
@@ -1,3 +1,4 @@
+#include <algorithm>
 #include <chrono>
 #include <sstream>
 #include <stdio.h>
@@ -17,6 +18,8 @@
 #define SET_LOOPBACK_FALSE "loopback_false"
 #define NEW_TIMESTAMPS "new_timestamps"
 #define OLD_TIMESTAMPS "old_timestamps"
+#define SET_INDEX_PREFIX "set_index_percentage:"
+#define MAX_LINE_LENGTH 8192
 
 
 PlaybackFileBoard::PlaybackFileBoard (struct BrainFlowInputParams params)
@@ -29,7 +32,8 @@ PlaybackFileBoard::PlaybackFileBoard (struct BrainFlowInputParams params)
     loopback = false;
     initialized = false;
     use_new_timestamps = true;
-    this->state = (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR;
+    pos_percentage.resize (3);
+    std::fill (pos_percentage.begin (), pos_percentage.end (), -1);
 }
 
 PlaybackFileBoard::~PlaybackFileBoard ()
@@ -45,6 +49,7 @@ int PlaybackFileBoard::prepare_session ()
         safe_logger (spdlog::level::info, "Session is already prepared");
         return (int)BrainFlowExitCodes::STATUS_OK;
     }
+
     if (params.master_board == (int)BoardIds::NO_BOARD)
     {
         safe_logger (spdlog::level::err, "master board id is not provided");
@@ -69,6 +74,46 @@ int PlaybackFileBoard::prepare_session ()
         return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
     }
 
+    if (!params.file.empty ())
+    {
+        std::vector<long int> offsets;
+        int res = get_file_offsets (params.file, offsets);
+        if (res != (int)BrainFlowExitCodes::STATUS_OK)
+        {
+            return res;
+        }
+        else
+        {
+            file_offsets.push_back (offsets);
+        }
+    }
+    if (!params.file_aux.empty ())
+    {
+        std::vector<long int> offsets;
+        int res = get_file_offsets (params.file_aux, offsets);
+        if (res != (int)BrainFlowExitCodes::STATUS_OK)
+        {
+            return res;
+        }
+        else
+        {
+            file_offsets.push_back (offsets);
+        }
+    }
+    if (!params.file_anc.empty ())
+    {
+        std::vector<long int> offsets;
+        int res = get_file_offsets (params.file_anc, offsets);
+        if (res != (int)BrainFlowExitCodes::STATUS_OK)
+        {
+            return res;
+        }
+        else
+        {
+            file_offsets.push_back (offsets);
+        }
+    }
+
     initialized = true;
     return (int)BrainFlowExitCodes::STATUS_OK;
 }
@@ -104,20 +149,8 @@ int PlaybackFileBoard::start_stream (int buffer_size, const char *streamer_param
         streaming_threads.push_back (std::thread ([this]
             { this->read_thread ((int)BrainFlowPresets::ANCILLARY_PRESET, params.file_anc); }));
     }
-    // wait for data to ensure that everything is okay
-    std::unique_lock<std::mutex> lk (this->m);
-    auto sec = std::chrono::seconds (1);
-    if (cv.wait_for (lk, 2 * sec,
-            [this] { return this->state != (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; }))
-    {
-        return this->state;
-    }
-    else
-    {
-        safe_logger (spdlog::level::err, "no data received in 2sec, stopping thread");
-        this->stop_stream ();
-        return (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR;
-    }
+
+    return (int)BrainFlowExitCodes::STATUS_OK;
 }
 
 int PlaybackFileBoard::stop_stream ()
@@ -130,7 +163,6 @@ int PlaybackFileBoard::stop_stream ()
             streaming_thread.join ();
         }
         streaming_threads.clear ();
-        this->state = (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR;
         return (int)BrainFlowExitCodes::STATUS_OK;
     }
     else
@@ -147,6 +179,7 @@ int PlaybackFileBoard::release_session ()
         free_packages ();
         initialized = false;
     }
+    file_offsets.clear ();
     return (int)BrainFlowExitCodes::STATUS_OK;
 }
 
@@ -160,7 +193,7 @@ void PlaybackFileBoard::read_thread (int preset, std::string file)
     }
 
     FILE *fp;
-    fp = fopen (file.c_str (), "r");
+    fp = fopen (file.c_str (), "rb");
     if (fp == NULL)
     {
         safe_logger (spdlog::level::err, "failed to open file in thread");
@@ -174,7 +207,7 @@ void PlaybackFileBoard::read_thread (int preset, std::string file)
     {
         package[i] = 0.0;
     }
-    char buf[4096];
+    char buf[MAX_LINE_LENGTH];
     double last_timestamp = -1.0;
     bool new_timestamps = use_new_timestamps; // to prevent changing during streaming
     int timestamp_channel = board_preset["timestamp_channel"];
@@ -183,6 +216,26 @@ void PlaybackFileBoard::read_thread (int preset, std::string file)
     while (keep_alive)
     {
         auto start = std::chrono::high_resolution_clock::now ();
+        // prevent race condition with another config_board method call
+        lock.lock ();
+        double cur_index = pos_percentage[preset];
+        if ((int)cur_index >= 0)
+        {
+            int new_pos = (int)(cur_index * (file_offsets[preset].size () / 100.0));
+            try
+            {
+                fseek (fp, file_offsets[preset][new_pos], SEEK_SET);
+                safe_logger (spdlog::level::trace, "set position in a file to {}", new_pos);
+            }
+            catch (...)
+            {
+                // should never happen since input is already validated
+                safe_logger (spdlog::level::warn, "invalid position in a file");
+            }
+            last_timestamp = -1;
+            pos_percentage[preset] = -1;
+        }
+        lock.unlock ();
         char *res = fgets (buf, sizeof (buf), fp);
         if ((loopback) && (res == NULL))
         {
@@ -226,16 +279,14 @@ void PlaybackFileBoard::read_thread (int preset, std::string file)
         }
         for (int i = 0; i < num_rows; i++)
         {
-            package[i] = std::stod (splitted[i]);
-        }
-        // notify main thread
-        if (this->state != (int)BrainFlowExitCodes::STATUS_OK)
-        {
+            try
+            {
+                package[i] = std::stod (splitted[i]);
+            }
+            catch (...)
             {
-                std::lock_guard<std::mutex> lk (this->m);
-                this->state = (int)BrainFlowExitCodes::STATUS_OK;
+                safe_logger (spdlog::level::err, "failed to parse value: {}", splitted[i].c_str ());
             }
-            this->cv.notify_one ();
         }
         if (last_timestamp > 0)
         {
@@ -284,6 +335,31 @@ int PlaybackFileBoard::config_board (std::string config, std::string &response)
     {
         use_new_timestamps = false;
     }
+    else if (strncmp (config.c_str (), SET_INDEX_PREFIX, strlen (SET_INDEX_PREFIX)) == 0)
+    {
+        try
+        {
+            double new_index = std::stod (config.substr (strlen (SET_INDEX_PREFIX)));
+            if (((int)new_index >= 0) && ((int)new_index < 100))
+            {
+                lock.lock ();
+                std::fill (pos_percentage.begin (), pos_percentage.end (), new_index);
+                lock.unlock ();
+            }
+            else
+            {
+                safe_logger (
+                    spdlog::level::err, "invalid index value, should be between 0 and 100");
+                return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+            }
+        }
+        catch (const std::exception &e)
+        {
+            safe_logger (spdlog::level::err, "need to write a number after {}, exception is: {}",
+                SET_INDEX_PREFIX, e.what ());
+            return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+        }
+    }
     else
     {
         safe_logger (spdlog::level::warn, "invalid config string {}", config);
@@ -291,3 +367,37 @@ int PlaybackFileBoard::config_board (std::string config, std::string &response)
 
     return (int)BrainFlowExitCodes::STATUS_OK;
 }
+
+int PlaybackFileBoard::get_file_offsets (std::string filename, std::vector<long int> &offsets)
+{
+    offsets.clear ();
+
+    FILE *fp = fopen (filename.c_str (), "rb");
+    if (fp == NULL)
+    {
+        safe_logger (spdlog::level::err, "failed to open file: {}", filename.c_str ());
+        return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+    }
+
+    char buf[MAX_LINE_LENGTH];
+    long int bytes_read = 0;
+    while (true)
+    {
+        offsets.push_back (bytes_read);
+        char *res = fgets (buf, sizeof (buf), fp);
+        bytes_read += (long int)strlen (buf);
+        if (res == NULL)
+        {
+            break;
+        }
+    }
+
+    fclose (fp);
+    if (offsets.size () < 2)
+    {
+        safe_logger (spdlog::level::err, "empty file: {}", filename);
+        return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+    }
+
+    return (int)BrainFlowExitCodes::STATUS_OK;
+}
\ No newline at end of file
diff --git a/src/board_controller/synthetic_board.cpp b/src/board_controller/synthetic_board.cpp
index f4189177f..22a34b06c 100644
--- a/src/board_controller/synthetic_board.cpp
+++ b/src/board_controller/synthetic_board.cpp
@@ -194,20 +194,20 @@ void SyntheticBoard::read_thread ()
             aux_package[channel] = (double)channel;
         }
         aux_package[board_descr["auxiliary"]["timestamp_channel"].get<int> ()] = get_timestamp ();
-        package[board_descr["auxiliary"]["package_num_channel"].get<int> ()] = (double)counter;
-        package[board_descr["auxiliary"]["battery_channel"].get<int> ()] =
+        aux_package[board_descr["auxiliary"]["package_num_channel"].get<int> ()] = (double)counter;
+        aux_package[board_descr["auxiliary"]["battery_channel"].get<int> ()] =
             (dist_around_one (mt) - 0.1) * 100;
         for (int channel : board_descr["auxiliary"]["accel_channels"])
         {
-            package[channel] = dist_around_one (mt) - 0.1;
+            aux_package[channel] = dist_around_one (mt) - 0.1;
         }
         for (int channel : board_descr["auxiliary"]["gyro_channels"])
         {
-            package[channel] = dist_around_one (mt) - 0.1;
+            aux_package[channel] = dist_around_one (mt) - 0.1;
         }
         for (int channel : board_descr["auxiliary"]["eda_channels"])
         {
-            package[channel] = dist_around_one (mt);
+            aux_package[channel] = dist_around_one (mt);
         }
         for (int chan_num = 0; chan_num < (int)board_descr["auxiliary"]["ppg_channels"].size ();
              chan_num++)
@@ -215,20 +215,20 @@ void SyntheticBoard::read_thread ()
             int channel = board_descr["auxiliary"]["ppg_channels"][chan_num];
             if (chan_num == 0)
             {
-                package[channel] = 500.0 * dist_around_one (mt);
+                aux_package[channel] = 500.0 * dist_around_one (mt);
             }
             else
             {
-                package[channel] = 253500.0 * dist_around_one (mt);
+                aux_package[channel] = 253500.0 * dist_around_one (mt);
             }
         }
         for (int channel : board_descr["auxiliary"]["temperature_channels"])
         {
-            package[channel] = dist_around_one (mt) / 10.0 + 36.5;
+            aux_package[channel] = dist_around_one (mt) / 10.0 + 36.5;
         }
         for (int channel : board_descr["auxiliary"]["resistance_channels"])
         {
-            package[channel] = 1000.0 * dist_around_one (mt);
+            aux_package[channel] = 1000.0 * dist_around_one (mt);
         }
 
         push_package (aux_package, (int)BrainFlowPresets::AUXILIARY_PRESET);
diff --git a/src/ml/train/train_classifiers.py b/src/ml/train/train_classifiers.py
index 0f3c08374..a76922b25 100644
--- a/src/ml/train/train_classifiers.py
+++ b/src/ml/train/train_classifiers.py
@@ -7,7 +7,6 @@
 import numpy as np
 from sklearn.svm import SVC
 from sklearn.linear_model import LogisticRegression
-from sklearn.gaussian_process import GaussianProcessClassifier
 from sklearn.ensemble import RandomForestClassifier
 from sklearn.ensemble import StackingClassifier
 from sklearn.neighbors import KNeighborsClassifier


Test patch:
diff --git a/python_package/examples/tests/playback_test.py b/python_package/examples/tests/playback_test.py
index f71e82da6..ab62a4e24 100644
--- a/python_package/examples/tests/playback_test.py
+++ b/python_package/examples/tests/playback_test.py
@@ -17,7 +17,9 @@ def main():
     board = BoardShim(board_id, params)
     board.prepare_session()
     board.start_stream()
-    time.sleep(10)
+    time.sleep(5)
+    board.config_board('set_index_percentage:10')
+    time.sleep(5)
     data_default = board.get_board_data(preset=BrainFlowPresets.DEFAULT_PRESET)
     data_aux = board.get_board_data(preset=BrainFlowPresets.AUXILIARY_PRESET)
     board.stop_stream()