Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Software synchronize with other DAQ #55

Open
chenxinfeng4 opened this issue Dec 26, 2022 · 3 comments
Open

Software synchronize with other DAQ #55

chenxinfeng4 opened this issue Dec 26, 2022 · 3 comments

Comments

@chenxinfeng4
Copy link

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
Copy link
Author

chenxinfeng4 commented Dec 28, 2022

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).

@sneakers-the-rat
Copy link
Contributor

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

@chenxinfeng4
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants