diff --git a/Resources/Python/record_control_example_client.py b/Resources/Python/record_control_example_client.py new file mode 100644 index 0000000000000000000000000000000000000000..9b4c0f899366ac26378629073787faf58c076883 --- /dev/null +++ b/Resources/Python/record_control_example_client.py @@ -0,0 +1,69 @@ +""" + A zmq client to test remote control of open-ephys GUI +""" + +import zmq +import os +import time + + +def run_client(): + + # Basic start/stop commands + start_cmd = 'StartRecord' + stop_cmd = 'StopRecord' + + # Example settings + rec_dir = os.path.join(os.getcwd(), 'Output_RecordControl') + print "Saving data to:", rec_dir + + # Some commands + commands = [start_cmd + ' RecDir=%s' % rec_dir, + start_cmd + ' PrependText=Session01 AppendText=Condition01', + start_cmd + ' PrependText=Session01 AppendText=Condition02', + start_cmd + ' PrependText=Session02 AppendText=Condition01', + start_cmd, + start_cmd + ' CreateNewDir=1'] + + # Connect network handler + ip = '127.0.0.1' + port = 5556 + timeout = 1. + + url = "tcp://%s:%d" % (ip, port) + + with zmq.Context() as context: + with context.socket(zmq.REQ) as socket: + socket.RCVTIMEO = int(timeout * 1000) # timeout in milliseconds + socket.connect(url) + + # Finally, start data acquisition + socket.send('StartAcquisition') + answer = socket.recv() + print answer + time.sleep(5) + + for start_cmd in commands: + + for cmd in [start_cmd, stop_cmd]: + socket.send(cmd) + answer = socket.recv() + print answer + + if 'StartRecord' in cmd: + # Record data for 5 seconds + time.sleep(5) + else: + # Stop for 1 second + time.sleep(1) + + # Finally, stop data acquisition; it might be a good idea to + # wait a little bit until all data have been written to hard drive + time.sleep(0.5) + socket.send('StopAcquisition') + answer = socket.recv() + print answer + + +if __name__ == '__main__': + run_client() diff --git a/Source/CoreServices.cpp b/Source/CoreServices.cpp index 1f4412f6fb2104eb4be746327661ce4ed7f6a8da..7e4d1325382fb7e70a9340cb01c51bd299f8d3d1 100644 --- a/Source/CoreServices.cpp +++ b/Source/CoreServices.cpp @@ -52,6 +52,16 @@ void setRecordingStatus(bool enable) getControlPanel()->setRecordState(enable); } +bool getAcquisitionStatus() +{ + return getControlPanel()->getAcquisitionState(); +} + +void setAcquisitionStatus(bool enable) +{ + getControlPanel()->setAcquisitionState(enable); +} + void sendStatusMessage(const String& text) { getBroadcaster()->sendActionMessage(text); @@ -77,6 +87,26 @@ int64 getSoftwareTimestamp() return getMessageCenter()->getTimestamp(true); } +void setRecordingDirectory(String dir) +{ + getControlPanel()->setRecordingDirectory(dir); +} + +void createNewRecordingDir() +{ + getControlPanel()->labelTextChanged(NULL); +} + +void setPrependTextToRecordingDir(String text) +{ + getControlPanel()->setPrependText(text); +} + +void setAppendTextToRecordingDir(String text) +{ + getControlPanel()->setAppendText(text); +} + namespace RecordNode { void createNewrecordingDir() @@ -105,4 +135,4 @@ int addSpikeElectrode(SpikeRecordInfo* elec) } }; -}; \ No newline at end of file +}; diff --git a/Source/CoreServices.h b/Source/CoreServices.h index f319882333879d7a6316640a00b07209595c33fe..8e47876300a0039328318ce369db3de948bbd19e 100644 --- a/Source/CoreServices.h +++ b/Source/CoreServices.h @@ -42,6 +42,12 @@ bool getRecordingStatus(); /** Activated or deactivates recording */ void setRecordingStatus(bool enable); +/** Returns true if the GUI is acquiring data */ +bool getAcquisitionStatus(); + +/** Activates or deactivates data acquisition */ +void setAcquisitionStatus(bool enable); + /** Sends a string to the message bar */ void sendStatusMessage(const String& text); @@ -59,6 +65,18 @@ int64 getGlobalTimestamp(); /** Gets the software timestamp based on a high resolution timer aligned to the start of each processing block */ int64 getSoftwareTimestamp(); +/** Set new recording directory */ +void setRecordingDirectory(String dir); + +/** Create new recording directory */ +void createNewRecordingDir(); + +/** Manually set the text to be prepended to the recording directory */ +void setPrependTextToRecordingDir(String text); + +/** Manually set the text to be appended to the recording directory */ +void setAppendTextToRecordingDir(String text); + namespace RecordNode { /** Forces creation of new directory on recording */ diff --git a/Source/Processors/NetworkEvents/NetworkEvents.cpp b/Source/Processors/NetworkEvents/NetworkEvents.cpp index 3b8fefc97ac0ff9043c6c22ef2431eaec4c19c25..3f444cdca27cc21d465f565bf4c50a609692f073 100644 --- a/Source/Processors/NetworkEvents/NetworkEvents.cpp +++ b/Source/Processors/NetworkEvents/NetworkEvents.cpp @@ -405,7 +405,31 @@ String NetworkEvents::handleSpecialMessages(StringTS msg) } */ - return String("NotHandled"); + + /** Start/stop data acquisition */ + String s = msg.getString(); + + const MessageManagerLock mmLock; + if (s.compareIgnoreCase("StartAcquisition") == 0) + { + if (!CoreServices::getAcquisitionStatus()) + { + CoreServices::setAcquisitionStatus(true); + } + return String("StartedAcquisition"); + } + else if (s.compareIgnoreCase("StopAcquisition") == 0) + { + if (CoreServices::getAcquisitionStatus()) + { + CoreServices::setAcquisitionStatus(false); + } + return String("StoppedAcquisition"); + } + else + { + return String("NotHandled"); + } } void NetworkEvents::process(AudioSampleBuffer& buffer, @@ -565,4 +589,4 @@ void NetworkEvents::createZmqContext() if (zmqcontext == nullptr) zmqcontext = zmq_ctx_new(); //<-- this is only available in version 3+ #endif -} \ No newline at end of file +} diff --git a/Source/Processors/RecordControl/RecordControl.cpp b/Source/Processors/RecordControl/RecordControl.cpp index f32f9cb18d251be8a8cd5e897fea3cb4c1724a65..65a08a02c5e7c43ae60d8cfae57882c8c73d2d4a 100644 --- a/Source/Processors/RecordControl/RecordControl.cpp +++ b/Source/Processors/RecordControl/RecordControl.cpp @@ -110,8 +110,124 @@ void RecordControl::handleEvent(int eventType, MidiMessage& event, int) { CoreServices::setRecordingStatus(!CoreServices::getRecordingStatus()); } + } + else if (eventType == MESSAGE) + { + handleNetworkEvent(event); + } + +} + +void RecordControl::handleNetworkEvent(MidiMessage& event) +{ + /** Extract network message from midi event */ + const uint8* dataptr = event.getRawData(); + int bufferSize = event.getRawDataSize(); + int len = bufferSize - 6; // 6 for initial event prefix + String msg = String((const char*)(dataptr + 6), len); + + /** Command is first substring */ + StringArray inputs = StringArray::fromTokens(msg, " "); + String cmd = String(inputs[0]); + const MessageManagerLock mmLock; + if (String("StartRecord").compareIgnoreCase(cmd) == 0) + { + if (!CoreServices::getRecordingStatus()) + { + /** First set optional parameters (name/value pairs)*/ + if (msg.contains("=")) + { + String s = msg.substring(cmd.length()); + StringPairArray dict = parseNetworkMessage(s); + + StringArray keys = dict.getAllKeys(); + for (int i=0; i<keys.size(); i++) + { + String key = keys[i]; + String value = dict[key]; + + if (key.compareIgnoreCase("CreateNewDir") == 0) + { + if (value.compareIgnoreCase("1") == 0) + { + CoreServices::createNewRecordingDir(); + } + } + else if (key.compareIgnoreCase("RecDir") == 0) + { + CoreServices::setRecordingDirectory(value); + } + else if (key.compareIgnoreCase("PrependText") == 0) + { + CoreServices::setPrependTextToRecordingDir(value); + } + else if (key.compareIgnoreCase("AppendText") == 0) + { + CoreServices::setAppendTextToRecordingDir(value); + } + } + } + + /** Start recording */ + CoreServices::setRecordingStatus(true); + } } + else if (String("StopRecord").compareIgnoreCase(cmd) == 0) + { + if (CoreServices::getRecordingStatus()) + { + CoreServices::setRecordingStatus(false); + } + } +} + +StringPairArray RecordControl::parseNetworkMessage(String msg) +{ + StringArray splitted; + splitted.addTokens(msg, "=", ""); + + StringPairArray dict = StringPairArray(); + String key = ""; + String value = ""; + for (int i=0; i<splitted.size()-1; i++) + { + String s1 = splitted[i]; + String s2 = splitted[i+1]; + + /** Get key */ + if (!key.isEmpty()) + { + if (s1.contains(" ")) + { + int i1 = s1.lastIndexOf(" "); + key = s1.substring(i1+1); + } + else + { + key = s1; + } + } + else + { + key = s1.trim(); + } + + /** Get value */ + if (i < splitted.size() - 2) + { + int i1 = s2.lastIndexOf(" "); + value = s2.substring(0, i1); + } + else + { + value = s2; + } + + dict.set(key, value); + } + + return dict; +} -} \ No newline at end of file diff --git a/Source/Processors/RecordControl/RecordControl.h b/Source/Processors/RecordControl/RecordControl.h index b813d8cee5ecb8f82f59e7ff7e031cf3ffece4c7..b43470dcc4f4ca8b1b651471be0de88803401c31 100644 --- a/Source/Processors/RecordControl/RecordControl.h +++ b/Source/Processors/RecordControl/RecordControl.h @@ -26,8 +26,8 @@ #include "../../../JuceLibraryCode/JuceHeader.h" #include "../GenericProcessor/GenericProcessor.h" +#include "../NetworkEvents/NetworkEvents.h" #include "RecordControlEditor.h" -#include "../RecordNode/RecordNode.h" /** @@ -47,6 +47,11 @@ public: void setParameter(int, float); void updateTriggerChannel(int newChannel); void handleEvent(int eventType, MidiMessage& event, int); + void handleNetworkEvent(MidiMessage& event); + + //* Split network message into name/value pairs (name1=val1 name2=val2 etc) */ + StringPairArray parseNetworkMessage(String msg); + bool enable(); bool isUtility() @@ -67,4 +72,4 @@ private: }; -#endif \ No newline at end of file +#endif diff --git a/Source/UI/ControlPanel.cpp b/Source/UI/ControlPanel.cpp index 56a776cdf15e7f920875d5b55b1721560c3dbca9..4cc8b1e1178131270e7c39f0fe061bf880e44124 100755 --- a/Source/UI/ControlPanel.cpp +++ b/Source/UI/ControlPanel.cpp @@ -489,6 +489,33 @@ void ControlPanel::setRecordState(bool t) } +bool ControlPanel::getRecordingState() +{ + + return recordButton->getToggleState(); + +} + +void ControlPanel::setRecordingDirectory(String path) +{ + File newFile(path); + filenameComponent->setCurrentFile(newFile, true, sendNotificationSync); + + graph->getRecordNode()->newDirectoryNeeded = true; + masterClock->resetRecordTime(); +} + +bool ControlPanel::getAcquisitionState() +{ + return playButton->getToggleState(); +} + +void ControlPanel::setAcquisitionState(bool state) +{ + playButton->setToggleState(state, sendNotification); +} + + void ControlPanel::updateChildComponents() { @@ -972,6 +999,16 @@ String ControlPanel::getTextToPrepend() } } +void ControlPanel::setPrependText(String t) +{ + prependText->setText(t, sendNotificationSync); +} + +void ControlPanel::setAppendText(String t) +{ + appendText->setText(t, sendNotificationSync); +} + void ControlPanel::setDateText(String t) { dateText->setText(t, dontSendNotification); @@ -1048,4 +1085,4 @@ StringArray ControlPanel::getRecentlyUsedFilenames() void ControlPanel::setRecentlyUsedFilenames(const StringArray& filenames) { filenameComponent->setRecentlyUsedFilenames(filenames); -} \ No newline at end of file +} diff --git a/Source/UI/ControlPanel.h b/Source/UI/ControlPanel.h index 22ecafaeb5b546e6f043ec0fd68cf01172c4035b..69581084126022e71c85e48faedd161bcb27cf5d 100755 --- a/Source/UI/ControlPanel.h +++ b/Source/UI/ControlPanel.h @@ -301,6 +301,19 @@ public: /** Used to manually turn recording on and off.*/ void setRecordState(bool isRecording); + + /** Return current recording state.*/ + bool getRecordingState(); + + /** Set recording directory and update FilenameComponent */ + void setRecordingDirectory(String path); + + /** Return current acquisition state.*/ + bool getAcquisitionState(); + + /** Used to manually turn recording on and off.*/ + void setAcquisitionState(bool state); + /** Returns a boolean that indicates whether or not the FilenameComponet is visible. */ bool isOpen() @@ -317,6 +330,12 @@ public: /** Used by RecordNode to set the filename. */ String getTextToAppend(); + /** Manually set the text to be prepended to the recording directory */ + void setPrependText(String text); + + /** Manually set the text to be appended to the recording directory */ + void setAppendText(String text); + /** Set date text. */ void setDateText(String);