From 25730e0a843bcfea522f4a6095d66ac39b73167c Mon Sep 17 00:00:00 2001
From: MuijzerF <f.muijzer@utwente.nl>
Date: Mon, 20 Jan 2025 17:04:05 +0100
Subject: [PATCH] Added start-stop and sample rate control via Serial Commands

---
 src/main.cpp | 167 ++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 158 insertions(+), 9 deletions(-)

diff --git a/src/main.cpp b/src/main.cpp
index 471f2f7..ffaa4a9 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -12,8 +12,12 @@ using namespace helpers;               // contains the verbose levels from Instr
 const float versionNumber = 1.1;    // version displayed on boot
 const uint16_t serialTimeOut = 1000; // timeout in ms
 const long serialBaudrate = 115200;
+const byte numChars = 32; //maximum length of messages parsed on receive.
 static uint16_t logFreqSamples = 200; // give samplecounter and summary every n samples
-const uint8_t downSampler = 16;        // average n number of samples before storing. 1 = no downsampling, 2 = average two samples
+uint8_t downSampler = 16;        // average n number of samples before storing. 1 = no downsampling, 2 = average two samples
+const uint8_t maxDownSampler = 128;    // max number of samples to average
+const uint16_t minSampleRate = 10; // Lowest Acceptable Sample Rate
+const uint16_t maxSampleRate = 8096; // Highest Acceptable Sample Rate
 
 const uint8_t channelCountSize = 1;                          // always fixed at 1 byte
 const unsigned char channelCount[channelCountSize] = {0x01}; // number of active channels send to the plot, for now this is ADC. Should be 0x02 if Batt is also send.
@@ -38,7 +42,7 @@ byte sensorStatusBuffer[8];
 
 
 // the task durations define the sample rate of the tasks:
-const long aquisitionTaskDuration = 125;      // µs per cycle   //for aquisition of sensor data
+long aquisitionTaskDuration = 125;      // µs per cycle   //for aquisition of sensor data
 const long communicationTaskDuration = 10000; // µs per cycle   //of data transmission to client via Blueooth
 const uint8_t jitterMax = 10;                 // after this number of missed samples, the device is stopped.
 int jitterCounter = 0;                        // amount of times the sample momement was more than one sample time off.
@@ -54,13 +58,13 @@ Task communicationTask(communicationTaskDuration, TASK_FOREVER, &communicationTa
 Scheduler runner; // tasks are added to the scheduler in the setup() sequence and the scheduler is executed in main() loop.
 
 long ADCresult;
-long ADCresultArray[downSampler];
+long ADCresultArray[maxDownSampler];
 float ADCvolts;
 byte *bADCvolts[4]; // 4 byte float for uScope format
 float battVolts;
 byte *bBattvolts[4]; // 4 byte float for uScope format
-elapsedMicros elapsedMicrosTime; //timer that can easily be reset
-
+elapsedMicros elapsedMicrosTime; //timer for aquisition
+elapsedMillis elapsedMillisTime; //timer for receiving commands
 
 namespace buffers
 {
@@ -87,12 +91,70 @@ namespace buffers
 }
 using namespace buffers;
 
+
+//read messages received, this call is blocking for as long as new data continues to arrive
+//default start and end markers are < & >, other can be defines via arguments.
+//characters between the start and end marker are parsed as commands. For example "foo<start>foo"  is returned as "start"
+//maxReadTime is reserved for future use, to implement a timeout function if new characters keep comming in.
+//based on https://forum.arduino.cc/t/serial-input-basics-updated/382007
+char *receiveSerialMessage(const char startMarker = '<', const char endMarker = '>', const int maxReadTime = 100)
+{
+    static char receivedChars[numChars]; // an array to store the received data
+
+    static boolean recvInProgress = false;
+    boolean newData = false;
+    static byte index = 0;
+    char charactersRead;
+    elapsedMillisTime = 0;
+
+    //TODO: cancel while loop if new data continues to arrive
+    while (Serial.available() > 0 && newData == false && elapsedMillisTime < maxReadTime) //read until no new data is available, or maxReadTime is reached
+    {
+        charactersRead = Serial.read();
+
+        if (recvInProgress == true)
+        {
+            if (charactersRead != endMarker)
+            {
+                receivedChars[index] = charactersRead;
+                index++;
+                if (index >= numChars)
+                {
+                    index = numChars - 1; //TODO: comment FM: does not seem logical to do?
+
+                    helpers::printToLog("Received more characters via serial than the max array length!  " + String(receivedChars), verboseLevels::error);
+                }
+            }
+            else
+            {
+                receivedChars[index] = '\0'; // terminate the string
+                recvInProgress = false;
+                index = 0;
+                newData = true;
+            }
+        }
+
+        else if (charactersRead == startMarker)
+        {
+            recvInProgress = true;
+        }
+    }
+    if (newData == true) //print the parsed command for debug purposes.
+    {
+        helpers::printToLog("Received via Serial: " + String(receivedChars), verboseLevels::info);
+    }
+
+    return receivedChars;
+}
+
+
 uint32_t sampleCounter = 0;
 uint32_t sampleCounterOld = 0;
 uint32_t millisOld = 0;
 uint8_t millisWraps = 0;                 // count the number of times millis() reached 2^32.
 uint8_t downSampleCounter = 0;          // count up each aquisition cycle
 
+
 /*
 this taks reads the sensor data at a fixed interval. Based upon the realtime example of library TaskScheduler (https://github.com/arkhipenko/TaskScheduler)
 this task will be started and stopped based upon the <start> / <stop> commands received via Bluetooth.
@@ -278,8 +340,95 @@ void aquisitionTask_Callback()
 // this task will be active right from the boot, and will never be stopped.
 void communicationTask_Callback()
 {
+  //read data from Serial
+
+   // characters between the start and end marker are parsed as commands. For example "foo<start>foo"  is returned as "start"
+    const char startMarker = '<';
+    const char endMarker = '>';
+    const int maxReadTime = 100; //ms
+    char *receivedChars = receiveSerialMessage(startMarker, endMarker, maxReadTime);
+
+    if (strcmp(receivedChars, "start") == 0)
+    {
+      if (!aquisitionTask.isEnabled())
+      {
+        memset(receivedChars, 0, sizeof(receivedChars) ); // clean command buffer
+        jitterCounter = 0; // reset the counter for a clean start
+        aquisitionTask.enable();
+        helpers::printToLog("Start command received, enabled aquisitionTask (start sampling)", verboseLevels::info);
+      }
+    }
+    else if (strcmp(receivedChars, "stop") == 0)
+    {
+      if (aquisitionTask.isEnabled())
+      {
+        memset(receivedChars, 0, sizeof(receivedChars) ); // clean command buffer
+        aquisitionTask.disable();
+        helpers::printToLog("Stop command received: disabled aquisitionTask (stop sampling).", verboseLevels::info);
+      }
+    }
+    else if (strncmp(receivedChars, "SetSampleRate", 13) == 0) // command starting with "SetSampleRate", should always be followed by 4 integers, for example "SetSampleRate1000" for 1000 Hz
+    {
+      if (aquisitionTask.isEnabled())
+      {
+        helpers::printToLog("Received Set Sample Rate command, but aquisition is active!", verboseLevels::error);
+      }
+      else
+      {
+          char argument[5];                        // stores the value received from the host in ASCCI text
+          memcpy(argument, &receivedChars[13], 4); // copy characters from full input command 
+          argument[4] = '\0';                      // termination character
+          int newSampleRate = atoi(argument);
+          helpers::printToLog("Received SetSampleRate Command: " + String(newSampleRate), verboseLevels::info);
+          
+          if(newSampleRate < minSampleRate || newSampleRate > maxSampleRate)
+          {
+            helpers::printToLog("Received Set Sample Rate command is below " + String(minSampleRate) + " or above " + String(maxSampleRate), verboseLevels::error);
+          }
+          else if(newSampleRate / downSampler < minSampleRate)
+          {
+            helpers::printToLog("Received Set Sample Rate command is too low in combination with configured downsampler: " + String(downSampler), verboseLevels::error);
+          }
+          else
+          {     
+            aquisitionTaskDuration = 1000000 / newSampleRate;   // µs per cycle   //for aquisition of sensor data
+            aquisitionTask.setInterval(aquisitionTaskDuration);
+          }
+      }
+      memset(receivedChars, 0, sizeof(receivedChars) ); // clean command buffer
+    }
+    else if (strncmp(receivedChars, "SetDownSampleRate", 17) == 0) // command starting with "SetDownSampleRate", should always be followed by 4 integers, for example "SetDownSampleRate0016" for 16 times downsample
+    {
+      if (aquisitionTask.isEnabled())
+      {
+        helpers::printToLog("Received Set DownSample Rate command, but aquisition is active!", verboseLevels::error);
+      }
+      else
+      {
+          char argument[5];                        // stores the value received from the host in ASCCI text
+          memcpy(argument, &receivedChars[17], 4); // copy characters from full input command 
+          argument[4] = '\0';                      // termination character
+          int newDownSampleRate = atoi(argument);
+          helpers::printToLog("Received SetDownSampleRate Command: " + String(newDownSampleRate), verboseLevels::info);
+          
+          if(newDownSampleRate < 1 || newDownSampleRate > maxDownSampler)
+          {
+            helpers::printToLog("Received Set Down Sample Rate command is below 1 or above " + String(maxDownSampler), verboseLevels::error);
+          }
+          else if((1000000 / aquisitionTask.getInterval()) / newDownSampleRate < minSampleRate)
+          {
+            
+            helpers::printToLog("Received Set Down Sample Rate command is too low in combination with configured sample rate: " + String(1000000 / aquisitionTask.getInterval()), verboseLevels::error);
+          }
+          else
+          {     
+            downSampler = newDownSampleRate; //set new down sample rate
+          }
+      }
+      memset(receivedChars, 0, sizeof(receivedChars) ); // clean command buffer
+    }
 
-  // write data to Bluetooth
+  // write data to Serial
 
   if (btBufferA.isBufferLocked() && btBufferB.isBufferLocked())
   {
@@ -301,7 +450,7 @@ void communicationTask_Callback()
       helpers::printToLog(" try to send buffer B of size (bytes): " + String(btBufferB.getPointerPosition()) + ", data: " + String(btBufferB.buffer[10]), verboseLevels::extraBuffers);
       if (Serial.availableForWrite())
       {
-        Serial.write((uint8_t *)btBufferB.buffer, btBufferB.getPointerPosition()); // write buffer until pointer position
+       Serial.write((uint8_t *)btBufferB.buffer, btBufferB.getPointerPosition()); // write buffer until pointer position
       }
       btBufferB.clearBuffer(); // clear buffer B & reset pointer
     }
@@ -369,8 +518,8 @@ void setup()
   communicationTask.enable();
   helpers::printToLog("Communication task started!", verboseLevels::info);
 
-  aquisitionTask.enable();
-  helpers::printToLog("enabled aquisitionTask (start sampling)", verboseLevels::info);
+  //aquisitionTask.enable();
+  //helpers::printToLog("enabled aquisitionTask (start sampling)", verboseLevels::info);
 }
 void loop()
 {
-- 
GitLab