/// <summary>
        /// Override this method to implement whatever should happen with the samples...
        /// IMPORTANT: Avoid heavy processing logic within this method, update a state and use
        /// coroutines for more complexe processing tasks to distribute processing time over
        /// several frames
        /// </summary>
        /// <param name="newSample"></param>
        /// <param name="timeStamp"></param>
        protected void Process2(int numSamples, int[,] newSamples, double[] timeStamps)
        //protected override void Process(int numSamples, int[,] newSamples, double[] timeStamps)
        {
            ExciteOMeterManager.DebugLog("ECG NumSamples: " + numSamples);

            // pull as long samples are available
            for (int i = 0; i < numSamples; i++)
            {
                if (newSamples[0, i] > 1.0e7)  // Check values with bad parsing
                {
                    ExciteOMeterManager.DebugLog("Error parsing value: " + BitConverter.ToString(BitConverter.GetBytes(newSamples[0, i])) + ", " + newSamples[0, i].ToString("F2"));
                    continue;
                }
                else
                {
                    EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), newSamples[0, i]);
                    LoggerController.instance.WriteLine(LogName.VariableRawECG, newSamples[0, i].ToString("F0") + "," + newSamples[0, i].ToString("F0"));
                }
            }

            // Debug.Log($"Receiving from stream {StreamName}, first sample {newSample[0]}");

            //TODO: The event only sends float[], all samples need to be parsed to float

            // EoM_Events.Send_OnDataReceived(VariableType, new float[1]{(float) newSample[0]});
            //LoggerController.instance.WriteLine(LogName.VariableRawECG, timestamp.ToString("F0") + "," + newSample[0].ToString("F0"));
        }
        /// <summary>
        /// Calculate feature value from a unidimensional sample-based feature
        /// </summary>
        void SampleBasedCalculation()
        {
            // Check that data is available
            if (timestamps.Count == 0 && dataBuffer.Count == 0)
            {
                // ExciteOMeterManager.DebugLog("No timestamps or data were found to calculate features");
                ExciteOMeterManager.LogInFile("No incoming data " + incomingDataType.ToString() + " was found to calculate feature " + outputDataType.ToString());
                return;
            }
            // Calculate feature
            featureValue = CalculateFeature(timestamps.ToArray(), dataBuffer.ToArray());

            // Calculate offset of timestamp that corresponds to the calculated feature (# displacements to the left in timestamps)
            // Examples: Assume `sampleBuffer=5`
            //           If `offsetSamplesTimestamp=0`, t for calculated feature is [t-4,t-3,...,t]
            //           If `offsetSamplesTimestamp=3`, t for calculated feature is [t-1,t,t+1,t+2,t+3]
            indexOffsetForTimestamp = sampleBuffer - offsetSamplesTimestamp - 1;

            // Send events and log in file
            ExciteOMeterManager.DebugLog("A new feature was calculated in " + outputDataType.ToString() + ": " + timestamps[indexOffsetForTimestamp] + ", " + featureValue.ToString());

            // Flag to know if it is the first calculation of the feature.
            // If so, the new feature has to match all the timestamps existing before the first timestamp of the feature.
            if (matchLengthOfInputSignal)
            {
                if (isFirstCalculatedFeature)
                {
                    isFirstCalculatedFeature = false;
                    idxStartRepeating        = 0; // No previous data in array, repeat from beginning of input timestamps.
                }
                else
                {
                    // CASE: Match length and buffer already contains data from previous window
                    // Based on overlap and offset, the position where to start repeating timestamps is the following formula.
                    idxStartRepeating = overlappingSamples - offsetSamplesTimestamp;
                }

                // Fill the previous timestamps of the input signal with the same value of this feature.
                for (int i = idxStartRepeating; i <= indexOffsetForTimestamp; i++)
                {
                    // Write in files to collect data corresponding to
                    EoM_Events.Send_OnDataReceived(outputDataType, timestamps[i], featureValue);
                    LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[i]) + "," + ExciteOMeterManager.ConvertFloatToString(featureValue));
                }
            }
            else
            {
                Debug.LogWarning("Error calculating feature. Matching sampling error: logIdentifier" + logIdentifier.ToString());

                //// CASE: DO NOT MATCH LENGTH OF INPUT SIGNAL, BUT USE TIMESTAMP DIFFERENT THAN LAST SAMPLE
                //EoM_Events.Send_OnDataReceived(outputDataType, timestamps[indexOffsetForTimestamp],  featureValue);
                //LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[indexOffsetForTimestamp]) + "," + ExciteOMeterManager.ConvertFloatToString(featureValue));
            }

            // Rearrange overlap in signal
            elementsToDelete = sampleBuffer - overlappingSamples;

            timestamps.RemoveRange(0, elementsToDelete);
            dataBuffer.RemoveRange(0, elementsToDelete);
        }
 private void ShowElapsedSessionTime()
 {
     if (sessionTimeParent.activeSelf)
     {
         sessionTimeText.text = ExciteOMeterManager.GetTimestamp().ToString("F0");
     }
 }
Пример #4
0
        protected override float CalculateFeature(float[] timestamps, float[] values)
        {
            /*
             * REFERENCE: https://www.frontiersin.org/articles/10.3389/fpubh.2017.00258/full
             *
             * RMSSD
             * The root mean square of successive differences between normal heartbeats (RMSSD) is obtained by first calculating each successive time difference between heartbeats in ms. Then, each of the values is squared and the result is averaged before the square root of the total is obtained. While the conventional minimum recording is 5 min, researchers have proposed ultra-short-term periods of 10 s (30), 30 s (31), and 60 s (36).
             */

            int length = values.Length;

            if (length < 2)
            {
                ExciteOMeterManager.DebugLog("It is not possible to calculate RMSSD with less than 2 values");
                return(0);
            }

            // Calculation
            N = length - 1; // Size of final vector of differences

            cumsum = 0;
            for (int i = 0; i < N; i++)
            {
                diff    = values[i + 1] - values[i];
                cumsum += (float)Math.Pow(diff, 2);
            }
            ;

            result = (float)Math.Sqrt(cumsum / N);

            return(result);
        }
Пример #5
0
        /// <summary>
        /// Override this method to implement whatever should happen with the samples...
        /// IMPORTANT: Avoid heavy processing logic within this method, update a state and use
        /// coroutines for more complexe processing tasks to distribute processing time over
        /// several frames
        /// </summary>
        /// <param name="newSample"></param>
        /// <param name="timeStamp"></param>
        protected override void Process(float[] newSample, double timeStamp)
        {
            //TODO: Use the timestamp from the sensor, which is in nanoseconds.
            EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), newSample[0]);

            LoggerController.instance.WriteLine(LogName.VariableRrInterval, ExciteOMeterManager.GetTimestampString() + "," + ExciteOMeterManager.ConvertFloatToString(newSample[0]));
        }
Пример #6
0
        // JUST CALLED BY ExciteOMeterManager WHEN POST-PROCESSING IS FINISHED.
        public void FinalStopLogSession()
        {
            // Logging is finished
            currentlyLogging = false;
            ExciteOMeterManager.SetCurrentlyRecordingVariable(false);

            // Notify everyone before closing logs.
            // Specially for feature calculation that matches length of input data before log is closed.
            EoM_Events.Send_OnLoggingStateChanged(currentlyLogging);

            // STOP LOGGING
            SetLoggingState(false);
            CloseLogFiles();

            // Create JSON
            SessionVariablesController.instance.StopCurrentSession();

            if (recordingScreenshots)
            {
                ScreenRecorder.instance.StopScreenRecorder();
            }

            // Allow custom events from the end user
            ExciteOMeterManager.instance.OnStopSessionLog.Invoke();
        }
Пример #7
0
        /// <summary>
        /// Override this method to implement whatever should happen with the samples...
        /// IMPORTANT: Avoid heavy processing logic within this method, update a state and use
        /// coroutines for more complexe processing tasks to distribute processing time over
        /// several frames
        /// </summary>
        /// <param name="newSample"></param>
        /// <param name="timeStamp"></param>
        protected override void Process(short[] newSample, double timeStamp)
        {
            //TODO: The event only sends float[], all samples need to be parsed to float
            EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[0]);

            LoggerController.instance.WriteLine(LogName.VariableHeartRate, ExciteOMeterManager.GetTimestampString() + "," + ExciteOMeterManager.ConvertFloatToString(newSample[0], 0));
        }
Пример #8
0
        void TimeBasedCalculation()
        {
            // New window time is the original window time minus overlapping window. Set elapsed time in proportional position.
            elapsedWindowTime = windowTime * overlappingFraction;

            // Check that data is available
            if (timestamps.Count == 0 && dataBuffer.Count == 0)
            {
                ExciteOMeterManager.LogInFile("No incoming data " + incomingDataType.ToString() + " was found to calculate feature " + outputDataType.ToString());
                return;
            }
            // Calculate feature
            featureValue = CalculateFeature(timestamps.ToArray(), dataBuffer.ToArray());

            // Send events and log in file
            ExciteOMeterManager.DebugLog("A new feature was calculated in " + outputDataType.ToString() + ": " + timestamps[timestamps.Count - 1] + ", " + featureValue.ToString());
            EoM_Events.Send_OnDataReceived(outputDataType, timestamps[timestamps.Count - 1], featureValue);
            LoggerController.instance.WriteLine(logIdentifier, timestamps[timestamps.Count - 1] + "," + featureValue.ToString());

            // Rearrange overlap in signal
            // Overlap should not be greater than 95%, because it would generate very often feature calculations that might affect performance.
            int elementsToDelete = (int)(Mathf.Clamp(1.0f - (overlappingFraction), 0f, 0.95f) * dataBuffer.Count);

            timestamps.RemoveRange(0, elementsToDelete);
            dataBuffer.RemoveRange(0, elementsToDelete);
        }
        void PostProcessing()
        {
            if (isSampleBasedFeature && matchLengthOfInputSignal)
            {
                // CASE: Match length of input signal, recording has finished but input signal is larger than features.
                // This saves in the log the number of lines remaining to match the length of the input.
                idxStartRepeating = isFirstCalculatedFeature? 0 : (overlappingSamples - offsetSamplesTimestamp);
                for (int i = idxStartRepeating; i < timestamps.Count; i++)
                {
                    // Write in files to collect data corresponding to
                    EoM_Events.Send_OnDataReceived(outputDataType, timestamps[i], featureValue);
                    LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[i]) + "," + ExciteOMeterManager.ConvertFloatToString(featureValue));
                }
            }

            ////////////// POSTPROCESSING
            // Postprocessing control
            instancesFinishedPostprocessing++;
            if (instancesFinishedPostprocessing == numInstances)
            {
                // All feature extraction have finished postprocessing
                ExciteOMeterManager.instance.PostProcessingExciteOMeterLevel();
                // Reset
                instancesFinishedPostprocessing = 0;
            }
        }
        // Update is called once per frame
        void Update()
        {
            if (!active)
            {
                return;
            }

            // Timer control
            elapsedTime += Time.deltaTime;

            // Emulate connection disconnection HR
            if (sendHR && !previousSendHR)
            {
                EoM_Events.Send_OnStreamConnected(DataType.HeartRate);
            }
            else if (!sendHR && previousSendHR)
            {
                EoM_Events.Send_OnStreamDisconnected(DataType.HeartRate);
            }

            // Emulate connection disconnection RRi
            if (sendRrI && !previousSendRRi)
            {
                EoM_Events.Send_OnStreamConnected(DataType.RRInterval);
            }
            else if (!sendRrI && previousSendRRi)
            {
                EoM_Events.Send_OnStreamDisconnected(DataType.RRInterval);
            }


            // Send data each "sendingPeriod"
            if (elapsedTime >= sendingPeriod)
            {
                // Reset timer for next event
                elapsedTime = 0.0f;

                // Calculate new random values
                randomVariation = Random.Range(-variance, variance);

                // Setup new random values. If HR increases, RRi should decrease.
                HR  += HR * randomVariation;
                RRi -= RRi * randomVariation * Random.Range(1.0f, 3.0f);

                // Send events
                float HRf = (float)Mathf.RoundToInt(HR);
                EoM_Events.Send_OnDataReceived(DataType.HeartRate, ExciteOMeterManager.GetTimestamp(), HRf);
                LoggerController.instance.WriteLine(LogName.VariableHeartRate, ExciteOMeterManager.GetTimestamp().ToString("F6") + "," + HR.ToString("F0"));

                EoM_Events.Send_OnDataReceived(DataType.RRInterval, ExciteOMeterManager.GetTimestamp(), RRi);
                LoggerController.instance.WriteLine(LogName.VariableRrInterval, ExciteOMeterManager.GetTimestamp().ToString("F6") + "," + RRi.ToString("F3"));
            }

            // To detect changes in runtime
            previousSendHR  = sendHR;
            previousSendRRi = sendRrI;
        }
Пример #11
0
        /// <summary>
        /// Override this method to implement whatever should happen with the samples...
        /// IMPORTANT: Avoid heavy processing logic within this method, update a state and use
        /// coroutines for more complexe processing tasks to distribute processing time over
        /// several frames
        /// </summary>
        /// <param name="newSample"></param>
        /// <param name="timeStamp"></param>
        protected override void Process(short[] newSample, double timeStamp)
        {
            //TODO: The event only sends float[], all samples need to be parsed to float
            EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[0]);
            Debug.Log("hello from the OG Script");

            LoggerController.instance.WriteLine(LogName.VariableHeartRate, ExciteOMeterManager.GetTimestamp().ToString("F6") + "," + newSample[0].ToString("F0"));
            Debug.Log(newSample);
        }
Пример #12
0
 void SetupMarkerButtonCallback()
 {
     quickMarkerButton = gameObject.GetComponent <Button>();
     if (quickMarkerButton != null)
     {
         quickMarkerButton.onClick.RemoveAllListeners();
         // Send an EoM event when clicked the button
         quickMarkerButton.onClick.AddListener(delegate { ExciteOMeterManager.TriggerMarker(markerMessage, MarkerLabel.QUICK_MARKER); });
         quickMarkerButton.onClick.AddListener(delegate { ExecuteAction(); });
     }
 }
Пример #13
0
        ////// MARKERS UI
        public void CreateManualMarker(string defaultText, MarkerLabel markerLabel = MarkerLabel.CUSTOM_MARKER)
        {
            EoM_Events.Send_OnStringReceived(DataType.ManualMarkers, ExciteOMeterManager.GetTimestamp(), defaultText);
            LogMessageUI.instance.WriteConsoleText("New custom marker at " + ExciteOMeterManager.GetTimestampString(2) + " with message " + defaultText);
            GameObject           go     = Instantiate(instanceMarkerPrefab, markersParent);
            CustomMarkerScriptUI script = go.GetComponent <CustomMarkerScriptUI>();

            script.Setup(ExciteOMeterManager.GetTimestamp(), defaultText, markerLabel);
            // sessionMarkers.Add(script); // Used to keep record of existing session markers
            LoggerController.instance.SaveScreenshot();
        }
Пример #14
0
        /// <summary>
        /// Callback method for the Resolver gets called each time the resolver misses a stream within its cache
        /// </summary>
        /// <param name="stream"></param>
        public virtual void AStreamGotLost(LSLStreamInfoWrapper stream)
        {
            if (!isTheExpected(stream))
            {
                return;
            }

            ExciteOMeterManager.DebugLog(string.Format("LSL Stream {0} Lost for {1}", stream.Name, name));

            OnStreamLost();
        }
Пример #15
0
        /// <summary>
        /// Find a common resolver for all the instances
        /// </summary>
        void Awake()
        {
            if (resolver == null)
            {
                resolver = FindObjectOfType <Resolver>();
            }

            if (VariableType == DataType.NONE)
            {
                ExciteOMeterManager.LogError("Please define the type of measurement of this LSL inlet in GameObject: " + gameObject.name);
            }
        }
Пример #16
0
        void ProcessStringLog(ExciteOMeter.DataType type, float timestamp, string message, MarkerLabel label)
        {
            bool written = LoggerController.instance.WriteLine(LogName.EventsAndMarkers,
                                                               ExciteOMeterManager.ConvertFloatToString(timestamp) + "," +
                                                               type.ToString() + "," +
                                                               message + "," +
                                                               label.ToString());

            if (!written)
            {
                Debug.LogWarning("The Logger Controller has not been setup to store strings. Please setup a file with LogID EventsAndMarkers.");
            }
        }
        // In case it is inheriting from InletIntSample instead of InletIntChunk
        // protected void Process2(int[] newSample, double timestamp)
        protected override void Process(int[] newSample, double timestamp)
        {
            // Debug.Log($"Receiving from stream {StreamName}, first sample {newSample[0]}");

            //TODO: The event only sends float[], all samples need to be parsed to float

            if (newSample[0] > 1.0e7)  // Check values with bad parsing
            {
                Debug.Log("Error parsing value ECG: " + BitConverter.ToString(BitConverter.GetBytes(newSample[0])) + ", " + newSample[0].ToString("F2"));
            }
            else
            {
                EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), newSample[0]);
                LoggerController.instance.WriteLine(LogName.VariableRawECG, timestamp.ToString("F0") + "," + newSample[0].ToString("F0"));
            }
        }
Пример #18
0
        /// <summary>
        /// Callback method for the Resolver gets called each time the resolver found a stream
        /// </summary>
        /// <param name="stream"></param>
        public virtual void AStreamIsFound(LSLStreamInfoWrapper stream)
        {
            if (!isTheExpected(stream))
            {
                return;
            }

            ExciteOMeterManager.DebugLog(string.Format("LSL Stream {0} found for {1}", stream.Name, name));

            // In case the signal emulator is running. Deactivate it.
            EoM_SignalEmulator.DisableEmulator();

            inlet            = new LSL.liblsl.StreamInlet(stream.Item);
            expectedChannels = stream.ChannelCount;

            OnStreamAvailable();
        }
Пример #19
0
 /// <summary>
 /// Set instance for settings object and initialize callbacks of UI
 /// </summary>
 private void Awake()
 {
     // Check singleton, each time the menu scene is loaded, the instance is replaced with the newest script
     if (instance == null)
     {
         instance = this;
         // This object is destroyed only when returninig from EoM offline analysis
         DontDestroyOnLoad(instance.gameObject);
     }
     else
     {
         Destroy(this.gameObject);
         // Uncomment to update Singleton when loading a new scene?
         //Destroy(instance.gameObject);
         //instance = this;
     }
 }
Пример #20
0
        public bool SaveScreenshot()
        {
            if (!recordingScreenshots)
            {
                return(false);
            }

            string screenshot_filepath = ScreenRecorder.instance.CaptureScreenshot();

            if (screenshot_filepath == null)
            {
                return(false);
            }

            // Screenshot was successful
            EoM_Events.Send_OnStringReceived(DataType.Screenshots, ExciteOMeterManager.GetTimestamp(), screenshot_filepath, MarkerLabel.NONE);

            return(true);
        }
Пример #21
0
        /// <summary>
        /// Override this method to implement whatever should happen with the samples...
        /// IMPORTANT: Avoid heavy processing logic within this method, update a state and use
        /// coroutines for more complexe processing tasks to distribute processing time over
        /// several frames
        /// </summary>
        /// <param name="newSample"></param>
        /// <param name="timeStamp"></param>
        protected override void Process(int[] newSample, double timestamp)
        {
            //Assuming that a sample contains at least 3 values for x,y,z
            // Debug.Log($"Receiving from stream {StreamName}, first sample {newSample[0]}");

            if (newSample[0] > 1.0e7)  // Check values with bad parsing
            {
                ExciteOMeterManager.DebugLog("Error parsing value ACC: " + BitConverter.ToString(BitConverter.GetBytes(newSample[0])) + ", " + newSample[0].ToString("F2"));
            }
            else
            {
                //EoM_Events.Send_OnDataReceived(VariableType, new float[3]{(float) newSample[0], (float) newSample[1], (float) newSample[2]});
                // TODO: The event for ACC should be a different delegate receiving float[] instead of single float.
                EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[0]);
                EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[1]);
                EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[2]);

                LoggerController.instance.WriteLine(LogName.VariableRawACC, timestamp.ToString("F2") + "," + newSample[0].ToString("F0") + "," + newSample[1].ToString("F0") + "," + newSample[2].ToString("F0"));
            }
        }
Пример #22
0
        public void StartLogSession()
        {
            if (!currentlyLogging)
            {
                // Allow custom events from the end user
                ExciteOMeterManager.instance.OnStartSessionLog.Invoke();

                currentlyLogging = true;
                ExciteOMeterManager.SetCurrentlyRecordingVariable(true);

                // START LOGGING
                SetLoggingState(true);
                // Creates directory and files for CSV logging
                SetupNewLog();
                // Setups a JSON file to be saved in the same directory
                SessionVariablesController.instance.SetupNewSession(thisLogPath);

                // SEND EoM Event
                EoM_Events.Send_OnLoggingStateChanged(currentlyLogging);
            }
        }
Пример #23
0
        // Creates an entire new session for the specific log
        private void SetupNewLog()
        {
            // Clean loggers from previous session. They are cleaned afterwards because after log finishes,
            // logs are needed for postprocessing of ExciteOMeter level
            dictLoggers.Clear();

            // Create new folder based on the time of creation of the session
            DateTime initialTimeLog   = DateTime.Now;
            string   folder_timeStamp = DateTime.Now.ToString(ExciteOMeterManager.GetFormatTimestampDateTime()); // If changed the format, change it in the OfflineAnalyzerManager.cs

            thisLogPath = folderToSaveLogs + "/" + folder_timeStamp + "_" + SettingsManager.Values.logSettings.sessionId + "/";
            Directory.CreateDirectory(thisLogPath);

            recordingScreenshots = SettingsManager.Values.logSettings.recordScreenshots;
            screenshotPeriodSecs = SettingsManager.Values.logSettings.screenshotsPeriodSecs;

            if (recordingScreenshots)
            {
                Directory.CreateDirectory(thisLogPath + screenshot_folder);
                ScreenRecorder.instance.SetupScreenRecorder(thisLogPath + screenshot_folder, Camera.main);
            }

            // Setup loggers for this session
            foreach (DataLogger logger in loggers)
            {
                if (logger.logID == LogName.UNDEFINED)
                {
                    Debug.LogError("Error configuring loggers. Please identify a unique logID from the list for the file" + logger.filename);
                }
                else if (dictLoggers.ContainsKey(logger.logID))
                {
                    Debug.LogError("Error configuring logger. Another log has been configured with the logID " + logger.logID + " in " + dictLoggers[logger.logID].filename + " . Please define a unique logId for the filename for " + logger.filename);
                }
                else
                {
                    dictLoggers.Add(logger.logID, logger);
                    logger.InitializeDataLogger(thisLogPath + logger.filename, logger.headers);
                }
            }
        }
Пример #24
0
        private void SetLoggingState(bool state)
        {
            // Define first timestamp of the log
            if (state == true)
            {
                // Started logging
                firstTimestamp = ExciteOMeterManager.GetTimestamp();
                ExciteOMeterManager.DebugLog("First timestamp at " + firstTimestamp.ToString());
                isFirstTimestampConfigured = true;
            }
            else
            {
                // Stopped logging
                isFirstTimestampConfigured = false;
                firstTimestamp             = 0.0f;
                ExciteOMeterManager.DebugLog("First timestamp back to " + firstTimestamp.ToString());
            }

            foreach (DataLogger l in loggers)
            {
                l.IsLogging = currentlyLogging;
            }
        }
Пример #25
0
        // Start is called before the first frame update
        void Start()
        {
            // PostProcessing flag
            numInstances++;

            // Default values, if this needs to be changed per feature reimplementing the following function in the child class
            isTimeBasedFeature  = !SettingsManager.Values.featureSettings.DEFAULT_IS_SAMPLE_BASED;
            windowTime          = SettingsManager.Values.featureSettings.DEFAULT_WINDOW_TIME_SECS;
            overlappingFraction = SettingsManager.Values.featureSettings.DEFAULT_OVERLAP_FRACTION;

            isSampleBasedFeature = SettingsManager.Values.featureSettings.DEFAULT_IS_SAMPLE_BASED;
            sampleBuffer         = SettingsManager.Values.featureSettings.DEFAULT_SAMPLE_BUFFER;
            overlappingSamples   = SettingsManager.Values.featureSettings.DEFAULT_OVERLAP_SAMPLES;

            offsetSamplesTimestamp = SettingsManager.Values.featureSettings.DEFAULT_OFFSET_SAMPLES_TIMESTAMP;

            // Only for sample-based, whether input and output are forced to have same length by resampling data.
            matchLengthOfInputSignal = SettingsManager.Values.featureSettings.matchInputOutputLength;

            // CONDITIONS
            if (isSampleBasedFeature)
            {
                if (overlappingSamples >= sampleBuffer)
                {
                    // Error, the number of samples to delete
                    ExciteOMeterManager.LogInFile("The samples to delete in buffer feature " + outputDataType.ToString() + " are larger than sampleBufferLength. Check config.json");
                    overlappingSamples = 0;
                }
                else if (offsetSamplesTimestamp > overlappingSamples)
                {
                    ExciteOMeterManager.LogInFile("The offset timestamp of timestamp in feature " + outputDataType.ToString() + " needs to be lower than overlapSamplesLength. Check config.json");
                    offsetSamplesTimestamp = 0;
                }
            }
            // If there are some configurations needed for the specific feature
            SetupStart();
        }
        ////////////// MULTIDIMENSIONAL FEATURES
        /// <summary>
        /// Calculate features from a MULTIDIMENSIONAL sample-based feature
        /// </summary>
        void SampleBasedCalculationArray()
        {
            // Check that data is available
            if (timestamps.Count == 0 && dataBufferArray.Count == 0)
            {
                // ExciteOMeterManager.DebugLog("No timestamps or data were found to calculate features");
                ExciteOMeterManager.LogInFile("No incoming data " + incomingDataType.ToString() + " was found to calculate feature " + outputDataType.ToString());
                return;
            }
            // Calculate feature
            featureArray = CalculateFeatureArray(timestamps.ToArray(), dataBufferArray.ToArray());

            // Calculate offset of timestamp that corresponds to the calculated feature (# displacements to the left in timestamps)
            // Examples: Assume `sampleBuffer=5`
            //           If `offsetSamplesTimestamp=0`, t for calculated feature is [t-4,t-3,...,t]
            //           If `offsetSamplesTimestamp=3`, t for calculated feature is [t-1,t,t+1,t+2,t+3]
            indexOffsetForTimestamp = sampleBuffer - offsetSamplesTimestamp - 1;

            // Send events and log in file
            ExciteOMeterManager.DebugLog("A new feature was calculated in " + outputDataType.ToString() + ": " + timestamps[indexOffsetForTimestamp] + ", Length: " + featureArray.Length.ToString());

            // Flag to know if it is the first calculation of the feature.
            // If so, the new feature has to match all the timestamps existing before the first timestamp of the feature.
            if (matchLengthOfInputSignal)
            {
                if (isFirstCalculatedFeature)
                {
                    isFirstCalculatedFeature = false;
                    idxStartRepeating        = 0; // No previous data in array, repeat from beginning of input timestamps.
                }
                else
                {
                    // CASE: Match length and buffer already contains data from previous window
                    // Based on overlap and offset, the position where to start repeating timestamps is the following formula.
                    idxStartRepeating = overlappingSamples - offsetSamplesTimestamp;
                }

                // Get the DataType for each of the features that are calculated
                DataType[] featureDataTypes = Constants.SubsetOfFeaturesTransformDataTypes(logIdentifier);

                // Fill the previous timestamps of the input signal with the same value of this feature.
                for (int i = idxStartRepeating; i <= indexOffsetForTimestamp; i++)
                {
                    // Write in files to collect data corresponding to

                    // Create string to save in CSV
                    string featureArrayText = "";
                    foreach (float v in featureArray)
                    {
                        featureArrayText += "," + ExciteOMeterManager.ConvertFloatToString(v, 4);
                    }

                    bool logIsWriting = LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[i]) + featureArrayText);
                    if (!logIsWriting)
                    {
                        Debug.LogWarning("Error writing movement data. Please setup LoggerController with a file with LogID is" + logIdentifier.ToString());
                    }


                    //// --------- TODO
                    //// Send an event with the multidimensional data for the receivers taht can handle multidimensionality
                    //EoM_Events.Send_OnDataArrayReceived(outputDataType, timestamps[i], featureArray);

                    //// Visualizer is designed to analyze unidimensional data, therefore multidimensional needs to be sent one by one to the system
                    //StartCoroutine(SendDataEventsMovement(ExciteOMeterManager.GetTimestamp()));
                    // BUG: Sending events from the coroutine does not seem to be received...
                    // ---------
                    // If the above works, delete the remaining code!! ----------------------
                    if (featureDataTypes.Length != featureArray.Length)
                    {
                        Debug.LogError("Mismatch between the calculated array of features and the expected, in feature with logIdentifier" + logIdentifier);
                        return;
                    }

                    for (int j = 0; j < featureArray.Length; j++)
                    {
                        EoM_Events.Send_OnDataReceived(featureDataTypes[j], timestamps[i], featureArray[j]);
                    }
                    /// --------------------------------------------------------
                }
            }
            else
            {
                // CASE: DO NOT MATCH LENGTH OF INPUT SIGNAL, BUT USE TIMESTAMP DIFFERENT THAN LAST SAMPLE
                EoM_Events.Send_OnDataReceived(outputDataType, timestamps[indexOffsetForTimestamp], featureValue);
                LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[indexOffsetForTimestamp]) + "," + ExciteOMeterManager.ConvertFloatToString(featureValue));
            }

            // Rearrange overlap in signal
            elementsToDelete = sampleBuffer - overlappingSamples;

            timestamps.RemoveRange(0, elementsToDelete);
            dataBufferArray.RemoveRange(0, elementsToDelete);
        }
Пример #27
0
        public void FixedUpdate()
        {
            if (isConfigured && ExciteOMeterManager.currentlyRecordingSession)
            {
                // Timer control
                elapsedTime += Time.fixedDeltaTime;

                // Send data each "sendingPeriod"
                if (elapsedTime >= sendingPeriod)
                {
                    // Reset timer for next event
                    elapsedTime = 0.0f;
                    // Define array
                    transformArray[0] = objectToTrack.position.x;
                    transformArray[1] = objectToTrack.position.y;
                    transformArray[2] = objectToTrack.position.z;
                    transformArray[3] = objectToTrack.localRotation.w;    // Quaternion without axis.
                    transformArray[4] = objectToTrack.localRotation.x;    // Q in the axis i
                    transformArray[5] = objectToTrack.localRotation.y;    // Q in the axis j
                    transformArray[6] = objectToTrack.localRotation.z;    // Q in the axis k
                    transformArray[7] = objectToTrack.localEulerAngles.x; // Pitch (up-down)
                    transformArray[8] = objectToTrack.localEulerAngles.y; // Yaw (left-right)
                    transformArray[9] = objectToTrack.localEulerAngles.z; // Roll (towards shoulders)

                    // Create string to save in CSV
                    transformArrayText = "";
                    foreach (float v in transformArray)
                    {
                        transformArrayText += "," + ExciteOMeterManager.ConvertFloatToString(v, 4);
                    }

                    logIsWriting = LoggerController.instance.WriteLine(logToWrite, ExciteOMeterManager.GetTimestampString() + transformArrayText);
                    if (!logIsWriting)
                    {
                        Debug.LogWarning("Error writing movement data. Please setup LoggerController with a file with LogID is" + logToWrite.ToString());
                    }

                    //Debug.Log("Sending Movement from " + gameObject.name + " > " + transformArrayText);

                    // Send an event with the multidimensional data for the receivers taht can handle multidimensionality
                    EoM_Events.Send_OnDataArrayReceived(DataType.Headset_array, ExciteOMeterManager.GetTimestamp(), transformArray);

                    // Send values individually, because they need to be stored in the .json file as unidimensional data, so that they can be visualized

                    // --------- TODO
                    //// Visualizer is designed to analyze unidimensional data, therefore multidimensional needs to be sent one by one to the system
                    //StartCoroutine(SendDataEventsMovement(ExciteOMeterManager.GetTimestamp()));
                    // BUG: Sending events from the coroutine does not seem to be received...
                    // ---------
                    // If the above works, delete the remaining code!! ----------------------
                    if (transformArray.Length != typesTransformArray.Length)
                    {
                        Debug.LogError("The movement arrays are not the same length");
                    }

                    for (int i = 0; i < transformArray.Length; i++)
                    {
                        EoM_Events.Send_OnDataReceived(typesTransformArray[i], ExciteOMeterManager.GetTimestamp(), transformArray[i]);
                    }
                    /// --------------------------------------------------------
                }
            }
        }
Пример #28
0
 public void SendAutomaticMarker2()
 {
     EoM_Events.Send_OnStringReceived(DataType.AutomaticMarkers, ExciteOMeterManager.GetTimestamp(), "RMSSD > 3*SD", MarkerLabel.ABNORMAL_RMSSD);
     LoggerController.instance.SaveScreenshot();
 }
Пример #29
0
        public static bool Calculate(SessionVariables sessionData)
        {
            // Check that both arrays are the same length
            int N = sessionData.RMSSD.timestamp.Count;

            // Assess length of each timeseries, by default are assummed equal
            bool equalLength = true, useRRi = false, useRMSSD = false;

            if (sessionData.RRi.timestamp.Count != sessionData.RMSSD.timestamp.Count)
            {
                // timeseries are different length
                equalLength = false;
                N           = Math.Min(sessionData.RRi.timestamp.Count, sessionData.RMSSD.timestamp.Count);

                // Which is the shorter signal?
                if (N == sessionData.RRi.timestamp.Count)
                {
                    useRRi = true;
                }
                else if (N == sessionData.RMSSD.timestamp.Count)
                {
                    useRMSSD = true;
                }
            }

            // Mean values
            float meanRRi   = 0f;
            float meanRMSSD = 0f;

            for (int i = 0; i < N; i++)
            {
                // Check timestamps are similar :: +/-10ms offset
                if (Math.Abs(sessionData.RRi.timestamp[i] - sessionData.RMSSD.timestamp[i]) > 0.1f)
                {
                    ExciteOMeterManager.DebugLog("WARNING: Two timestamps differ in more than 0.1s");
                }

                // Cumulative sum
                meanRRi   += sessionData.RRi.value[i];
                meanRMSSD += sessionData.RMSSD.value[i];
            }
            meanRRi   = meanRRi / N;
            meanRMSSD = meanRMSSD / N;

            // Standard deviation
            float stdRRi   = 0f;
            float stdRMSSD = 0f;

            for (int i = 0; i < N; i++)
            {
                stdRRi   += (float)Math.Pow(sessionData.RRi.value[i] - meanRRi, 2);
                stdRMSSD += (float)Math.Pow(sessionData.RMSSD.value[i] - meanRMSSD, 2);
            }
            stdRRi   = (float)Math.Sqrt(stdRRi / N);
            stdRMSSD = (float)Math.Sqrt(stdRMSSD / N);

            // Placeholder for z-score
            double zScoreRRi = 0f, percentileRRi = 0f;
            double zScoreRMSSD = 0f, percentileRMSSD = 0f;

            // Final EoM level
            float timestampEOM = 0.0f;
            float valueEOM     = 0.0f;

            // Calculate final EoM level
            for (int i = 0; i < N; i++)
            {
                zScoreRRi   = (double)((sessionData.RRi.value[i] - meanRRi) / stdRRi);
                zScoreRMSSD = (double)((sessionData.RMSSD.value[i] - meanRMSSD) / stdRMSSD);

                percentileRRi   = Phi(zScoreRRi);
                percentileRMSSD = Phi(zScoreRMSSD);

                // Average of percentile of RRI and percentile of RMSSD
                valueEOM = (1 - (float)(percentileRRi + percentileRMSSD) / 2.0f);

                if (equalLength)
                {
                    timestampEOM = (sessionData.RRi.timestamp[i]);
                }
                else if (useRRi)
                {
                    timestampEOM = (sessionData.RRi.timestamp[i]);
                }
                else if (useRMSSD)
                {
                    timestampEOM = (sessionData.RMSSD.timestamp[i]);
                }

                // Add to class that is exported as JSON
                SessionVariablesController.instance.WritePostProcessedExciteOMeterIndex(timestampEOM, valueEOM);
                // Add to CSV file
                LoggerController.instance.WriteLine(LogName.EOM, timestampEOM.ToString("F6") + "," + valueEOM.ToString("F5"));
            }

            return(true);
        }