Miniscope-DAQ-QT-Software icon indicating copy to clipboard operation
Miniscope-DAQ-QT-Software copied to clipboard

Software synchronize with other DAQ

Open chenxinfeng4 opened this issue 2 years ago • 3 comments

Great job for your miniscope development. I have other customized behavior recording system that can be triggered by both keyboard shorcut and python/websocket. How to synchronize this behavior recording system with the Miniscope DAQ software. I knew Miniscope DAQ provides hardware triggers, how about software trigger (even small latency is tolerable).

chenxinfeng4 avatar Dec 26 '22 06:12 chenxinfeng4

I found the way out. I've implemented python/websocket in my own QT DAQ SOFTWARE (ArControl) that I can you python to start/stop a QT GUI recording button. Here I transform that code into your QT DAQ SOFTWARE (Miniscope).

1. Create tcpserver.h and tcpserver.cpp.

//tcpserver.h
#ifndef TCPSERVER_H
#define TCPSERVER_H

#include <QObject>
#include <QThread>
#include <QTcpSocket>
#include <QTcpServer>
#include <QDebug>
#include "controlpanel.h"


class TcpServer : public QTcpServer
{
    Q_OBJECT
public:
    explicit TcpServer(ControlPanel *,QObject *parent = 0);
    bool tcpcmd_start_record(char *outmsg);
    bool tcpcmd_stop_record(char *outmsg);
    int tcpcmd_query_record(char *outmsg);

signals:
    void tell_socket_port(QString port);
    void recordButtonClick();
    void stopButtonClick();

private:
    ControlPanel *controlPanel;

public slots:
    void on_socket_activate(bool activate);

protected:
    void incomingConnection(qintptr socketDescriptor);
};


namespace TCPSERVER_PRIVATE{
    class MyThread : public QThread
    {
        Q_OBJECT
    public:
        explicit MyThread(qintptr ID, TcpServer *tcpserver, QObject *parent = 0);
        void run();

    signals:
        void error(QTcpSocket::SocketError socketerror);

    public slots:
        void readyRead();
        void disconnected();

    private:
        TcpServer *tcpServer;
        QTcpSocket *socket;
        qintptr socketDescriptor;
    };
}

#endif // TCPSERVER_H
//tcpserver.cpp
#include <QQuickItem>
#include "tcpserver.h"

using namespace TCPSERVER_PRIVATE;

TcpServer::TcpServer(ControlPanel *ctl, QObject *parent):
    QTcpServer(parent), controlPanel(ctl)
{
    connect(this, SIGNAL(recordButtonClick()),
            controlPanel->rootObject->findChild<QQuickItem*>("bRecord"),
            SIGNAL(activated()));
    connect(this, SIGNAL(stopButtonClick()),
            controlPanel->rootObject->findChild<QQuickItem*>("bStop"),
            SIGNAL(activated()));
    connect(this, SIGNAL(tell_socket_port(QString)),
            controlPanel,
            SLOT(receiveMessage(QString)));
}


void TcpServer::on_socket_activate(bool activate)
{
    if(activate){
        quint16 port = 20172;
        if(this->listen(QHostAddress::Any, port) || this->listen(QHostAddress::Any))
        {
            qDebug() << "Server started!" << this->serverPort();
            emit tell_socket_port(QString("Connect socket to PORT [%1].").arg(this->serverPort()));
        }
        else
        {
            qDebug() << "Server could not start";
            emit tell_socket_port(QString("Warning: connect socket failed"));
        }
    }
    else{
        this->close();
    }
}

// This function is called by QTcpServer when a new connection is available.
void TcpServer::incomingConnection(qintptr socketDescriptor)
{
    // We have a new connection
    qDebug() << socketDescriptor << " Connecting...";

    // Every new connection will be run in a newly created thread
    MyThread *thread = new MyThread(socketDescriptor, this);

    // connect signal/slot
    // once a thread is not needed, it will be beleted later
    connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();
}

bool TcpServer::tcpcmd_start_record(char *outmsg)
{
    bool currentstuts = controlPanel->rootObject->property("recording").toBool();
    if(currentstuts){
        strcpy(outmsg, "Error! Already started before.");
        return false;
    }
    bool btn_enabled = true;
    if(!btn_enabled){
        strcpy(outmsg, "Error! Device not connected.");
        return false;
    }
    emit recordButtonClick();
    strcpy(outmsg, "OK! Task should be starting.");
    return true;
}

bool TcpServer::tcpcmd_stop_record(char *outmsg)
{
    bool currentstuts = controlPanel->rootObject->property("recording").toBool();
    if(!currentstuts){
        strcpy(outmsg, "Error! Already stopped before.");
        return false;
    }
    bool btn_enabled = true;
    if(!btn_enabled){
        strcpy(outmsg, "Error! Device not connected.");
        return false;
    }
    emit stopButtonClick();
    strcpy(outmsg, "OK! Task should be stopping.");
    return true;
}

int TcpServer::tcpcmd_query_record(char *outmsg)
{
    bool btn_enabled = true;
    if(!btn_enabled){
        strcpy(outmsg, "Device not connected.");
        return 0;
    }
    bool currentstuts = controlPanel->rootObject->property("recording").toBool();
    if(currentstuts){
        strcpy(outmsg, "Running.");
        return 2;
    }
    else{
        strcpy(outmsg, "Stopped.");
        return 1;
    }
}

MyThread::MyThread(qintptr ID, TcpServer *tcpServer, QObject *parent) :
    QThread(parent)
{
    this->socketDescriptor = ID;
    this->tcpServer = tcpServer;
}

void MyThread::run()
{
    // thread starts here
    qDebug() << " Thread started";

    socket = new QTcpSocket();

    // set the ID
    if(!socket->setSocketDescriptor(this->socketDescriptor))
    {
        // something's wrong, we just emit a signal
        emit error(socket->error());
        return;
    }

    // connect socket and signal
    // note - Qt::DirectConnection is used because it's multithreaded
    //        This makes the slot to be invoked immediately, when the signal is emitted.

    connect(socket, SIGNAL(readyRead()), this, SLOT(readyRead()), Qt::DirectConnection);
    connect(socket, SIGNAL(disconnected()), this, SLOT(disconnected()));

    // We'll have multiple clients, we want to know which is which
    qDebug() << socketDescriptor << " Client connected";

    // make this thread a loop,
    // thread will stay alive so that signal/slot to function properly
    // not dropped out in the middle when thread dies
    exec();
}

void MyThread::disconnected()
{
    qDebug() << socketDescriptor << " Disconnected";
    socket->deleteLater();
    exit(0);
}

void MyThread::readyRead()
{
    const int MaxLength = 1024;
    char buffer[MaxLength+1];
    qint64 byteCount = socket->read(buffer, MaxLength);
    buffer[byteCount] = 0;
    qDebug() << socket->bytesAvailable() << buffer;

    char response[MaxLength+1];
    if(strcmp(buffer, "start_record")==0)
    {
        tcpServer->tcpcmd_start_record(response);
    }
    else if(strcmp(buffer, "stop_record")==0)
    {
        tcpServer->tcpcmd_stop_record(response);
    }
    else if(strcmp(buffer, "query_record")==0)
    {
        tcpServer->tcpcmd_query_record(response);
    }
    else
    {
        strcpy(response, "Error! Not a valid command.");
    }
    socket->write(response);
    socket->flush();
    socket->waitForBytesWritten(100);
}

2. Modifiy the controlpanel.h

//controlpanel.h
class ControlPanel : public QObject
{
    Q_OBJECT
public:
    ...
    QObject *rootObject;   //change it from private to public
    ...

3. Modify the backend.cpp

//backend.cpp
#include "tcpserver.h"

void backEnd::constructUserConfigGUI()
{
    controlPanel = new ControlPanel(this, m_userConfig);
    QObject::connect(this, SIGNAL (sendMessage(QString) ), controlPanel, SLOT( receiveMessage(QString)));

    TcpServer *tcpServer = new TcpServer(controlPanel, this);
    tcpServer->on_socket_activate(true);
}

4(Optional). Modify the Miniscope-DAQ-QT-Software.pro. Add network keyword.

QT += qml quick widgets network

5. Compile and run.

You can see a web socket tcp server is running on port [20172]. image

6. Use python3 to start / stop Control Pannel.

I can also use python3 to synchronize other DAQ system.

# %%
import socket
import time
# %% create connection
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serve_ip = 'localhost'
serve_port = 20172       #default ArControl Recorder Socket PORT
tcp_socket.connect((serve_ip, serve_port))


def send_read(send_data):
    send_data_byte = send_data.encode("utf-8")
    tcp_socket.send(send_data_byte)

    from_server_msg = tcp_socket.recv(1024)
    print(from_server_msg.decode("utf-8"))

# %% Supported commands
cmds = ['query_record', 'start_record', 'stop_record']

for send_data in cmds:
    send_read(send_data)
    time.sleep(5)

image Further, I can bind global-hotkey to trigger python>start_record(F2) and python>stop_record(F4). Both socket and hotkey is available now.

If you like this idea, I would open a pull request. However, I only test based on V1.02, not the latest version (I fail to setup the QT>python|numpy link).

chenxinfeng4 avatar Dec 28 '22 13:12 chenxinfeng4

OK this rocks, sorry nobody responded earlier. We'll be moving this functionality into a package that decouples the GUI from the underlying I/O functions, but in the future we will gladly gladly gladly accept PRs like this

sneakers-the-rat avatar Jan 12 '24 23:01 sneakers-the-rat

Did you see my PR? How did you plan to do the synchronization? I have developed a software synchronizer that can trigger multiple hardwares&softwares, and across multiple computers. I hope it would be inspiring for your project.

image

chenxinfeng4 avatar Jan 13 '24 07:01 chenxinfeng4