From bcf43ae53dbcb428d81fa60f5b4d04e4d89d784f Mon Sep 17 00:00:00 2001 From: Christian Henning Date: Sun, 6 Jan 2019 13:20:46 +0100 Subject: [PATCH] The GUI during an experiment was refurbished and improved. Two major features were added: the possibility to pause and stop a recording. The code has been extensively tested in our lab over several months and no bugs have been detected so far. --- CONTRIBUTING.md | 17 + README.md | 23 +- TODO.md | 10 +- control/README.md | 55 ++- control/behavior_camera/bcam_view.m | 47 +- control/behavior_camera/preview_bcams.m | 7 +- control/experiment_design/getAnalogDesign.m | 3 +- control/experiment_design/getDigitalDesign.m | 3 +- control/experiment_design/getShockDesign.m | 2 +- control/experiment_design/getSoundDesign.m | 3 +- control/experiment_design/getTriggerDesign.m | 10 +- control/experiment_design/inputDataCallback.m | 152 +++++- control/experiment_design/queueOutputDesign.m | 90 +++- control/experiment_design/readDesign.m | 2 +- control/experiment_design/setInputListener.m | 14 +- control/experiment_design/setOutputListener.m | 27 ++ control/helper/bin2matInput.m | 66 ++- control/helper/bin2matOutput.m | 15 +- control/helper/cleanupProgram.m | 83 ++++ control/helper/isEventInWindow.m | 29 +- control/helper/myError.m | 3 + control/helper/playSounds.m | 127 ++++- control/helper/preprocessParams.m | 8 + control/helper/resetAllChannels.m | 106 +++++ control/params.m | 46 +- control/run_experiment.m | 108 +++-- control/view/PauseEventData.m | 34 ++ control/view/RecViewCtrl.m | 433 ++++++++++++++++++ control/view/rec_view.fig | Bin 0 -> 11448 bytes control/view/rec_view.m | 286 ++++++++++++ docs/imgs/recording_view_screenshot.PNG | Bin 0 -> 76579 bytes lib/README.md | 12 + 32 files changed, 1678 insertions(+), 143 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 control/helper/cleanupProgram.m create mode 100644 control/helper/resetAllChannels.m create mode 100644 control/view/PauseEventData.m create mode 100644 control/view/RecViewCtrl.m create mode 100644 control/view/rec_view.fig create mode 100644 control/view/rec_view.m create mode 100755 docs/imgs/recording_view_screenshot.PNG diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6f88516 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing to the Control Software of our Experimental Setups + +First of all (and **most importantly**), we only want to have code contributions that follow basic coding guidelines. There is no specific guideline for this repository chosen yet, the current ones should become pretty obvious when browsing the existing code. + +* Lines should be no longer than **80 characters** +* All names (even local variables) must reflect the meaning of its entity +* All methods/functions need a docstring describing its purpose, arguments and return values +* If possible, program object-oriented. Example classes (to compare coding guidelines) are [DesignIterator](misc/experiment_design/DesignIterator.m) and [RecordingDesign](misc/experiment_design/RecordingDesign.m) +* All changes should be well documented + +## What can be pushed to the *master* branch? + +Only code, that is general for all kinds of supported experimental setups may be pushed to the [control](control) folder. Supported experimental setups are only restricted by the capabilities of a NIDAQ board. + +Whenever you have to develope code, that is specific to a certain experiment, you are obligated to **create a new branch**. Otherwise, you would render the software potentially unusable for future experiments. + +If you intend to push code specific to a certain experiment type (such as an evaluation pipeline for *active-avoidance* experiments), then you have to make sure, that the code is as general as possible with respect to all possibilities covered by this experimental type (if not possible, documented these cases well). If you cannot go through this effort, you have to develope your program in a separate branch. diff --git a/README.md b/README.md index de77289..01a7c2b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Conducting sophisticated Fear Conditioning, Active Avoidance and other Experiments with a Single Software -This repository is meant to control fear conditioning (FC), active avoidance (AA) or other experimental setups. The software was developed in the lab of [Benjamin Grewe](https://www.ini.uzh.ch/people/bgrewe). The code allows a simple way to design and run complex experiments and record for multiple subjects in parallel; all controlled and aligned with a single computer. It also contains an expressive evaluation pipeline for FC experiments to automatically evaluate the outcome of an experiment once the data is acquired. +This repository is meant to control fear conditioning (FC), active avoidance (AA) or other experimental setups. The software was developed in the lab of [Benjamin Grewe](https://www.ini.uzh.ch/people/bgrewe). The code allows a simple way to design and run complex real-time experiments and record for multiple subjects in parallel; all controlled and aligned with a single computer. It also contains an expressive evaluation pipeline for FC experiments to automatically evaluate the outcome of an experiment once the data is acquired. ## Installation The software is developed in Matlab (tested with Matlab R2017b). It makes use of several toolboxes, such as the [Image Acquisition Toolbox](https://ch.mathworks.com/products/imaq.html) and the [Data Acquisition Toolbox](https://ch.mathworks.com/products/daq.html). -Before running the programs, make sure that all the **required libraries** are downloaded and stored in the folder `lib`. A list of all required libraries and their download links are provided in the `lib` folder. +Before running the programs, make sure that all the **required libraries** are downloaded and stored in the folder [lib](lib). A list of all required libraries and their download links are provided in the [lib](lib) folder. ## Documentation -More documentation is provided in the folder `docs`. The code is well documented and should be straight-forward to use by users, that are familiar with Matlab. After reading the documentation, one should start by exploring the repository (just opening files and reading their docstrings). Additional documentation can be requested from . +More documentation is provided in the folder [docs](docs). The code is well documented and should be straight-forward to use by users, that are familiar with Matlab. After reading the documentation, one should start by exploring the repository (just opening files and reading their docstrings). Additional documentation can be requested from . ## Experimental Setup @@ -41,7 +41,11 @@ We now provide a detailed explanation for some of the above folders. ### Control -The fear conditioning or active-avoidance setup is controlled by the `run_experiment` script in the folder `control`. An experiment is configured via `params.m` in the same folder. Recordings can either be controlled by a design file or generated ad-hoc (note, automatically generated design files only allow very simplistic designs). +The setup is controlled by the [run_experiment](control/run_experiment.m) script in the folder [control](control). The control software provides a user-friendly way to conduct an experiment. Its focus lays on **ensuring the correct timing of all stimuli, such that they can be precisely aligned with neural recordings**. + +![A screenshot of the control software during a recording. Note, the content is blurred.](docs/imgs/recording_view_screenshot.PNG) + +An experiment is configured via [params.m](control/params.m) in the same folder. Recordings can either be controlled by a design file or generated ad-hoc (note, automatically generated design files only allow very simplistic designs). Here is a short summary of the main features currently implemented (June 24th, 2018): * Capability to run multiple recordings in parallel @@ -51,12 +55,13 @@ Here is a short summary of the main features currently implemented (June 24th, 2 * Sounds can be played via the NIDAQ board or via the soundcard (in which case one can record them to correct timing afterwards) * Recordings can be started via an external trigger * Arbitrary trigger signals can be generated +* Sessions can be paused and terminated ### Experiments -All experiment related scripts should be in the folder `experiments`. The subfolders contain code specific to an experiment, including how to generate the design file. Note, that some experiments may use outdated design file formats. One can always check the design file format via the script `misc/experiment_design/check_design_file.m`. In the folder `experiments/examples` are examples specifically designed to make new users familiar with the design file programming. +All experiment related scripts should be in the folder [experiments](experiments). The subfolders contain code specific to an experiment, including how to generate the design file. Note, that some experiments may use outdated design file formats. One can always check the design file format via the script [check_design_file](misc/experiment_design/check_design_file.m). In the folder [experiments/examples](experiments/examples) are examples specifically designed to make new users familiar with the design file programming. ### Evaluation -Preliminary evaluation scripts are available in the folder `evaluation`. **Note**, we currently provide only code for FC experiments. The provided evaluation pipeline only looks at the animals behavior. +Preliminary evaluation scripts are available in the folder [evaluation](evaluation). **Note**, we currently provide only code for FC experiments. The provided evaluation pipeline only looks at the animals behavior. The evaluation code requires that freezing traces have been extracted beforehand. The freezing detection software is currently hosted in another repository *FreezingDetection*. To get access to our freezing detection pipeline, please contact or . @@ -76,10 +81,10 @@ The design file is one of the most important aspects, as it completely defines t The design file itself is a file named `experiment.mat`, that contains a variable `experiment`. -The design file is the only piece that a user has to contribute to control its experiment with this software collection. It adheres a certain format that can be checked via the script `misc/experiment_design/check_design_file.m`. +The design file is the only piece that a user has to contribute to control its experiment with this software collection. It adheres a certain format that can be checked via the script [check_design_file](misc/experiment_design/check_design_file.m). The design file is contained in the design folder (which may incorparate other files that are referenced in the design file via relative paths (e.g., sound files)). The experiment is designed in a tree structure (where edges may be multi-edges). The first level are *cohorts*, which themselves split into *groups* (one may also think of groups and subgroups). These are divided into *sessions*, which are split into *subjects*. -Please run the script `experiments/examples/fear_conditioning/generate_fc_design_file.m` to generate an example design folder. The script expects an output folder name as argument. Please explore the resulting `experiment.mat` file (it also generates a JSON file with the same content, in case this is easier to read). +Please run the script [experiments/examples/fear_conditioning/generate_fc_design_file.m](experiments/examples/fear_conditioning/generate_fc_design_file.m) to generate an example design folder. The script expects an output folder name as argument. Please explore the resulting `experiment.mat` file (it also generates a JSON file with the same content, in case this is easier to read). -A detailed explanantion of design file format can be found in the `docs` folder. +A detailed explanantion of design file format can be found in the [docs](docs) folder. diff --git a/TODO.md b/TODO.md index cb0b404..24f68b3 100644 --- a/TODO.md +++ b/TODO.md @@ -6,10 +6,11 @@ ## Setup Control * Add plots, that show output design of all channels. -* Allow cancelling the recording while it is running (delete cameras, reset nidaq, write 0 to all used NIDAQ channels). * Replace assertions by meaningful error messages. -* Add more visualizations (feedback) during a recording (e.g., state if an event is pushed to the NIDQAQ). TODO, what to do, during bulkmode? -> Solution, decouple output from data pushed to the NIDAQ (just use the NIDQQ start time timestamp to align events from the design file with the OS time) -* Add a few example `params.m` files. +* Add a few example [params](control/params.m) files. +* Log events at correct time. When data is send to the event logger (`RecViewCtrl.logEvent`), we should also pass a time stamp. Then we put these events into a queue (sorted by their time stamp). We can then log the events at the right time by clearing the queue inside the function `updateTime` (there doesn't seem to be a good priority queue implementation in Matlab). +* Tool [take_reference_frame](control/tools/take_reference_frame.m): Ensure that ROIs close to a boundary are pushed to the boundary. +* When a pause (or continue) request is scheduled in [queueOutputDesign](control/experiment_design/queueOutputDesign.m), then set a session variable indicating when the scheduled window will start. This can be used in the GUI as a static text field, that shows how many seconds are left until the session actually pauses/continues. ## Evaluation * For memory efficiency, traces are currently stored as event lists. However, to extract information from them we convert them to binary traces over time. Instead, we should add function that allows to condition and compare event-lists. @@ -22,10 +23,11 @@ * One could redo it from scratch, where the design itself is decomposed into objects (shocks (inherited from digital events), sounds (inherited from analog events), analog and digital itself inherited from events ...), that can be serialized and written to a file. * Write a GUI that visualizes an experimental design (plot single subject designs, play tones, ...). * Maybe we write some classes that give an interface to read and manipulate designs, this could be used to create designs, control them and to evaluate an experiment. -* Enrich the method `getRelFolder` of class `DesignIterator`, such that singletons are not part of the path (e.g., an experiment that only contains a single cohort, doesn't need *Cohort1* in its path). How to ensure downwards compatibility? We could check whether the `p.rootDir` already exists and what path is used there? Note, that an auto-generated design would still require the full path. +* Enrich the method `getRelFolder` of class [DesignIterator](misc/experiment_design/DesignIterator.m), such that singletons are not part of the path (e.g., an experiment that only contains a single cohort, doesn't need *Cohort1* in its path). How to ensure downwards compatibility? We could check whether the `p.rootDir` already exists and what path is used there? Note, that an auto-generated design would still require the full path. ## Postprocessing * There should be a common postprocessing for all recordings independent of the experiment type. Possible tasks are listed below. The goal is, that the user doesn't have to care about recording issues when evaluating the data * Correct sound onset timing if sound card has been used * Handle dropped camera frames * If miniscope recording has been triggered via NIDAQ, then align all timestamps +* If the user has stopped a recording, then the written data (inputs, outputs and video) have inconsistent timestamps. That should be corrected. diff --git a/control/README.md b/control/README.md index bcdc853..9ace6a7 100644 --- a/control/README.md +++ b/control/README.md @@ -2,7 +2,7 @@ The *control software* controls an experimental setup (with a [NIDAQ](http://www.ni.com/data-acquisition/) device at its core). -The software is configured via the local file `params.m`. Afterwards, one can start a *recording* by running the script `run_experiment.m`. E.g., just enter +The software is configured via the local file [params.m](params.m). Afterwards, one can start a *recording* by running the script [run_experiment.m](run_experiment.m). E.g., just enter ```Matlab >> run_experiment @@ -14,13 +14,13 @@ in the console (ensure, that the current Matlab folder is the folder that contai * A contant HIGH signal for the duration of a recording can be generated via a trigger channel by setting `p.triggerRate` to 0. This is necessary, for instance, when triggering a recording with one of the custom Miniscopes. * If for any reason, a recording has to be aborted, it is advisable to restart Matlab. -* The configurations in `params.m` are checked via the function `control/helper/preprocessParams.m`. This function doesn't produce user-friendly messages at the moment. If an assertion in this function fails for any reason, you have most likely a mistake in your `params.m`. Try to understand why the assertion failed and revise your configuration. -* At the moment, **there must be at least one (analog) input channel specified** in the `params.m`, as we can't access the NIDAQ timestamps otherwise. If no input channel is needed, just specify an unused channel (you can delete the recorded data afterwards). +* The configurations in [params.m](params.m) are checked via the function [preprocessParams.m](helper/preprocessParams.m). This function doesn't produce user-friendly messages at the moment. If an assertion in this function fails for any reason, you have most likely a mistake in your [params.m](params.m). Try to understand why the assertion failed and revise your configuration. +* At the moment, **there must be at least one (analog) input channel specified** in the [params.m](params.m), as we can't access the NIDAQ timestamps otherwise. If no input channel is needed, just specify an unused channel (you can delete the recorded data afterwards). * If digital channels are used, then there must be at least one analog channel specified (which is why we said, that at least one *analog* input channel should be specified). Otherwise, the clock is not initialized and the recording will fail. ## How to run multiple recordings at once? -As described in the `params.m` file, one may run several recordings in parallel and control these with a single computer. This, of course, requires one to have more than one experimental chamber. +As described in the [params.m](params.m) file, one may run several recordings in parallel and control these with a single computer. This, of course, requires one to have more than one experimental chamber. All options, that are recording specific can be specified as matrices, where each row specifies the options for a particular recording. @@ -67,3 +67,50 @@ p.bcROIPosition = [200, 30, 400, 400, 180, 10, 400, 400; ... 150, 35, 400, 400, 160, 15, 400, 400]; ``` +## The Recording View + +This is the GUI displayed during the recording. It logs the events that are currently scheduled and presents a live view of all behavior cameras. + +### The Behavior Live View + +In this part of the GUI, there is a preview of all behavior cameras. Note, that during the recording, the camera is triggered. For instance, if the recording is paused, then there are no triggers provided to the cameras via the NIDAQ and thus the image stalls. + +### The Event Logger + +The event logger should help the user to see what stimuli are presented to the animal, such that he can easily follow the recording. Events that are presented via the NIDAQ, are logged as soon as they are send to the NIDAQ. Thus, if the session runs in bulk mode, all events are logged at the beginning of the session. Otherwise, the events are logged as soon as their window is send to the NIDAQ (which depends on the option `p.continuousWin`). + +For sounds played via the sound card, the events will be logged as soon as they are send to the sound card. + +### The *Stop Recording* button + +This button will interrupt the current recording and close all files. Note, as this interrupts a current NIDAQ session, the data actually send out by the NIDAQ must not match the output data stored in the recording folder. + +### The *Pause* button + +This is a new feature, that **hasn't been heavily tested yet**. The pause button only works in continous mode. The pause button does not immediately trigger a reaction. When the NIDAQ requests the next window of data (in continuous mode), the program handles the *pause request*. Instead of sending the next batch of data, it will force all output channels to be zero. A similar procedure is triggered when pressing the *Continue* button. + +Note, if sounds are played via the sound card, you may observe unwanted behavior. The reason lies in the delay between the request of a pause and the actual onset of the pause. If the sound onset lies in between this delay, then the sound is played, such that the pause is likely to start during the duration of the sound. A user should keep this in mind when using this feature. + +As pausing the session causes an extension of the session duration, the recorded input data is larger than expected. This is corrected in an online fashion, such that pauses are filtered out in the output file `input_data.mat`. As this feature hasn't been heavily tested, there is always an additional file `input_data_raw.mat`, when using the continuous mode, which contains the raw input recordings (together with their timestamps). This is just to prevent data loss, in case there is a bug in the new implementation of the pause button. + +If one wishes to use the raw data, he can filter out the paused time windows by using the matrix `output_windows` stored in the file `output_windows_debug.mat`. This matrix is an `n x 6` matrix, where `n` denotes the number of windows send to the NIDAQ. The meanings of each column are as follows: + +* Start of the window in seconds (`-1` if it is a paused window) +* End of the window in seconds (`-1` if it is a paused window) +* Length of the window in time steps (the number of steps per second is defined by the NIDAQ rate) +* Cumulative number of steps of all windows that have been send so far (including this one) +* Cumulative number of steps of all windows that have been send so far (including this one), excluding the once that were paused +* `0` if normal output window, otherwise `1` (e.g., if pause) + +## Output files + +The software creates an output folder for each recording. Here is a short description of the files, that may be stored in this folder. + +* `logfile.txt`: Contains the console logs, that where displayed during the runtime of the recording (identical for all parallel recordings). +* `params.m`: A copy of the parameters file, used to trigger the recording (identical for all parallel recordings). +* `input_data.mat`: Contains the recordings of all input channels as well as the relative NIDAQ timestamps plus the NIDAQ session start time. The recordings and timestamps purified from possible stall windows (such as pauses or wait windows (when waiting for input callbacks at the end of a continuous session)). +* `input_data_raw.mat`: This file is similar to `input_data.mat` and only created if the session runs in continuous mode. In contrast to `input_data.mat`, the data contained in this file is not purified from stall windows. This file is a backup, in case the purification process fails. In future versions, once the purification is sufficiently tested, the creation of this file should be optional. +* `output_data.mat`: Contains the data, that has been send to the NIDAQ (i.e., the data of all output channels). Stall windows are not contained. +* The folder also contains the behavior videos. +* If no design file is used, the folder will contain the auto-generated design. +* `output_windows_debug.mat`: See the [Pause Button](### The *Pause* button) section. \ No newline at end of file diff --git a/control/behavior_camera/bcam_view.m b/control/behavior_camera/bcam_view.m index 13dfe01..2a37bff 100644 --- a/control/behavior_camera/bcam_view.m +++ b/control/behavior_camera/bcam_view.m @@ -35,22 +35,27 @@ num_recs = d.numRecs; cams_per_rec = size(p.bcDeviceID, 2); - fig = figure('Name', 'Behavior Camera Live View'); + % NOTE, we add the subplots to an uipanel, as we can't add suplots to + % an axes that is within the GUIDE figure. However, for some reasone, + % we cannot set the current figure (using figPanel.Parent) nor the + % current axes (I tried various methods to set gca and gcf). The reason + % is not quite clear. Hence, one always has to set which figure and + % axes has to be used in the following code. + figPanel = d.recView.getBCamPanel(); % FIXME dirty solution to maximize the window. try - warning('off','all') - pause(0.00001); - frame_h = get(handle(gcf),'JavaFrame'); - set(frame_h,'Maximized',1); - warning('on','all') + warning('off', 'all') + pause(0.1); + frame_h = get(handle(figPanel.Parent), 'JavaFrame'); + set(frame_h, 'Maximized', 1); + pause(0.1); + warning('on', 'all') catch warning('Could not maximize window.'); end - subplots = cell(1, num_cams); - - axis('square'); + subplots = cell(1, num_cams); hImages = cell(num_cams, 1); num_rows = floor(sqrt(num_cams)); @@ -64,9 +69,10 @@ nBands = d.bcVidObjects{i}.NumberOfBands; subplots{i} = subplot(num_rows, num_cols, ... - (r-1) * cams_per_rec + c); + (r-1) * cams_per_rec + c, 'Parent', figPanel); - hImages{i} = image( zeros(vidRes(2), vidRes(1), nBands) ); + hImages{i} = image(subplots{i}, zeros(vidRes(2), vidRes(1), ... + nBands)); setappdata(hImages{i}, 'UpdatePreviewWindowFcn', ... @custom_preview_fcn); @@ -76,7 +82,7 @@ preview(d.bcVidObjects{i}, hImages{i}); - title(['Cohort ' num2str(p.cohort(r)) ', ' ... + title(subplots{i}, ['Cohort ' num2str(p.cohort(r)) ', ' ... 'Group ' num2str(p.group(r)) ', ' ... 'Session ' num2str(p.session(r)) ', ' ... 'Subject ' num2str(p.subject(r)) newline ... @@ -84,16 +90,13 @@ 'Frames acquired: ' ... num2str(d.bcVidObjects{i}.FramesAcquired)]); - set(gca,'Visible','off') - set(get(gca,'Title'),'Visible','on') + set(subplots{i}, 'Visible', 'off') + set(get(subplots{i}, 'Title'), 'Visible', 'on') + + %axis(subplots{i}, 'equal') + axis(subplots{i}, 'image') end - end - - % Make sure that previews keep their size. - axesHandles = findobj(get(gcf,'Children'), 'flat', 'Type', 'axes'); - axis(axesHandles, 'equal') - - d.bcFigure = fig; + end % Timer, that updates the frames acquired yet. t = timer('BusyMode', 'drop', 'Period', 0.5, ... @@ -103,8 +106,6 @@ start(t); - - dataObj.d = d; end diff --git a/control/behavior_camera/preview_bcams.m b/control/behavior_camera/preview_bcams.m index d620cae..4dc9fca 100644 --- a/control/behavior_camera/preview_bcams.m +++ b/control/behavior_camera/preview_bcams.m @@ -60,7 +60,6 @@ cncBtn.Position(1) = cncBtn.Position(1) + 100; suptitle('Please check and confirm the behavior camera preview.'); - axis('square'); hImages = cell(num_cams, 1); num_rows = floor(sqrt(num_cams)); @@ -103,12 +102,10 @@ set(gca,'Visible','off') set(get(gca,'Title'),'Visible','on') + + axis(gca, 'image') end end - - % Make sure that previews keep their size. - axesHandles = findobj(get(gcf,'Children'), 'flat', 'Type', 'axes'); - axis(axesHandles, 'equal') logger.info('preview_bcams', ['Please confirm the camera settings ' ... 'as shown in the preview window by clicking "ok".']); diff --git a/control/experiment_design/getAnalogDesign.m b/control/experiment_design/getAnalogDesign.m index ccd3840..0628bde 100644 --- a/control/experiment_design/getAnalogDesign.m +++ b/control/experiment_design/getAnalogDesign.m @@ -43,7 +43,8 @@ analog events specified in the design file (p.analogChannel), not analog [eventInWin, onsetStep, offsetStep, evOnInd, evOffInd] = ... isEventInWindow(session, startTime, numSteps, ... - event.onset, event.duration, numel(event.interp)); + event.onset, event.duration, numel(event.interp), ... + 'analog', event.type, i); if eventInWin design(onsetStep:offsetStep, r, c) = ... diff --git a/control/experiment_design/getDigitalDesign.m b/control/experiment_design/getDigitalDesign.m index a7a6bf8..f4039fb 100644 --- a/control/experiment_design/getDigitalDesign.m +++ b/control/experiment_design/getDigitalDesign.m @@ -42,7 +42,8 @@ digital events specified in the design file (p.digitalChannel), not digital [eventInWin, onsetStep, offsetStep, evOnInd, evOffInd] = ... isEventInWindow(session, startTime, numSteps, ... - event.onset, event.duration, numel(event.interp)); + event.onset, event.duration, numel(event.interp), ... + 'digital', event.type, i); if eventInWin design(onsetStep:offsetStep, r, c) = ... diff --git a/control/experiment_design/getShockDesign.m b/control/experiment_design/getShockDesign.m index ec75f85..407eab1 100644 --- a/control/experiment_design/getShockDesign.m +++ b/control/experiment_design/getShockDesign.m @@ -34,7 +34,7 @@ [eventInWin, onsetStep, offsetStep, evOnInd, evOffInd] = ... isEventInWindow(session, startTime, numSteps, ... - event.onset, event.duration); + event.onset, event.duration, -1, 'us'); if eventInWin channelInd = event.channel; diff --git a/control/experiment_design/getSoundDesign.m b/control/experiment_design/getSoundDesign.m index 63a8651..6263b3e 100644 --- a/control/experiment_design/getSoundDesign.m +++ b/control/experiment_design/getSoundDesign.m @@ -36,7 +36,8 @@ [eventInWin, onsetStep, offsetStep, evOnInd, evOffInd] = ... isEventInWindow(session, startTime, numSteps, ... - event.onset, event.duration, soundDurInSteps); + event.onset, event.duration, soundDurInSteps, ... + 'sound', event.type); if eventInWin design(onsetStep:offsetStep, i, 1) = ... diff --git a/control/experiment_design/getTriggerDesign.m b/control/experiment_design/getTriggerDesign.m index a71d95d..8f64df5 100644 --- a/control/experiment_design/getTriggerDesign.m +++ b/control/experiment_design/getTriggerDesign.m @@ -53,6 +53,7 @@ if p.triggerIsAnalog(i) singleCycle = singleCycle * p.triggerAmplitude(i); end + periodSize = numel(singleCycle); % The start time might be in the middle of a period. Similarly, % the end time might also be in the middle of a period. So the @@ -61,8 +62,15 @@ stepsNeeded = numSteps; % Start index in first cycle. - startIndex = mod(startTime * session.Rate, periodSize) + 1; + startIndex = floor(mod(startTime * session.Rate, ... + periodSize)) + 1; + traceStart = singleCycle(startIndex:end, :); + % What if there is less than one period in the current window? + if numel(traceStart) > stepsNeeded + traceStart = ... + singleCycle(startIndex:startIndex+stepsNeeded-1, :); + end stepsNeeded = stepsNeeded - numel(traceStart); numFullCycles = floor(stepsNeeded / periodSize); diff --git a/control/experiment_design/inputDataCallback.m b/control/experiment_design/inputDataCallback.m index 84ad109..d0c920b 100644 --- a/control/experiment_design/inputDataCallback.m +++ b/control/experiment_design/inputDataCallback.m @@ -1,4 +1,4 @@ -% Copyright 2018 Rik Ubaghs, Christian Henning +% Copyright 2018 Christian Henning, Rik Ubaghs % % Licensed under the Apache License, Version 2.0 (the "License"); % you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ % limitations under the License. %{ @title :inputDataCallback.m -@author :ru, ch +@author :ch, ru @contact :christian@ini.ethz.ch @created :11/30/2017 @version :1.0 @@ -37,16 +37,18 @@ function inputDataCallback(src, evt) if isfield(src.UserData, 'triggerTime') % FIXME there should be no assertions in the code. assert(src.UserData.triggerTime == evt.TriggerTime); + else + d.recView.setRecStartTime(evt.TriggerTime, src); end src.UserData.triggerTime = evt.TriggerTime; - + % This assertion cannot trigger, as this was tested while % preprocessing the parameters. assert(size(p.inputChannel, 1) == 1 || ... size(p.inputChannel, 1) == d.numRecs); for i = 1:d.numRecs - filepath = d.tempInputFileNames{i}; + filepath = d.tempInputRawFileNames{i}; if size(p.inputChannel, 1) == 1 inputData = evt.Data; @@ -56,8 +58,146 @@ function inputDataCallback(src, evt) data = [evt.TimeStamps, inputData]'; - fid = fopen(filepath,'a'); + fid = fopen(filepath, 'a'); fwrite(fid, data, 'double'); fclose(fid); + + % We don't wanna write input data, that was gathered during pauses. + % That is why we need to filter the inputData and timestamps before + % writing them into the file. + if p.correctRecordedInputs && ~p.useBulkMode + data = correctTime(src, evt.TimeStamps, inputData); + + filepath = d.tempInputFileNames{i}; + fid = fopen(filepath, 'a'); + fwrite(fid, data, 'double'); + fclose(fid); + end + end + + % Check, whether all the data of this recording has been read so far + % and we can interrupt the contiuous mode. + if ~p.useBulkMode + lastTS = evt.TimeStamps(end); + wins = src.UserData.d.outputDataWindows; + if ~isempty(wins) + % How much data has been pushed to the NIDAQ so far? + pushedDur = wins(end, 5) / src.Rate; + if pushedDur < (src.UserData.d.duration - 0.001) + % Full recording has not been pushed to the NIDAQ yet. + return; + end + + % We take the last unpaused window, as the windows appended to + % the session, when all data has been pushed to the NIDAQ, are + % paused windows. + winInd = find(wins(:, 6) == 0, 1, 'last'); + + pausedSteps = wins(winInd, 4) - wins(winInd, 5); + pausedSecs = pausedSteps / src.Rate; + lastTS = lastTS - pausedSecs; + end + + lastTS = lastTS - 1/src.Rate; + + if lastTS >= src.UserData.d.duration + % We can safely end the session. + src.stop(); + end + end +end + +function data = correctTime(session, timeStamps, inputData) +% CORRECTTIME Since there might be pauses during the session, we don't +% wanna write this paused data into the written binary files. This function +% will correct the timestamps (to make them look as there were no pauses) +% and makes sure that no input data (recorded during a pause) makes it to +% the output file. +% +% Args: +% - session: The current NIDAQ session. +% - timeStamps: The timestamps delivered for the current input callback. +% - inputData: The input data recorded and delivered to the current input +% callback. +% +% Returns: +% The data that can be written to the binary output file. I.e. we +% concatenate, the corrected timestamps and the input data along the column +% axes and then transpose this matrix (such that rows represent channels. +% -> data = [correctedTimeStamps, correctedInputData]';\ + global inputDataCorrectionFailed; + logger = log4m.getLogger(); + + wins = session.UserData.d.outputDataWindows; + if isempty(wins) + % We cannot correct the data yet. Though, that should be no + % problem, as the first window is always no pause. + return; end -end \ No newline at end of file + + numStepsStart = floor(timeStamps(1) * session.Rate); + + startSteps = [0; wins(1:end-1, 4)]; + % In which window is the first timestamp? + currWinInd = find(numStepsStart >= startSteps & ... + numStepsStart < wins(:, 4), 1, 'last'); + + % This should not happen, as we + if isempty(currWinInd) + inputDataCorrectionFailed = 1; + logger.error('inputDataCallback', ['Could not find window for ' ... + 'current time stamp.']); + data = [timeStamps, inputData]'; + return; + end + + % First timestamp in current window. + fi = 1; + + while 1 + currWinEnd = wins(currWinInd, 4) / session.Rate; + % Last timestamp in current window. + li = find(timeStamps < currWinEnd, 1, 'last'); + + % How many paused steps happened so far? + pausedSteps = wins(currWinInd, 4) - wins(currWinInd, 5); + pausedSecs = pausedSteps / session.Rate; + + % Subtract the seconds, that were paused so far from the timesteps. + % Note, the timestamps might become negative, if the current window + % is paused as well. However, as these timestamps would be within a + % pause, they are not writte into a file. + timeStamps(fi:li) = timeStamps(fi:li) - pausedSecs; + + % If current window is a pause, we don't wanna write this data into + % a file. + if wins(currWinInd, 6) == 1 + inds = true(numel(timeStamps), 1); + inds(fi:li) = 0; + timeStamps = timeStamps(inds); + inputData = inputData(:, inds); + + fi = fi - 1; + li = fi; + end + + if li >= numel(timeStamps) + break; + else + currWinInd = currWinInd + 1; + fi = li + 1; + + % Should not happen. + if currWinInd > size(wins, 1) + inputDataCorrectionFailed = 1; + logger.error('inputDataCallback', ['Could not find ' ... + 'window for current time stamp.']); + data = [timeStamps, inputData]'; + return; + end + end + end + + data = [timeStamps, inputData]'; +end + diff --git a/control/experiment_design/queueOutputDesign.m b/control/experiment_design/queueOutputDesign.m index 70b0260..bd72863 100644 --- a/control/experiment_design/queueOutputDesign.m +++ b/control/experiment_design/queueOutputDesign.m @@ -21,27 +21,49 @@ FIXME this function is very memory inefficient. %} function queueOutputDesign(session, ~) -%QUEUEOUTPUTDATA Control the queuing of data for output channels. - +%QUEUEOUTPUTDESIGN Control the queuing of data for output channels. + global requestingPauseRespCont; + logger = log4m.getLogger(); p = session.UserData.p; d = session.UserData.d; if d.duration <= d.timeRef - % FIXME we have to explicitly stop the continuous mode. We cannot + % We have to explicitly stop the continuous mode. We cannot % run "session.stop();" as this would also stop the processing of % the data that is still on the NIDAQ! - % As a quick fix, we simply send only zeros to the NIDAQ and stop - % it, when this method is called the next time. - if d.stopSession - session.stop(); - else - session.UserData.d.stopSession = true; - numOutputChannels = length(session.Channels) - ... - numel(p.inputChannel); - session.queueOutputData(zeros( ... - floor(p.continuousWin * session.Rate), numOutputChannels)); + % We send zeros to the NIDAQ, until the inputCallback has read the + % last data sample (from the actual recording). The input listener + % will then stop the session. + zeroCyclesAfterRec = d.zeroCyclesAfterRec; + zeroCyclesAfterRec = zeroCyclesAfterRec + 1; + session.UserData.d.zeroCyclesAfterRec= zeroCyclesAfterRec; + + numSteps = floor(p.continuousWin * session.Rate); + + session.UserData.d.totalStepsPushedSoFar = ... + d.totalStepsPushedSoFar + numSteps; + % We consider this extra frame as a pause frame, such that its + % timestamps don't get written to the output file. + session.UserData.d.outputDataWindows = cat(1, ... + d.outputDataWindows, [-1, -1, numSteps, ... + session.UserData.d.totalStepsPushedSoFar, ... + d.recStepsPushedSoFar, 1]); + + numOutputChannels = length(session.Channels) - ... + numel(p.inputChannel); + session.queueOutputData(zeros(numSteps, numOutputChannels)); + + % Sending two empty windows at the end of the session is relatively + % normal. The first one is send when the last recording window is + % processed. The second one is send when this las recording window + % just ended and the input listener is about to receive the last + % data. + if zeroCyclesAfterRec > 2 + logger.debug('queueOutputDesign', ['Waiting to end ' ... + 'the session. Sending another empty window while ' ... + 'waiting for all inputs to be received.']); end return; @@ -54,12 +76,43 @@ function queueOutputDesign(session, ~) else endTime = min(d.duration, startTime + p.continuousWin); end - + numSteps = floor((endTime - startTime) * session.Rate); numStepsDesired = (endTime - startTime) * session.Rate; + + if requestingPauseRespCont == 1 + requestingPauseRespCont = 0; + d.sessionPaused = ~d.sessionPaused; + notify(d.recView, 'PauseEv', PauseEventData(d.sessionPaused)); + end + + d.totalStepsPushedSoFar = d.totalStepsPushedSoFar + numSteps; + if ~d.sessionPaused + d.recStepsPushedSoFar = d.recStepsPushedSoFar + numSteps; + end + + d.outputDataWindows = cat(1, d.outputDataWindows, ... + [startTime, endTime, numSteps, d.totalStepsPushedSoFar, ... + d.recStepsPushedSoFar, d.sessionPaused]); + + if d.sessionPaused + d.outputDataWindows(end, 1:2) = -1; + + %% Pause Session + % If we are pausing the session, we just send zeros accross all + % output channels. + session.UserData.d = d; % Note, that d.timeRef wasn't set yet! + numOutputChannels = length(session.Channels) - ... + numel(p.inputChannel); + logger.debug('queueOutputDesign', ['Pausing: Queueing zeros ' ... + 'as output data for a time frame of ' ... + num2str(endTime-startTime) ' s].']); + session.queueOutputData(zeros(numSteps, numOutputChannels)); + return; + end if numSteps ~= numStepsDesired && startTime == 0 - logger.warn('queueOutputData', ['NIDAQ rate, duration of ' ... + logger.warn('queueOutputDesign', ['NIDAQ rate, duration of ' ... 'recording and/or "p.continuousWin" does not allow ' ... 'perfect time discretization.']); end @@ -93,10 +146,10 @@ function queueOutputDesign(session, ~) analogDesign = getAnalogDesign(session, startTime, numSteps); data2Queue = prepareData4Queue(session, data2Queue, analogDesign, ... 'analog', endTime); - + %d = session.UserData.d; - % Write designs to file. + %% Write designs to file. for i = 1:d.numRecs filepath = d.tempOutputFileNames{i}; @@ -152,7 +205,8 @@ function queueOutputDesign(session, ~) fclose(fid); end - logger.debug('queueOutputData', ['Queueing output data for time ' ... + %% Queue the data to the NIDAQ + logger.debug('queueOutputDesign', ['Queueing output data for time ' ... 'frame [' num2str(startTime) 's, ' num2str(endTime) 's].']); session.queueOutputData(data2Queue); diff --git a/control/experiment_design/readDesign.m b/control/experiment_design/readDesign.m index bf5d8c3..2840f7d 100644 --- a/control/experiment_design/readDesign.m +++ b/control/experiment_design/readDesign.m @@ -73,7 +73,7 @@ Read the properties and subjects (needed for this recording) from the if i == 1 d.duration = subject.getDuration(); - elseif d.duration ~= subject.getDuration(); + elseif d.duration ~= subject.getDuration() myError('readDesign', 'Durations of recordings differ.'); end diff --git a/control/experiment_design/setInputListener.m b/control/experiment_design/setInputListener.m index 951fedc..7fdbc98 100644 --- a/control/experiment_design/setInputListener.m +++ b/control/experiment_design/setInputListener.m @@ -23,14 +23,26 @@ %SETINPUTLISTENER Setup listener for input channels. % session = SETINPUTLISTENER(session) - %p = session.UserData.p; + p = session.UserData.p; d = session.UserData.d; tempInputFileNames = cell(1, d.numRecs); + % This file will contain the raw recordings, where no correction has + % been applied to. + tempInputRawFileNames = cell(1, d.numRecs); for i = 1:d.numRecs tempInputFileNames{i} = fullfile(d.expDir{i}, 'input_data.bin'); + tempInputRawFileNames{i} = fullfile(d.expDir{i}, ... + 'input_data_raw.bin'); end d.tempInputFileNames = tempInputFileNames; + d.tempInputRawFileNames = tempInputRawFileNames; + + % No pauses in this mode (also no problems with ending the session). + if p.useBulkMode + d.tempInputRawFileNames = tempInputFileNames; + d.tempInputFileNames = []; + end % We could further specify when the callback functions is called by % setting NotifyWhenDataAvailableExceeds. diff --git a/control/experiment_design/setOutputListener.m b/control/experiment_design/setOutputListener.m index 498f603..aae04a4 100644 --- a/control/experiment_design/setOutputListener.m +++ b/control/experiment_design/setOutputListener.m @@ -31,10 +31,37 @@ p = session.UserData.p; d = session.UserData.d; + %% Some variables that will be modified during the session. % This will be our timer reference when queuing data on the NIDAQ. % I.e., we will queue data from d.timeRef until % min(session_duration, d.timeRef + p.continuousWin) d.timeRef = 0; + % This flag will tell use, whether we are currently pausing the + % session. + d.sessionPaused = 0; + % This will be an array of size n x 6, where n is the number of output + % windows pushed to the NIDAQ (including paused windows). + % The four numbers per row will be: start of win (in sec), end of win + % (in sec), num steps in win, cumulative number of steps of all wins + % (including this one), cumulative number of steps of all wins except + % paused once and whether it is a pausing window. + % This information can later be used, to handle pauses outside the data + % queuing function. + % Note, that if paused, the time windows have no meaning. + d.outputDataWindows = []; + % The total number of time frames pushed to the NIDAQ so far (including + % paused windows). + % assert(sum(outputDataWindows(:,3)) == totalStepsPushedSoFar); + d.totalStepsPushedSoFar = 0; + % The total number of time frames pushed to the NIDAQ, when the session + % wasn't paused. + % assert(sum(outputDataWindows(outputDataWindows(:, 6) == 0, 3)) ... + % == recStepsPushedSoFar); + d.recStepsPushedSoFar = 0; + % The number of windows pushed to the NIDAQ after the last actual data + % window has been send (i.e., while waiting for the input listener to + % end the session. + d.zeroCyclesAfterRec = 0; tempOutputFileNames = cell(1, d.numRecs); for i = 1:d.numRecs diff --git a/control/helper/bin2matInput.m b/control/helper/bin2matInput.m index e0cd8d1..30a6842 100644 --- a/control/helper/bin2matInput.m +++ b/control/helper/bin2matInput.m @@ -28,24 +28,58 @@ function bin2matInput(session) logger = log4m.getLogger(); for i = 1:d.numRecs - sourceFN = d.tempInputFileNames{i}; - targetFN = fullfile(d.expDir{i}, 'input_data.mat'); - - fid = fopen(sourceFN,'r'); - raw = fread(fid, [size(p.inputChannel, 2)+1, inf], 'double'); - fclose(fid); + sourceFN = d.tempInputRawFileNames{i}; + if p.useBulkMode + targetFN = fullfile(d.expDir{i}, 'input_data.mat'); + else + targetFN = fullfile(d.expDir{i}, 'input_data_raw.mat'); + end + success = convertData(sourceFN, targetFN, ... + session.UserData.triggerTime, p); + + if success + logger.info('bin2matInput', ['Raw time stamps and raw ' ... + 'input channel recordings of recording ' num2str(i) ... + ' are stored in ' targetFN '.']); + end - timestamps = raw(1,:); - inputData = raw(2:size(raw, 1),:); - timestampOffset = session.UserData.triggerTime; + if p.correctRecordedInputs && ~p.useBulkMode + sourceFN = d.tempInputFileNames{i}; + targetFN = fullfile(d.expDir{i}, 'input_data.mat'); + success = convertData(sourceFN, targetFN, ... + session.UserData.triggerTime, p); - save(targetFN, 'timestamps', 'inputData', 'timestampOffset', ... - '-v7.3'); + if success + logger.info('bin2matInput', ['Time stamps and input ' ... + 'channel recordings of recording ' num2str(i) ... + ' are stored in ' targetFN '.']); + end + end + end +end + +function success = convertData(sourceFN, targetFN, sessionStart, p) + success = 1; - logger.info('bin2matInput', ['Time stamps and input channel ' ... - 'recordings of recording ' num2str(i) ' are stored in ' ... - targetFN '.']); - - delete(sourceFN); + logger = log4m.getLogger(); + + try + fid = fopen(sourceFN, 'r'); + raw = fread(fid, [size(p.inputChannel, 2)+1, inf], 'double'); + fclose(fid); + catch + logger.error('bin2matInput', ... + ['Could not read binary file: ' sourceFN]); + success = 0; + return; end + + timestamps = raw(1,:); + inputData = raw(2:size(raw, 1),:); + timestampOffset = sessionStart; + + save(targetFN, 'timestamps', 'inputData', 'timestampOffset', ... + '-v7.3'); + + delete(sourceFN); end \ No newline at end of file diff --git a/control/helper/bin2matOutput.m b/control/helper/bin2matOutput.m index 261baf8..f108b8f 100644 --- a/control/helper/bin2matOutput.m +++ b/control/helper/bin2matOutput.m @@ -35,9 +35,16 @@ function bin2matOutput(session) size(p.shockChannel, 2) + size(p.soundChannel, 2) + ... size(p.soundEventChannel, 2) + size(p.digitalChannel, 2) + ... size(p.analogChannel, 2); - fid = fopen(sourceFN,'r'); - raw = fread(fid, [numOutputChannels, inf], 'double'); - fclose(fid); + + try + fid = fopen(sourceFN,'r'); + raw = fread(fid, [numOutputChannels, inf], 'double'); + fclose(fid); + catch + logger.error('bin2matOutput', ... + ['Could not read binary file: ' sourceFN]); + return; + end offset = 1; triggerData = raw(offset:offset+size(p.triggerChannel, 2)-1,:); @@ -60,7 +67,7 @@ function bin2matOutput(session) 'seventData', 'digitalData', 'analogData', '-v7.3'); logger.info('bin2matOutput', ['Output channel data of ' ... - 'recording ' num2str(i) ' are stored in ' targetFN '.']); + 'recording ' num2str(i) ' is stored in ' targetFN '.']); delete(sourceFN); end diff --git a/control/helper/cleanupProgram.m b/control/helper/cleanupProgram.m new file mode 100644 index 0000000..e3e6ed9 --- /dev/null +++ b/control/helper/cleanupProgram.m @@ -0,0 +1,83 @@ +% Copyright 2018 Christian Henning +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +%{ +@title :cleanupProgram.m +@author :ch +@contact :henningc@ethz.ch +@created :08/27/2018 +@version :1.0 + +This function should be called after the daq session has stopped. +%} +function cleanupProgram(session) +%CLEANUPPROGRAM Cleanup the program after the daq session has stopped. +% +% This includes the stopping and deletion of timers, camera handles, ... +% Clean up DAQ session + logger = log4m.getLogger(); + + p = session.UserData.p; + d = session.UserData.d; + + cleanupFailed = 0; + + try + delete(d.inputListener); + delete(d.errorListener); + if ~p.useBulkMode + delete(d.outputListener); + end + release(session); + catch + cleanupFailed = 1; + logger.error('cleanupProgram', 'DAQ session cleanup failed.'); + end + + % Clean up Online Side Detection. + try + if p.useOnlineSideDetection + stop(d.sideDetectionTimer); + delete(d.sideDetectionTimer); + fclose(d.serialCommObj); + end + catch + cleanupFailed = 1; + logger.error('cleanupProgram', ['Online side detection ' ... + 'cleanup failed.']); + end + + % Clean up behavior cameras. + try + for i = 1:numel(p.bcDeviceID) + stop(d.bcVidObjects{i}); + end + stop(d.bcFigureTimer); + delete(d.bcFigureTimer); + d.recView.closeGUI() + for i = 1:numel(p.bcDeviceID) + delete(d.bcVidObjects{i}); + end + catch + cleanupFailed = 1; + logger.error('cleanupProgram', ['GUI and behavior camera ' ... + 'cleanup failed.']); + end + + if cleanupFailed + msgbox(['The program could not free all allocated resources.' ... + newline 'Please restart Matlab before running a new ' ... + 'recording.'], 'Cleanup Failed', 'error'); + end +end + diff --git a/control/helper/isEventInWindow.m b/control/helper/isEventInWindow.m index c5471df..8446e7e 100644 --- a/control/helper/isEventInWindow.m +++ b/control/helper/isEventInWindow.m @@ -21,7 +21,8 @@ function [eventInWin, onsetStep, offsetStep, evOnsetStep, evOffsetStep] ... = isEventInWindow(session, winStart, numStepsInWin, eventOnset, ... - eventDuration, eventDurInSteps) + eventDuration, eventDurInSteps, eventSource, ... + eventType, eventChannel) %ISEVENTINWINDOW This method checks, whether a specific event occurs within %a given time window. % @@ -34,6 +35,13 @@ % - eventDuration: Duration of event (in seconds). Value is ignored if % eventDurInSteps is given. % - eventDurInSteps: (optional) Duration of event (in NIDAQ time steps). +% - eventSource: (optional) Used for the GUI event logger. The source of +% the event (may be 'sound', 'us', 'analog' or 'digital'). +% - eventType: (optional) Used for the GUI event logger. The type of the +% event (not specified for US events). +% - eventChannel: (optional) Used for the GUI event logger. For analog or +% digital events, this denoted the channel, where the +% event is comming from. % % Returns: % - eventInWin: Flag, whether the event appears within the specified time @@ -54,7 +62,7 @@ endStep = startStep + numStepsInWin - 1; onsetStep = floor(eventOnset * session.Rate); - if (~exist('eventDurInSteps', 'var')) + if ~exist('eventDurInSteps', 'var') || eventDurInSteps == -1 numStepsEvent = floor(eventDuration * session.Rate); else numStepsEvent = eventDurInSteps; @@ -137,5 +145,22 @@ evOnsetStep = -1; evOffsetStep = -1; end + + if evOnsetStep == 1 && exist('eventSource', 'var') + durMin = floor(eventOnset / 60); + durSec = round(mod(eventOnset, 60)); + evStr = sprintf('[%02d:%02d] - %s event : duration - %d sec', ... + durMin, durSec, eventSource, eventDuration); + if ~strcmp(eventSource, 'us') + evStr = sprintf('%s, type - %s', evStr, eventType); + end + if exist('eventChannel', 'var') + evStr = sprintf('%s, %s channel - %d', evStr, eventSource, ... + eventChannel); + end + evStr = [evStr '.']; + + session.UserData.d.recView.logEvent(evStr); + end end diff --git a/control/helper/myError.m b/control/helper/myError.m index e9256d9..5fecac6 100644 --- a/control/helper/myError.m +++ b/control/helper/myError.m @@ -23,6 +23,9 @@ function myError(errSrc, errMsg) %MYERROR An wrapper of the error function that logs the message before %throwing the error. + global errorOccurredDuringSession; + errorOccurredDuringSession = 1; + logger = log4m.getLogger(); logger.fatal(errSrc, errMsg); diff --git a/control/helper/playSounds.m b/control/helper/playSounds.m index 5d2080f..101aa78 100644 --- a/control/helper/playSounds.m +++ b/control/helper/playSounds.m @@ -31,6 +31,8 @@ function playSounds(session, sessionStartTime) % FIXME: The function expects sound events to be ordered. % FIXME: Primitive function, does not play partial sounds if time already % progressed passed the sound onset. + global errorOccurredDuringSession; + logger = log4m.getLogger(); logger.warn('playSounds', ['Sounds are played via sound card. ' ... 'Precise timing cannot be ensured!']); @@ -51,11 +53,34 @@ function playSounds(session, sessionStartTime) for i = 1:d.subjects{1}.numSounds() sndEvent = d.subjects{1}.getSound(i); - onsetTime = sndEvent.onset; + onsetTime = getActualOnset(sndEvent.onset, sessionStartTime, ... + session); - pause((onsetTime - eps) - toc(sessionStartTime)); + while (onsetTime - eps) > elapsedTime(sessionStartTime) + % If the actual start time is known yet. + if isfield(session.UserData, 'triggerTime') + sessionStartTime = session.UserData.triggerTime; + end + + % If the recording has been stopped by the user, we exit this + % function early. + if session.UserData.d.recView.hasRecStopped() + return; + end + + % If the session has ended due to an occurred error, we also + % return this function. + if errorOccurredDuringSession + return; + end + + pause(min(1, (onsetTime-eps) - elapsedTime(sessionStartTime))); + + onsetTime = getActualOnset(sndEvent.onset, ... + sessionStartTime, session); + end - while toc(sessionStartTime) < onsetTime + while elapsedTime(sessionStartTime) < onsetTime % busy wait end @@ -63,6 +88,102 @@ function playSounds(session, sessionStartTime) logger.info('playSounds', ['Playing sound ' num2str(i) ... ', which has the sound type ' sndEvent.type '.']); + + % Log event in GUI. + durMin = floor(sndEvent.onset / 60); + durSec = round(mod(sndEvent.onset, 60)); + evStr = sprintf(['[%02d:%02d] - sound event : duration - %d ' ... + 'sec, type - %s.'], durMin, durSec, sndEvent.duration, ... + sndEvent.type); + session.UserData.d.recView.logEvent(evStr); + end +end + +function elapsedSecs = elapsedTime(refTime) + % Compute the elapsed seconds since a reference time point. + elapsedSecs = round((now - refTime) * 24 * 60 * 60); +end + +function onset = getActualOnset(origOnset, startTime, session) + % This method uses the function "correctOnset" to correct the onset + % time. It will stall until the current pause is over. + % + % Args: + % See method "correctOnset". + % + % Returns: + % The new onset time. + onset = -1; + while onset == -1 + [onset, waitSec] = correctOnset(origOnset, startTime, session); + if onset == -1 + pause(waitSec); + end + end +end + +function [onset, waitSecs] = correctOnset(onset, startTime, session) + % CORRECTONSET Correct the onset of an event, given that pause windows + % may appear within the session. + % + % Note, this function does not consider the length of a sound or + % whether this length will interfere with a requested pause window. + % + % Args: + % - onset: The original onset of the event (acc. to design). + % - startTime: The start time of the session. + % - session: The current NIDAQ session. + % + % Returns: + % - onset: The corrected onset. This is -1, if we are currently in a + % pause window, were no tones should be played. + % - waitSecs: Usually -1, except if onset is -1. Then this number tells + % us, how long we have to wait until the end of the + % current pause window. + waitSecs = -1; + + wins = session.UserData.d.outputDataWindows; + if isempty(wins) + % We cannot correct the onset yet. Though, that should be no + % problem, as the first window is always no pause. + return; end + + elapsedSecs = elapsedTime(startTime); + numSteps = floor(elapsedSecs * session.Rate); + + startSteps = [0; wins(1:end-1, 4)]; + % In which window are we currently in? + latestWinInd = find(numSteps >= startSteps & numSteps < wins(:, 4), ... + 1, 'last'); + + % Shouldn't happen, but we don't know yet the current window. Normally, + % the window was queued to the NIDAQ long before we reach a time step + % within the window. + if isempty(latestWinInd) + logger = log4m.getLogger(); + logger.warn('playSounds', ['Could not ensure that sound onset ' ... + 'time is correct.']); + + % All steps, that are considered pauses so far. + pausedSteps = wins(end, 4) - wins(end, 5); + % Simply add all steps, that were in paused windows so far. + onset = onset + pausedSteps / session.Rate; + return; + end + + % The session is currently paused, then we wait until the end of the + % current window before we check again. + if wins(latestWinInd, 6) == 1 + onset = -1; + waitSecs = (wins(latestWinInd, 4) - numSteps) / session.Rate; + return; + end + + numSteps = numSteps - (wins(latestWinInd, 4) - wins(latestWinInd, 5)); + % Actual time, considering pause windows. + elapsedSecsActual = numSteps / session.Rate; + + onset = onset + (elapsedSecs - elapsedSecsActual); end diff --git a/control/helper/preprocessParams.m b/control/helper/preprocessParams.m index 53e9204..83ee2a6 100644 --- a/control/helper/preprocessParams.m +++ b/control/helper/preprocessParams.m @@ -349,7 +349,15 @@ assert(size(p.sdShockChannels, 1) == nr); end end + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %%% Miscellaneous Options %%% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%% + if p.useBulkMode + p.correctRecordedInputs = 0; + end + %% Return corrected parameters. params = p; end diff --git a/control/helper/resetAllChannels.m b/control/helper/resetAllChannels.m new file mode 100644 index 0000000..2f41a14 --- /dev/null +++ b/control/helper/resetAllChannels.m @@ -0,0 +1,106 @@ +% Copyright 2018 Christian Henning +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +%{ +@title :resetAllChannels.m +@author :ch +@contact :henningc@ethz.ch +@created :08/29/2018 +@version :1.0 +%} +function resetAllChannels(p) +%RESETALLCHANNELS Set all output channels to LOW. + + s = daq.createSession('ni'); + + % Input channels + % Note, in case of using digital channels, at least one analog channel + % is required. This analog channel might be found within the inputs + % only, so we have to add them to the session. + if ~isempty(p.inputChannel) + addChannels(s, p.inputChannel, p.inputDAQDeviceID, ... + p.inputIsAnalog, 0); + end + + % Trigger Channels + if ~isempty(p.triggerChannel) + addChannels(s, p.triggerChannel, p.triggerDAQDeviceID, ... + p.triggerIsAnalog); + end + + % Shock Channels + if ~isempty(p.shockChannel) + addChannels(s, p.shockChannel, p.shockDAQDeviceID, ... + p.shockIsAnalog); + end + + % Sound Channels + if ~p.useSoundCard + addChannels(s, p.soundChannel, p.soundDAQDeviceID, ... + ones(size(p.soundChannel))); + end + + % Sound Event Channels + if ~isempty(p.soundEventChannel) + addChannels(s, p.soundEventChannel, ... + p.soundEventDAQDeviceID, p.soundEventIsAnalog); + end + + % Digital Channels from Design File + if ~isempty(p.digitalChannel) + addChannels(s, p.digitalChannel, p.digitalDAQDeviceID, ... + zeros(size(p.digitalChannel))); + end + + % Analog Channels from Design File + if ~isempty(p.analogChannel) + addChannels(s, p.analogChannel, p.analogDAQDeviceID, ... + zeros(size(p.analogChannel))); + end + + numOutputChannels = length(s.Channels) - numel(p.inputChannel); + + data = zeros(1, numOutputChannels); + queueOutputData(s, data); + + startForeground(s); + + release(s); +end + +function addChannels(session, channels, deviceID, isAnalog, isOutput) +% ADDCHANNELS Add analog and digital output channels to the session. + if (~exist('isOutput', 'var')) + isOutput = 1; + end + + for i = 1:numel(channels) + if isOutput + if isAnalog(i) + session.addAnalogOutputChannel(deviceID{i}, ... + channels{i}, 'Voltage'); + else + session.addDigitalChannel(deviceID{i}, ... + channels{i}, 'OutputOnly'); + end + else + if isAnalog(i) + session.addAnalogInputChannel(deviceID{i}, ... + channels{i}, 'Voltage'); + else + session.addDigitalChannel(deviceID{i}, ... + channels{i}, 'InputOnly'); + end + end + end +end diff --git a/control/params.m b/control/params.m index 8f31fcc..b659a1f 100644 --- a/control/params.m +++ b/control/params.m @@ -67,6 +67,7 @@ For arrays (or cell arrays), it holds that each recording is a new row p.designDir = fullfile('C:', 'Users', 'USERNAME', 'experiment', ... 'design'); + %% Experiment Directory % Where should we store all the recordings; p.rootDir = fullfile('C:', 'Users', 'USERNAME', 'experiment', ... @@ -158,9 +159,9 @@ For arrays (or cell arrays), it holds that each recording is a new row % - 'lrdesign': An additional output channel determines whether the % shock is delivered to the left or right chamber % (typical active avoidance setup). I.e., we expect to - % have 2 shock channels, the first one is simply one when - % a shock is supplied, the second one is one when the - % shock should be supplied to the right chamber, left + % have 2 shock channels, the first one is simply HIGH + % when a shock is supplied, the second one is HIGH when + % the shock should be supplied to the right chamber, left % otherwise. I.e., the design field "shock.channel" % determines whether the shock is provided to the left % (shock.channel == 1) or right (shock.channel == 2). @@ -168,11 +169,13 @@ For arrays (or cell arrays), it holds that each recording is a new row % not the design decides whether the shock is supplied % left or right. Instead, an additional digital input % makes that decision (0 == left, 1 == right). + % FIXME: Not implemented! Use Shuttle Detection Box for + % this purpose. p.shockMode = 'default'; % If shocking mode is 'lrposition', then we need the index of the % digital input, that determines left or right. % TODO: Linear or 2D index? - p.shockLRInput = [3]; + p.shockLRInput = []; %% Sound Parameters % Only if p.useSoundCard == 0. @@ -206,29 +209,29 @@ For arrays (or cell arrays), it holds that each recording is a new row p.analogDAQDeviceID = {}; %% Behavior Cameras - %p.bcAdapterName = {'gentl'}; - p.bcAdapterName = {'tisimaq_r2013_64'}; + p.bcAdapterName = {'gentl'}; + %p.bcAdapterName = {'tisimaq_r2013_64'}; p.bcDeviceID = [1, 2]; - %p.bcFormat = {'Mono8'}; - p.bcFormat = {'RGB24 (752x480)'}; + p.bcFormat = {'Mono8'}; + %p.bcFormat = {'RGB24 (752x480)'}; % Cameras from different vendors might have different interfaces. % Currently, we support the following cameras: % - 'guppy': Tested with Allied Vision Guppy PRO F125B. % - 'imagingsource': Tested with ImagingSource DMK 23FV024. - p.bcCamType = {'imagingsource'}; + p.bcCamType = {'guppy'}; % It is recommended to set the camera settings with the Image % Acquisition Toolbox once and then copy-paste them here. % General Settings p.bcExposureTime = [45000]; - p.bcGain = [10.0161, 10.0161]; + p.bcGain = [3.9849]; % Region of Interest % ROI is a 4-tuple: (X-Offset, Y-Offset, Width, Height). % Each row has n*4 values, where n is the number of cameras per % recording. - p.bcROIPosition = [202, 28, 430, 430, 180, 15, 430, 430]; + p.bcROIPosition = [264, 0, 800, 800, 264, 0, 800, 800]; % File logging p.bcLoggingMode = 'disk'; @@ -255,7 +258,7 @@ For arrays (or cell arrays), it holds that each recording is a new row % external signal. p.useExtTrigger = 0; % Specify PFI channel to receive recording trigger. - p.extTriggerChannel = 'dev2/PFI1'; + p.extTriggerChannel = 'dev1/PFI1'; % Specify timeout for waiting of trigger. p.extTriggerTimeout = 60; % in seconds @@ -297,7 +300,7 @@ For arrays (or cell arrays), it holds that each recording is a new row p.sdSoundDuration = 2; % in seconds p.sdSoundFreq = [3000, 6000]; % in Hz p.sdSoundOnsets = [3, 7]; % in seconds - p.sdSoundTypes = {'CS+', 'CS-'}; + p.sdSoundTypes = {'CS+', 'CS-'}; % Shock Parameters p.sdShockDuration = 1; % in seconds @@ -305,5 +308,22 @@ For arrays (or cell arrays), it holds that each recording is a new row % Unused option, no effect yet. p.sdShockIntensities = [6e-4, 6e-4]; % in A p.sdShockChannels = [-1, -1]; % in A + + %% Miscellaneous Options + % Online correction of recorded data. + % This option only makes sense if not using the bulk mode. + % This option is only temporarily, as the algorithm hasn't been fully + % tested. + % If using the continuous mode, the user has the option to pause the + % current session. During that time, it might be desirable, that the + % written input recordings (as well as their timestamps) are paused as + % well. Therefore, we can run an online correction, that corrects the + % timestamps and ignores the input data coming from pausing frames. + % When should I disable this option? Simply speaking, either if you run + % out of resources (for instance, this option will cause the generation + % of two output files, one corresponds to the raw data in case the + % algorithm fails) or if your session fails because of this algorithm + % :) (in which case, you should report the bug). + p.correctRecordedInputs = 1; end diff --git a/control/run_experiment.m b/control/run_experiment.m index c4fdbc5..eaced66 100644 --- a/control/run_experiment.m +++ b/control/run_experiment.m @@ -33,7 +33,7 @@ p = params(); p = preprocessParams(p); -% A data container that comprises all infos need by subfunctions. +% A data container that comprises all infos needed by subfunctions. data = struct(); data.p = p; data.d = struct(); @@ -114,12 +114,30 @@ end data = checkDesignCompatibility(data); +%% Global Variables used in this Program +% This flag is set to true, if the function myError is called. +global errorOccurredDuringSession; +errorOccurredDuringSession = 0; +% This flag is set, when the user requests to pause resp. continue (if +% already pausing) the session. +global requestingPauseRespCont; +requestingPauseRespCont = 0; +% The data written in the input callback is corrected such that pauses are +% not written to the file. This is an exploratory feature for now and might +% thus fail. We need to notify the user if this is the case. +global inputDataCorrectionFailed; +inputDataCorrectionFailed = 0; + %% Configure Cameras and Preview The Screens data = init_bcams(data); data = side_detection(data); data = preview_bcams(data); %% Open live view of behavior cameras +recView = RecViewCtrl(); +data.d.recView = recView; +data = recView.openGUI(data); + data = bcam_view(data); logger.info('run_experiment', 'Starting behavior camera acquisition.'); @@ -183,8 +201,9 @@ logger.info('run_experiment', 'Session is starting now ... '); startBackground(daqSession); -% TODO, request actual time step from first input listener callback. -sessionStartTime = tic; +% This is later updated from the actual start time given to the input +% listener. +sessionStartTime = now(); if p.useExtTrigger logger.info('run_experiment', 'Waiting for external trigger ... '); @@ -206,34 +225,19 @@ maxWaitDur = maxWaitDur + p.extTriggerTimeout; end wait(daqSession, maxWaitDur); -logger.info('run_experiment', 'Session finished.'); -%% Clean up -% Clean up DAQ session -delete(daqSession.UserData.d.inputListener); -delete(daqSession.UserData.d.errorListener); -if ~p.useBulkMode - delete(daqSession.UserData.d.outputListener); +if daqSession.UserData.d.recView.hasRecStopped() + logger.warn('run_experiment', 'Session has been stopped by user.'); +else + logger.info('run_experiment', 'Session finished.'); end -release(daqSession); -% Clean up Online Side Detection. -if p.useOnlineSideDetection - stop(daqSession.UserData.d.sideDetectionTimer); - delete(daqSession.UserData.d.sideDetectionTimer); - fclose(daqSession.UserData.d.serialCommObj); -end +% Save from cleanup. +sessionInterrupted = daqSession.UserData.d.recView.hasRecStopped(); -% Clean up behavior cameras. -for i = 1:numel(p.bcDeviceID) - stop(daqSession.UserData.d.bcVidObjects{i}); -end -stop(daqSession.UserData.d.bcFigureTimer); -delete(daqSession.UserData.d.bcFigureTimer); -close(daqSession.UserData.d.bcFigure); -for i = 1:numel(p.bcDeviceID) - delete(daqSession.UserData.d.bcVidObjects{i}); -end +%% Clean up +cleanupProgram(daqSession); +release(daqSession); %% Convert binary input/output data files to mat files. % FIXME to convert the binary data into mat files we currently read it all @@ -246,7 +250,53 @@ % send to the NIDAQ. bin2matOutput(daqSession); -logger.info('run_experiment', 'Recordings finished sucessfully.'); +% Save the output windows to a file for debugging purposes and if the input +% data correction failed, such that the user could repair it. +if ~p.useBulkMode + output_windows = daqSession.UserData.d.outputDataWindows; + nidaq_rate = daqSession.Rate; + for i = 1:numRecs + filename = fullfile(daqSession.UserData.d.expDir{i}, ... + 'output_windows_debug.mat'); + save(filename, 'output_windows', 'nidaq_rate'); + end +end + +if inputDataCorrectionFailed + msgbox(['The program tries to ensure the correctness of timestamps' ... + ' and input recordings in an online fashion, such that pauses ' ... + 'are transparent to the user.' newline ... + 'This correction failed!' newline 'Therefore, use the file ' ... + '"input_data_raw.mat" instead of "input_data.mat".' newline ... + 'Note, that this file contains the raw recordings.'], ... + 'Input Data Correction Failed', 'warn'); +end + +if sessionInterrupted + % We don't know the output values, of the interrupted output channels. + % Therefore, we set them all to LOW. + logger.info('run_experiment', 'Resetting all channels to LOW.'); + resetAllChannels(p); + + logger.warn('run_experiment', 'Recording could not finish.'); + % We did not check the output data for consistency, that has to be done + % in a postprocessing step. + % Note, that the output data is always written in chunks (or completely + % if using bulk mode). The input data depends on the callback + % thresholds. The video data depends on the actual time when the user + % has stopped the session, + msgbox(['The data written in the output folder might have ' ... + 'inconsistent lengths due to the stopped recording.'], ... + 'Recording Interrupted', 'warn'); +elseif errorOccurredDuringSession + logger.info('run_experiment', ... + 'An error occurred during the recording.'); + msgbox(['An error occurred during the session. Please study the ' ... + 'logfile and output data carefully for unwanted ' ... + 'consequences.'], 'Error during Session', 'warn'); +else + logger.info('run_experiment', 'Recordings finished sucessfully.'); +end % Copy logfile to folder of remaining recordings. clear('log4m'); @@ -257,4 +307,4 @@ warning(['Could not copy logfile to folder ' ... daqSession.UserData.d.expDir{i} '.']); end -end \ No newline at end of file +end diff --git a/control/view/PauseEventData.m b/control/view/PauseEventData.m new file mode 100644 index 0000000..a0654ff --- /dev/null +++ b/control/view/PauseEventData.m @@ -0,0 +1,34 @@ +% Copyright 2018 Christian Henning +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +%{ +@title :PauseEventData.m +@author :ch +@contact :henningc@ethz.ch +@created :08/27/2018 +@version :1.0 + +Capsulates data, that the GUI can use to know whether a pause request or a +continue request was performed. +%} +classdef (ConstructOnLoad) PauseEventData < event.EventData + properties + IsPausing + end + + methods + function data = PauseEventData(isPausing) + data.IsPausing = isPausing; + end + end +end \ No newline at end of file diff --git a/control/view/RecViewCtrl.m b/control/view/RecViewCtrl.m new file mode 100644 index 0000000..ea7b0be --- /dev/null +++ b/control/view/RecViewCtrl.m @@ -0,0 +1,433 @@ +% Copyright 2018 Christian Henning +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +%{ +@title :RecViewCtrl.m +@author :ch +@contact :henningc@ethz.ch +@created :08/24/2018 +@version :1.0 + +A controller for the view presented during a recording. This should be the +interface to the view for the remaining program. +%} +classdef RecViewCtrl < handle +%RecViewCtrl The controller for the view "rec_view". +% +% Note, though no pattern implemented, this class should be considered +% Singleton, i.e., only one instance should exist at a time. +% +% Note, the actual controller is still the "rec_view.m" file. This class +% can be seen as an interface between model and controller. + + properties + % The GUI handle, that should contain the BCam views. + bCamPanel + % The handles object of the actual GUI controller. Needed to call + % GUI callbacks. + guiHandles + % The actual start time of the recording. + recStartTime + updateTimeThread % Timer thread that updates the time. + updateTimeListener % The eventlistener, that listens to + % UpdateTimeEv. + pauseListener % The eventlistener, that listens to PauseEv. + guiClbkTime % A function handle, that can be used to update the + % time displayed on the GUI. + guiClbkEnableBtns % Enable stop and pause buttons, as soon as the + % recording has started. + guiClbkAddEv % Add an event string to the event logger in the GUI. + guiClbkClose % The close callback for the GUI figure. + guiClbkPauseText % Set the text of the pause button. + guiClbkStatus % Set the status text. + recDuration % Duration of the recording. Used to stop the timer + % update. + params % The params object. + daqSession % The daq session, that controls the recording. + recStopHandle % A progress bar handle, if the recording was + % stopped. + end + events + UpdateTimeEv % Event, that signals, that the GUI should update the + % timer (that shows the current Recording time). + PauseEv % Event, that is fired if the session has paused resp. + % continued. + end + + methods (Access = public) + function obj = RecViewCtrl() + % RecViewCtrl + % Initialize attributes. + obj.recStartTime = -1; + obj.guiClbkTime = -1; + obj.guiClbkEnableBtns = -1; + obj.daqSession = -1; + obj.recStopHandle = -1; + end + + function dataObj = openGUI(obj, dataObj) + % OPENGUI Construct and open the Recording View. + obj.params = dataObj.p; + + obj.recDuration = dataObj.d.duration; + durMin = floor(obj.recDuration / 60); + durSec = round(mod(obj.recDuration, 60)); + durStr = sprintf('/ %02d:%02d', durMin, durSec); + + % Note the view expects this controller as first vararg. + rec_view(obj, durStr); + + %% Add listener, that updates the time on incoming events. + % Asynchronous events from the timer can be synchronized with + % the GUI in this way. + % Note, listener and thread are closed in the closeRequest + % function of the GUI. + obj.updateTimeListener = addlistener(obj, 'UpdateTimeEv', ... + @updateTime); + + % Setup timer thread. + timerData.ctrl = obj; + obj.updateTimeThread = timer('TimerFcn', @timerFcn,... + 'ExecutionMode', 'fixedRate', ... + 'Period', 0.5, ... + 'BusyMode', 'drop', ... + 'UserData', timerData); + start(obj.updateTimeThread); + + %% Add listener, that gets notified on handled pause requests. + % If the session received our pause (resp. continue) request, + % we get notified via this event. + obj.pauseListener = addlistener(obj, 'PauseEv', @pauseUpdate); + end + + function setBCamPanel(obj, panel) + % SETBCAMPANEL Set the uipanel handle, in which the camera previews + % should be rendered. + % + % Args: + % - panel: An uipanel handle. + + obj.bCamPanel = panel; + end + + function panel = getBCamPanel(obj) + % GETBCAMPANEL Return the uipanel handle, in which the camera + % previews should be rendered. + % + % Returns: + % The property "bCamPanel". + + panel = obj.bCamPanel; + end + + function setGUIHandles(obj, handles) + % SETGUIHANDLES Set the guiHandles attribute. + % + % Args: + % - handles: The handles object from the controller. + obj.guiHandles = handles; + end + + function setUpdateFcns(obj, timerFcn, enableBtnFcn, ... + addEventFunc, closeFcn, pauseTextFcn, modifyStatus) + % SETUPDATEFCNS Set the update functions, that can be used interact + % with the GUI. + % + % Args: + % - timerFcn: The function, that can be used to update the timer. + % - enableBtnFcn: The function called when the recording starts, to + % enable the gui buttons. + % - addEventFunc: Function, that can be called to add an event to + % the event logger. + % - closeFcn: The close callback for the GUI. + % - pauseTextFcn: A function that allows to set the label of the + % pause button. + % - modifyStatus: A function, that can be used to modify the status + % text field of the GUI. + obj.guiClbkTime = timerFcn; + obj.guiClbkEnableBtns = enableBtnFcn; + obj.guiClbkAddEv = addEventFunc; + obj.guiClbkClose = closeFcn; + obj.guiClbkPauseText = pauseTextFcn; + obj.guiClbkStatus = modifyStatus; + end + + function setRecStartTime(obj, startTime, daqSession) + % SETRECSTARTTIME Set the start time of the recordings, such that + % the GUI can display the timer. + % + % Note, this function is also used to enable GUI features, that + % should not be present, as long as the recording hasn't started + % yet. + % + % Args: + % - startTime: The recording start time. + % - daqSession: The daq session, that controls the recording. + oldRecStartTime = obj.recStartTime; + obj.recStartTime = startTime; + obj.daqSession = daqSession; + + if oldRecStartTime ~= -1 + return; + end + + logger = log4m.getLogger(); + % FIXME: Can we be sure, that this function handle is already + % set? + if ~isa(obj.guiClbkEnableBtns, 'function_handle') + logger.warn('RecViewCtrl', ... + 'Could not enable GUI buttons.'); + return + end + + try + if obj.params.useBulkMode + logger.info('RecViewCtrl', ['Pause option is not ' ... + 'available in this recording due to bulk mode.']); + obj.guiClbkEnableBtns(obj.guiHandles, 0); + else + obj.guiClbkEnableBtns(obj.guiHandles, 1); + end + catch + logger = log4m.getLogger(); + logger.error('RecViewCtrl', ... + 'Could not enable GUI buttons.'); + end + end + + function updateTime(obj, ~) + % UPDATETIME This method listens to the 'UpdateTimeEv' and updates + % the timer displayed in the view. + if ~isa(obj.guiClbkTime, 'function_handle') + return + end + + logger = log4m.getLogger(); + + if obj.recStartTime == -1 + timeStr = '--:--'; + else + elapsedSeconds = round((now - obj.recStartTime) * ... + 24 * 60 * 60); + + % Clear paused windows from timer. + if isobject(obj.daqSession) + wins = obj.daqSession.UserData.d.outputDataWindows; + numSteps = floor(elapsedSeconds * obj.daqSession.Rate); + numStepsTotal = numSteps; + + % Search for the latest window, that spans the elapsed + % time and is not paused. + startSteps = [0; wins(1:end-1, 4)]; + latestWinInd = find(numSteps >= startSteps & ... + wins(:, 6) == 0, 1, 'last'); + + if numSteps > wins(latestWinInd, 4) + % The session is currently paused and we have to + % display an earlier timepoint. + numSteps = wins(latestWinInd, 5); + else + % Subtract possibly paused windows, that already + % happened. + % Subtract all paused windows. + numSteps = numSteps - (wins(latestWinInd, 4) - ... + wins(latestWinInd, 5)); + end + + elapsedSeconds = numSteps / obj.daqSession.Rate; + + % Compute status of session (i.e., paused or not + % paused. + obj.displayStatus(numStepsTotal, elapsedSeconds); + end + + % We don't wanna further update the timer: + if elapsedSeconds > obj.recDuration + elapsedSeconds = obj.recDuration; + end + durMin = floor(elapsedSeconds / 60); + durSec = mod(elapsedSeconds, 60); + timeStr = sprintf('%02d:%02d', durMin, durSec); + end + + try + obj.guiClbkTime(obj.guiHandles, timeStr); + catch + logger.error('RecViewCtrl', 'Could not update GUI time.'); + end + end + + function logEvent(obj, eventStr) + % LOGEVENT This method, will add the given event to the event + % logger in the GUI. + % + % Args: + % - eventStr: The string, that should be appended. + try + obj.guiClbkAddEv(obj.guiHandles, eventStr); + catch + logger = log4m.getLogger(); + logger.error('RecViewCtrl', 'Could not log GUI event.'); + end + end + + function closeGUI(obj) + % CLOSEGUI This function should be called to close the GUI figure, + % instead of calling close(guiFig). + % If recording has been stopped by user. + if obj.hasRecStopped() + waitbar(1, obj.recStopHandle, 'Recording stopped.'); + pause(0.01); + close(obj.recStopHandle) + end + + try + obj.guiClbkClose(obj.guiHandles); + catch + logger = log4m.getLogger(); + logger.error('RecViewCtrl', 'Could not close the GUI.'); + end + end + + function stopRecording(obj) + % This function is called by the GUI, if the user requested a + % stop of the recording. This function will stop the daq + % session. + logger = log4m.getLogger(); + logger.warn('RecViewCtrl', ... + 'Recording stop has been requested.'); + + if obj.params.useSoundCard + logger.warn('RecViewCtrl', ['Note, data already ' ... + 'flushed to the sound card cannot be stopped.']); + end + + waitbar_handle = waitbar(0, 'Recording will be stopped ...'); + pause(0.01); + waitbar(.33, waitbar_handle, 'Stopping DAQ Session ...'); + + logger.info('RecViewCtrl', 'Interrupting DAQ session.'); + obj.daqSession.stop(); + + waitbar(.66, waitbar_handle, 'Cleaning up workspace ...'); + obj.recStopHandle = waitbar_handle; + end + + function isStopped = hasRecStopped(obj) + % HASRECSTOPPED Has the recording been stopped by the user? + % + % Returns: + % Whether the recording has been stopped. + isStopped = ishandle(obj.recStopHandle); + end + + function pauseRequested(obj, btnLabel) + % PAUSEREQUESTED This method handles the case when the user + % requests a pause or continue of the session. + % + % Args: + % - btnLabel: The current button label, either 'Pause' or + % 'Continue'. + global requestingPauseRespCont; + requestingPauseRespCont = 1; + + logger = log4m.getLogger(); + if strcmp(btnLabel, 'Pause') + logger.error('RecViewCtrl', 'User requested pause.'); + else + logger.error('RecViewCtrl', 'User requested continue.'); + end + end + + function pauseUpdate(obj, eventData) + % PAUSEUPDATE This function gets called when the PauseEv event + % is fired. Hence, it is called when a pause/continue request + % was processed, such that we can enable the button again. + logger = log4m.getLogger(); + + if eventData.IsPausing + try + obj.guiClbkPauseText(obj.guiHandles, 'Continue'); + catch + logger.error('RecViewCtrl', ... + 'Could not set pause button text.'); + end + else + try + obj.guiClbkPauseText(obj.guiHandles, 'Pause'); + catch + logger.error('RecViewCtrl', ... + 'Could not set pause button text.'); + end + end + end + end + + methods (Access = private) + function displayStatus(obj, numSteps, elapsedRecSecs) + % DISPLAYSTATUS Compute the status of the session and display + % it to the user. + % + % Args: + % - numSteps: The number of steps processed so far in this + % recording (the total number, including paused + % steps). + % - elapsedRecSecs: The number of seconds elapsed in this + % recording (excluding pauses) so far. + try + wins = obj.daqSession.UserData.d.outputDataWindows; + + % Search for the latest window, that spans the elapsed + % time and is not paused. + startSteps = [0; wins(1:end-1, 4)]; + + if obj.hasRecStopped() + obj.guiClbkStatus(obj.guiHandles, ... + 'Session Interrupted', [0.85 0.33 0.1]); + return; + end + + if ~obj.daqSession.IsRunning + obj.guiClbkStatus(obj.guiHandles, [], [], false); + else + if elapsedRecSecs >= obj.recDuration + obj.guiClbkStatus(obj.guiHandles, ... + 'Ending Session', [0.5 0.5 0.5]); + return; + end + + % In which window are we currently in? + winInd = find(numSteps >= startSteps & ... + numSteps < wins(:, 4), 1, 'last'); + + if wins(winInd, 6) == 1 + obj.guiClbkStatus(obj.guiHandles, ... + 'Session Paused', [1 0 0]); + else + obj.guiClbkStatus(obj.guiHandles, ... + 'Session Running', [0 1 0]); + end + end + catch + logger.error('RecViewCtrl', ... + 'Could not set session status in GUI.'); + end + end + end +end + +function timerFcn(timerObj, ~) + % Send an event to the model, that it should update the timer. + data = get(timerObj, 'UserData'); + notify(data.ctrl, 'UpdateTimeEv'); +end \ No newline at end of file diff --git a/control/view/rec_view.fig b/control/view/rec_view.fig new file mode 100644 index 0000000000000000000000000000000000000000..99e6bcf5c7b0e080250719283b99b56913acd7a4 GIT binary patch literal 11448 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 N9|sFfJLtF8{{z>VD_8&k literal 0 HcmV?d00001 diff --git a/control/view/rec_view.m b/control/view/rec_view.m new file mode 100644 index 0000000..7cb2656 --- /dev/null +++ b/control/view/rec_view.m @@ -0,0 +1,286 @@ +function varargout = rec_view(varargin) +% REC_VIEW MATLAB code for rec_view.fig +% REC_VIEW, by itself, creates a new REC_VIEW or raises the existing +% singleton*. +% +% H = REC_VIEW returns the handle to a new REC_VIEW or the handle to +% the existing singleton*. +% +% REC_VIEW('CALLBACK',hObject,eventData,handles,...) calls the local +% function named CALLBACK in REC_VIEW.M with the given input arguments. +% +% REC_VIEW('Property','Value',...) creates a new REC_VIEW or raises the +% existing singleton*. Starting from the left, property value pairs are +% applied to the GUI before rec_view_OpeningFcn gets called. An +% unrecognized property name or invalid value makes property application +% stop. All inputs are passed to rec_view_OpeningFcn via varargin. +% +% *See GUI Options on GUIDE's Tools menu. Choose "GUI allows only one +% instance to run (singleton)". +% +% See also: GUIDE, GUIDATA, GUIHANDLES + + % Edit the above text to modify the response to help rec_view + + % Last Modified by GUIDE v2.5 27-Aug-2018 10:47:19 + + % Begin initialization code - DO NOT EDIT + gui_Singleton = 1; + gui_State = struct('gui_Name', mfilename, ... + 'gui_Singleton', gui_Singleton, ... + 'gui_OpeningFcn', @rec_view_OpeningFcn, ... + 'gui_OutputFcn', @rec_view_OutputFcn, ... + 'gui_LayoutFcn', [] , ... + 'gui_Callback', []); + if nargin && ischar(varargin{1}) + gui_State.gui_Callback = str2func(varargin{1}); + end + + if nargout + [varargout{1:nargout}] = gui_mainfcn(gui_State, varargin{:}); + else + gui_mainfcn(gui_State, varargin{:}); + end + % End initialization code - DO NOT EDIT +end + +% --- Executes just before rec_view is made visible. +function rec_view_OpeningFcn(hObject, eventdata, handles, varargin) +% This function has no output args, see OutputFcn. +% hObject handle to figure +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +% varargin command line arguments to rec_view (see VARARGIN) + + assert(nargin == 5) + handles.ctrl = varargin{1}; + + % Choose default command line output for rec_view + handles.output = hObject; + + % Update handles structure + guidata(hObject, handles); + + % UIWAIT makes rec_view wait for user response (see UIRESUME) + % uiwait(handles.figureMain); + + set(handles.textDuration, 'String', varargin{2}); + + % TODO figure not yet created, but we want to maximize it. Look at + % FileExchange for solutions. + + % Important, within GUIDE, I can only place axes. But I can't put + % subplots into an axes. That is why we use an uipanel instead. + bCamHandle = uipanel('Parent', hObject, 'Title', ... + 'Behavior Camera Live View', 'FontSize', 12, 'Position', ... + [0.02, 0.125, 0.75, 0.75]); + handles.bcamViewHandle = bCamHandle; + handles.ctrl.setBCamPanel(bCamHandle); + handles.ctrl.setGUIHandles(handles); + handles.ctrl.setUpdateFcns(@update_rec_timer, @enable_buttons, ... + @add_event_line, @my_close_fcn, @set_pause_btn_lbl, @set_status); +end + +% --- Outputs from this function are returned to the command line. +function varargout = rec_view_OutputFcn(hObject, eventdata, handles) +% varargout cell array for returning output args (see VARARGOUT); +% hObject handle to figure +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + +% Get default command line output from handles structure + varargout{1} = handles.output; +end + +% --- Executes on button press in btnPause. +function btnPause_Callback(hObject, eventdata, handles) +% hObject handle to btnPause (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + + btnText = get(handles.btnPause, 'String'); + if strcmp(btnText, 'Pause') + set(handles.btnPause, 'String', 'Pausing ...'); + set(handles.btnPause, 'Enable','off') + elseif strcmp(btnText, 'Continue') + set(handles.btnPause, 'String', 'Continuing ...'); + set(handles.btnPause, 'Enable','off') + else + % This point cannot be reached, as the button is disabled. + return; + end + + handles.ctrl.pauseRequested(btnText); +end + + +% --- Executes on button press in btnStop. +function btnStop_Callback(hObject, eventdata, handles) +% hObject handle to btnStop (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + msg = ['When stopping the recording, it cannot be resumed!' newline ... + 'Do you want to continue?']; + btnString = buttondlg(msg, 'Warning', 'Yes', 'No', ... + struct('Default','No','IconString','warn')); + + if strcmp(btnString, 'No') + return; + end + + handles.ctrl.stopRecording(); + set(handles.btnStop, 'Enable','off') +end + + +function editEventLogger_Callback(hObject, eventdata, handles) +% hObject handle to editEventLogger (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + +% Hints: get(hObject,'String') returns contents of editEventLogger as text +% str2double(get(hObject,'String')) returns contents of editEventLogger as a double + +end + +% --- Executes during object creation, after setting all properties. +function editEventLogger_CreateFcn(hObject, eventdata, handles) +% hObject handle to editEventLogger (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles empty - handles not created until after all CreateFcns called + +% Hint: edit controls usually have a white background on Windows. +% See ISPC and COMPUTER. + if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) + set(hObject,'BackgroundColor','white'); + end +end + + +% --- Executes when user attempts to close figureMain. +function figureMain_CloseRequestFcn(hObject, eventdata, handles) +% hObject handle to figureMain (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + + msg = ['The GUI should not be closed while the recording is ' ... + 'running.' newline 'Do you want to close it anyway?']; + btnString = buttondlg(msg, 'Warning', 'Yes', 'No', ... + struct('Default','No','IconString','warn')); + if strcmp(btnString, 'No') + return; + end + + % Close GUI. + my_close_fcn(handles); + + +end + +function update_rec_timer(handles, timeStr) +% Update the timer string on the GUI. +% +% Args: +% - handles: Structure with handles and user data (see GUIDATA). +% - timeStr: The String, that should be displayed (containing the time). + set(handles.textTime, 'String', timeStr); +end + +function enable_buttons(handles, enablePause) +% The user should not press any buttons, as long as the recording hasn't +% started yet (as the program state might be undefined in this moment). So +% we let the controller enable these buttons, as soon the recording +% started. +% +% Args: +% - handles: Structure with handles and user data (see GUIDATA). +% - enablePause: Whether the pause button should be enabled. + if enablePause + set(handles.btnPause, 'Enable', 'on'); + else + set(handles.btnPause, 'Enable', 'off'); + end + set(handles.btnStop, 'Enable', 'on'); +end + +function add_event_line(handles, eventLine) +% Add a line of text to the event logger view. +% +% Args: +% - handles: Structure with handles and user data (see GUIDATA). +% - eventLine: The line that should be appended to the logger. + currEvText = get(handles.editEventLogger, 'String'); + currEvText{end+1} = eventLine; + set(handles.editEventLogger, 'String', currEvText); + + % Quick fix taken from here: + % undocumentedmatlab.com/blog/setting-line-position-in-edit-box-uicontrol/ + % This fix should help us, to ensure, that the scrollbar is always set + % to the end of the edit field. + javaHandle = findjobj(handles.editEventLogger); + jEdit = javaHandle.getComponent(0).getComponent(0); + jEdit.setCaretPosition(jEdit.getDocument.getLength); +end + +function my_close_fcn(handles) +% We need to distinguish the cases when the user wants to close the figure +% (by pressing the close button) or when we want to close the GUI at the +% end of the recording. That's why this extra function exists in addition +% to "figureMain_CloseRequestFcn". +% +% Args: +% - handles: Structure with handles and user data (see GUIDATA). + hObject = handles.figureMain; + + if strcmp(get(handles.ctrl.updateTimeThread, 'Running'), 'on') + stop(handles.ctrl.updateTimeThread); + end + + % Make sure, notifications from the timer are no longer executed. + delete(handles.ctrl.updateTimeListener); + + delete(handles.ctrl.pauseListener); + + % Hint: delete(hObject) closes the figure + % Here we use a heuristic, to avoid errors on close. The GUI might be + % stuck in the update function, while this close event arives + % asynchronely. + % Therefore, we allow after the execution of this function, the update + % function to finish before we finally delete the GUI. + deleteTimer = timer('StartDelay', 0.3, 'TimerFcn', ... + @(src,evt)delete(hObject)); + start(deleteTimer); + %delete(hObject); +end + +function set_pause_btn_lbl(handles, label) +% Set a new label to the pause button. This function will enable the +% button. +% +% Args: +% - handles: Structure with handles and user data (see GUIDATA). +% - label: The text on the button. + set(handles.btnPause, 'String', label); + set(handles.btnPause, 'Enable', 'on') +end + +function set_status(handles, label, color, visible) +% Modify the status label. +% +% Args: +% - handles: Structure with handles and user data (see GUIDATA). +% - label: The text to display. +% - color: The color of the text to display. +% - visible: Whether the text should be visible or not. + if (~exist('visible', 'var')) + visible = true; + end + + if visible + set(handles.textStatus, 'String', label) + set(handles.textStatus, 'ForegroundColor', color) + set(handles.textStatus, 'Visible', 'on') + else + set(handles.textStatus, 'Visible', 'off') + end +end diff --git a/docs/imgs/recording_view_screenshot.PNG b/docs/imgs/recording_view_screenshot.PNG new file mode 100755 index 0000000000000000000000000000000000000000..76209c8474271792e6dc37467e79b904b38c91f5 GIT binary patch literal 76579 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