/// <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"); } }
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); }
/// <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])); }
// 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(); }
/// <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)); }
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; }
/// <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); }
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(); }); } }
////// 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(); }
/// <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(); }
/// <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); } }
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")); } }
/// <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(); }
/// <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; } }
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); }
/// <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")); } }
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); } }
// 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); } } }
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; } }
// 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); }
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]); } /// -------------------------------------------------------- } } }
public void SendAutomaticMarker2() { EoM_Events.Send_OnStringReceived(DataType.AutomaticMarkers, ExciteOMeterManager.GetTimestamp(), "RMSSD > 3*SD", MarkerLabel.ABNORMAL_RMSSD); LoggerController.instance.SaveScreenshot(); }
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); }