]> git.sesse.net Git - ultimatescore/commitdiff
Change to communicating over WebSockets instead of over ACMP.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 25 Feb 2018 23:54:59 +0000 (00:54 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 26 Feb 2018 00:07:16 +0000 (01:07 +0100)
client/Makefile
client/acmp_client.cpp [deleted file]
client/acmp_client.h [deleted file]
client/mainwindow.cpp
client/mainwindow.h
client/mainwindow.ui
client/ws_server.cpp [new file with mode: 0644]
client/ws_server.h [new file with mode: 0644]
score.js

index e17047e8d25a9ae4252a28174f7373ab6cf49c20..2d0b7dbd3c0aaf8ab0b6e245daf08324226c8d14 100644 (file)
@@ -2,13 +2,13 @@ CXX=g++
 PROTOC=protoc
 INSTALL=install
 EMBEDDED_BMUSB=no
-PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets
+PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets Qt5WebSockets
 CXXFLAGS ?= -O2 -g -Wall  # Will be overridden by environment.
 CXXFLAGS += -std=gnu++11 -fPIC $(shell pkg-config --cflags $(PKG_MODULES)) -pthread
 LDLIBS=$(shell pkg-config --libs $(PKG_MODULES)) -pthread
 
-OBJS_WITH_MOC = mainwindow.o
-OBJS += $(OBJS_WITH_MOC) main.o acmp_client.o event_device.o
+OBJS_WITH_MOC = mainwindow.o ws_server.o
+OBJS += $(OBJS_WITH_MOC) main.o event_device.o
 OBJS += $(OBJS_WITH_MOC:.o=.moc.o)
 
 %.o: %.cpp
diff --git a/client/acmp_client.cpp b/client/acmp_client.cpp
deleted file mode 100644 (file)
index bb1e56c..0000000
+++ /dev/null
@@ -1,213 +0,0 @@
-#include "acmp_client.h"
-
-#include <functional>
-
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <netdb.h>
-#include <unistd.h>
-#include <errno.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/ioctl.h>
-
-using namespace std;
-
-ACMPClient::ACMPClient(const string &host, int port)
-       : host(host), port(port) {}
-
-void ACMPClient::add_init_command(const string &cmd)
-{
-       init_commands.push_back(cmd + "\r\n");
-}
-
-void ACMPClient::start()
-{
-       t = thread(&ACMPClient::thread_func, this);
-}
-
-void ACMPClient::end()
-{
-       t.join();
-}
-
-void ACMPClient::send_command(const string &cmd)
-{
-       lock_guard<mutex> lock(mu);
-       queued_commands.push_back(cmd + "\r\n");
-}
-
-void ACMPClient::change_server(const string &host, int port)
-{
-       lock_guard<mutex> lock(mu);
-       queued_commands.push_back("");  // Marker for disconnect.
-       this->host = host;
-       this->port = port;
-}
-
-void ACMPClient::set_connection_callback(const std::function<void(bool)> &callback)
-{
-       connection_callback = callback; 
-}
-
-namespace {
-
-int lookup_and_connect(const char *host, int port)
-{
-       addrinfo hints;
-       memset(&hints, 0, sizeof(hints));
-       hints.ai_family = AF_UNSPEC;
-       hints.ai_socktype = SOCK_STREAM;
-
-       addrinfo *res;
-       char portstr[16];
-       snprintf(portstr, sizeof(portstr), "%d", port);
-       int err = getaddrinfo(host, portstr, &hints, &res);
-       if (err != 0) {
-               fprintf(stderr, "Lookup of %s:%d failed: %s\n", host, port, strerror(errno));
-               return -1;
-       }
-
-       for (addrinfo *p = res; p != NULL; p = p->ai_next) {
-               int sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
-               if (sock == -1) {
-                       perror("socket");
-                       continue;
-               }
-
-               if (connect(sock, p->ai_addr, p->ai_addrlen) == -1) {
-                       perror("connect");
-                       close(sock);
-                       continue;
-               }
-
-               // Success!
-               freeaddrinfo(res);
-               return sock;
-       }
-
-       freeaddrinfo(res);
-       return -1;
-}
-
-}  // namespace
-
-void ACMPClient::thread_func()
-{
-       if (connection_callback) {
-               connection_callback(false);
-       }
-       for ( ;; ) {
-               int sock, port_copy;
-               string host_copy;
-               {
-                       lock_guard<mutex> lock(mu);
-                       host_copy = host;
-                       port_copy = port;
-               }
-               sock = lookup_and_connect(host_copy.c_str(), port_copy);
-               if (sock == -1) {
-                       sleep(1);
-                       continue;
-               }
-
-               int one = 1;
-               if (ioctl(sock, FIONBIO, &one) == 1) {
-                       perror("ioctl(FIONBIO)");
-                       close(sock);
-                       sleep(1);
-                       continue;
-               }
-
-               printf("Connected to CasparCG.\n");
-               if (connection_callback) {
-                       connection_callback(true);
-               }
-
-               bool first = true;
-
-               for ( ;; ) {
-                       vector<string> commands;
-                       if (first) {
-                               commands = init_commands;
-                               first = false;
-                       } else {
-                               lock_guard<mutex> lock(mu);
-                               swap(commands, queued_commands);
-                       }
-
-                       bool broken = false;
-                       string buf;
-                       for (const string &cmd : commands) {
-                               buf += cmd;
-                               if (cmd.empty()) {
-                                       printf("Closing CasparCG socket for reconnection.\n");
-                                       broken = true;
-                                       break;
-                               }
-                       }
-
-                       if (broken) {
-                               break;
-                       }
-                       if (!buf.empty()) {
-                               printf("Writing: '%s'\n", buf.c_str());
-                       }
-
-                       size_t pos = 0;
-                       do {
-                               // Consume until there is no more.
-                               char junk[1024];
-                               int err = read(sock, junk, sizeof(junk));
-                               if (err == -1) {
-                                       if (err == EAGAIN) {
-                                               perror("read");
-                                               broken = true;
-                                               break;
-                                       }
-                               }
-                               if (err == 0) {
-                                       // Closed.
-                                       printf("Server closed connection.\n");
-                                       broken = true;
-                                       break;
-                               }
-                               if (err > 0) {
-                                       // Try again.
-                                       junk[err] = 0;
-                                       printf("From server: '%s'\n", junk);
-                                       continue;
-                               }
-
-                               if (pos < buf.size()) {
-                                       // Now write as much as we can.
-                                       err = write(sock, buf.data() + pos, buf.size() - pos);
-                                       if (err == -1) {
-                                               perror("write");
-                                               broken = true;
-                                               break;
-                                       }
-                                       if (err == 0) {
-                                               // Uh-oh. Buffer full for some reason?
-                                               usleep(10000);
-                                       }
-                                       pos += err;
-                               }
-                       } while (pos < buf.size());
-
-                       if (broken) {
-                               break;
-                       }
-                       if (buf.empty()) {
-                               usleep(100000);
-                               continue;
-                       }
-               }
-
-               close(sock);
-               if (connection_callback) {
-                       connection_callback(false);
-               }
-               sleep(1);
-       }
-}
diff --git a/client/acmp_client.h b/client/acmp_client.h
deleted file mode 100644 (file)
index a386b27..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-#ifndef _ACMP_CLIENT_H
-#define _ACMP_CLIENT_H 1
-
-#include <functional>
-#include <mutex>
-#include <string>
-#include <thread>
-#include <vector>
-
-class ACMPClient {
-public:
-       ACMPClient(const std::string &host, int port);
-
-       void add_init_command(const std::string &cmd);
-       void start();
-       void end();
-       void send_command(const std::string &cmd);  // Thread-safe.
-       void change_server(const std::string &host, int port);  // Thread-safe.
-       void set_connection_callback(const std::function<void(bool)> &callback);
-
-private:
-       void thread_func();
-
-       std::thread t;
-       std::vector<std::string> init_commands;
-       std::function<void(bool)> connection_callback;
-
-       std::mutex mu;
-       std::string host;  // Protected by mu.
-       int port;  // Protected by mu.
-       std::vector<std::string> queued_commands;  // Protected by mu.
-};
-
-#endif  // !defined(_ACMP_CLIENT_H)
index b47a9e6d4e858731f0fba99914193536341064ad..6b8f7136e06c41087bb1fc393145897a036fc44a 100644 (file)
@@ -52,7 +52,7 @@ string escape_quotes(const string &str)
 {
        string s = "";
        for (char ch : str) {
-               if (ch == '"' || ch == '\\') {
+               if (ch == '"') {
                        s += '\\';
                }
                s += ch;
@@ -84,19 +84,15 @@ MainWindow::MainWindow(QWidget *parent) :
     ui(new Ui::MainWindow)
 {
        ui->setupUi(this);
-       acmp = new ACMPClient("127.0.0.1", 5250);
-       acmp->add_init_command("MIXER 1 STRAIGHT_ALPHA_OUTPUT 1");
-       acmp->add_init_command("CG 1 ADD 0 \"score\" 1 \"<templateData></templateData>\"");
-       acmp->set_connection_callback([this](bool connected) {
+       ws = new WSServer("127.0.0.1", 5250);
+       ws->set_connection_callback([this](bool connected) {
                string msg = connected ? "Connected" : "Not connected";
                post_to_main_thread([this, msg]() {
-                       ui->casparcg_connected_label->setText(QString::fromStdString(msg));
+                       ui->ws_connected_label->setText(QString::fromStdString(msg));
                });
        });
 
-       acmp->start();
-
-       connect(ui->casparcg_reconnect_btn, &QPushButton::clicked, this, &MainWindow::casparcg_reconnect_clicked);
+       connect(ui->ws_disconnect_btn, &QPushButton::clicked, this, &MainWindow::ws_disconnect_clicked);
        connect(ui->set_initials_btn, &QPushButton::clicked, this, &MainWindow::set_initials_clicked);
        connect(ui->set_color_btn, &QPushButton::clicked, this, &MainWindow::set_color_clicked);
        connect(ui->set_score_btn, &QPushButton::clicked, this, &MainWindow::set_score_clicked);
@@ -143,14 +139,12 @@ MainWindow::MainWindow(QWidget *parent) :
 
 MainWindow::~MainWindow()
 {
-       acmp->end();
        delete ui;
 }
 
-void MainWindow::casparcg_reconnect_clicked()
+void MainWindow::ws_disconnect_clicked()
 {
-       acmp->change_server(ui->casparcg_host_box->text().toStdString(),
-                           stoi(ui->casparcg_port_box->text().toStdString()));
+       ws->change_port(stoi(ui->ws_port_box->text().toStdString()));
 }
 
 void MainWindow::set_initials_clicked()
@@ -158,8 +152,8 @@ void MainWindow::set_initials_clicked()
        map<string, string> param;
        param["team1"] = escape_html(ui->initials_1_edit->text().toStdString());
        param["team2"] = escape_html(ui->initials_2_edit->text().toStdString());
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 setteams");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval setteams()");
 }
 
 void MainWindow::set_color_clicked()
@@ -167,8 +161,8 @@ void MainWindow::set_color_clicked()
        map<string, string> param;
        param["team1color"] = ui->color_1_edit->text().toStdString();  // Should maybe be escaped, but meh.
        param["team2color"] = ui->color_2_edit->text().toStdString();
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 setcolors");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval setcolors()");
 }
 
 void MainWindow::set_score_clicked()
@@ -176,8 +170,8 @@ void MainWindow::set_score_clicked()
        map<string, string> param;
        param["score1"] = to_string(ui->score_1_box->value());
        param["score2"] = to_string(ui->score_2_box->value());
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 setscore");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval setscore()");
        autocomment_update();
 }
 
@@ -199,8 +193,8 @@ void MainWindow::set_clock_clicked()
        map<string, string> param;
        param["clock_min"] = to_string(ui->clock_min_box->value());
        param["clock_sec"] = to_string(ui->clock_sec_box->value());
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 setclockfromstate");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval setclockfromstate()");
 }
 
 void MainWindow::set_clock_limit_clicked()
@@ -208,42 +202,42 @@ void MainWindow::set_clock_limit_clicked()
        map<string, string> param;
        param["clock_limit_min"] = to_string(ui->clock_limit_min_box->value());
        param["clock_limit_sec"] = to_string(ui->clock_limit_sec_box->value());
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 setclocklimitfromstate");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval setclocklimitfromstate()");
 }
 
 void MainWindow::start_and_show_clock_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 startclock");  // Also shows.
+       ws->send_command("eval startclock()");  // Also shows.
 }
 
 void MainWindow::stop_clock_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 stopclock");
+       ws->send_command("eval stopclock()");
 }
 
 void MainWindow::show_clock_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 showclock");
+       ws->send_command("eval showclock()");
 }
 
 void MainWindow::hide_clock_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 hideclock");
+       ws->send_command("eval hideclock()");
 }
 
 void MainWindow::set_comment_clicked()
 {
        map<string, string> param;
        param["comment"] = ui->comment_edit->text().toStdString();
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 setcomment");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval setcomment()");
 }
 
 void MainWindow::set_and_show_comment_clicked()
 {
        set_comment_clicked();
-       acmp->send_command("cg 1 invoke 1 showcomment");
+       ws->send_command("eval showcomment()");
 }
 
 void MainWindow::set_and_show_autocomment_clicked()
@@ -254,7 +248,7 @@ void MainWindow::set_and_show_autocomment_clicked()
 
 void MainWindow::hide_comment_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 hidecomment");
+       ws->send_command("eval hidecomment()");
 }
 
 void MainWindow::show_lower_third_clicked()
@@ -262,13 +256,13 @@ void MainWindow::show_lower_third_clicked()
        map<string, string> param;
        param["text1"] = ui->lowerthird_heading_edit->text().toStdString();
        param["text2"] = ui->lowerthird_subheading_edit->text().toStdString();
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 setandshowlowerthird");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval setandshowlowerthird()");
 }
 
 void MainWindow::hide_lower_third_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 hidelowerthird");
+       ws->send_command("eval hidelowerthird()");
 }
 
 void MainWindow::quick_lower_third_activate()
@@ -279,12 +273,12 @@ void MainWindow::quick_lower_third_activate()
        } else if (code == "B") {
                add_goal(ui->score_2_box, 1);
        } else if (code == "C") {
-               acmp->send_command("cg 1 invoke 1 hidelowerthird");
+               ws->send_command("eval hidelowerthird()");
        } else {
                map<string, string> param;
                param["code"] = code;
-               acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-               acmp->send_command("cg 1 invoke 1 quicklowerthird");
+               ws->send_command("update " + serialize_as_json(param));
+               ws->send_command("eval quicklowerthird()");
        }
        ui->quick_lower_third_edit->clear();
 }
@@ -313,39 +307,39 @@ void MainWindow::autocomment_update()
 
 void MainWindow::show_scorebug_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 stopcarousel");
-       acmp->send_command("cg 1 invoke 1 hidetable");
-       acmp->send_command("cg 1 invoke 1 showscorebug");
+       ws->send_command("eval stopcarousel()");
+       ws->send_command("eval hidetable()");
+       ws->send_command("eval showscorebug()");
 }
 
 void MainWindow::show_group_clicked(const std::string &group_name)
 {
        map<string, string> param;
        param["group_name"] = group_name;
-       acmp->send_command("cg 1 invoke 1 stopcarousel");
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 showgroup_from_state");
+       ws->send_command("eval stopcarousel()");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval showgroup_from_state()");
 }
 
 void MainWindow::show_roster_clicked(const std::string &team_code)
 {
        map<string, string> param;
        param["team_code"] = team_code;
-       acmp->send_command("cg 1 invoke 1 stopcarousel");
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 showroster_from_state");
+       ws->send_command("eval stopcarousel()");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval showroster_from_state()");
 }
 
 void MainWindow::show_schedule_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 stopcarousel");
-       acmp->send_command("cg 1 invoke 1 showschedule");
+       ws->send_command("eval stopcarousel()");
+       ws->send_command("eval showschedule()");
 }
 
 void MainWindow::show_carousel_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 stopcarousel");
-       acmp->send_command("cg 1 invoke 1 showcarousel");
+       ws->send_command("eval stopcarousel()");
+       ws->send_command("eval showcarousel()");
 }
 
 void MainWindow::show_roster_carousel_clicked()
@@ -353,14 +347,14 @@ void MainWindow::show_roster_carousel_clicked()
        map<string, string> param;
        param["team1"] = escape_html(ui->initials_1_edit->text().toStdString());
        param["team2"] = escape_html(ui->initials_2_edit->text().toStdString());
-       acmp->send_command("cg 1 invoke 1 stopcarousel");
-       acmp->send_command("cg 1 update 1 \"" + escape_quotes(serialize_as_json(param)) + "\"");
-       acmp->send_command("cg 1 invoke 1 showrostercarousel_from_state");
+       ws->send_command("eval stopcarousel()");
+       ws->send_command("update " + serialize_as_json(param));
+       ws->send_command("eval showrostercarousel_from_state()");
 }
 
 void MainWindow::show_nothing_clicked()
 {
-       acmp->send_command("cg 1 invoke 1 hidescorebug");
-       acmp->send_command("cg 1 invoke 1 stopcarousel");
-       acmp->send_command("cg 1 invoke 1 hidetable");
+       ws->send_command("eval hidescorebug()");
+       ws->send_command("eval stopcarousel()");
+       ws->send_command("eval hidetable()");
 }
index ae675caa1b9e962700a76bca6efbfcf240b88cb6..1d7fe88d665b0ec725400af8ae5299e01fabf369 100644 (file)
@@ -3,7 +3,7 @@
 
 #include <QMainWindow>
 
-#include "acmp_client.h"
+#include "ws_server.h"
 #include "event_device.h"
 
 namespace Ui {
@@ -21,7 +21,7 @@ public:
        ~MainWindow();
 
 private:
-       void casparcg_reconnect_clicked();
+       void ws_disconnect_clicked();
        void set_initials_clicked();
        void set_color_clicked();
        void set_score_clicked();
@@ -50,7 +50,7 @@ private:
        void show_nothing_clicked();
 
        Ui::MainWindow *ui;
-       ACMPClient *acmp;
+       WSServer *ws;
        EventDevice *event_device;
 };
 
index 28a01a55aa9966bcb7c6f9a5ee3c2192c826cff2..cfc8cc40c3946dfb62f3485650f92c74d6c4f7a5 100644 (file)
         <item>
          <widget class="QLabel" name="label_11">
           <property name="text">
-           <string>CasparCG server:</string>
+           <string>WebSocket server port:</string>
           </property>
          </widget>
         </item>
         <item>
-         <widget class="QLineEdit" name="casparcg_host_box">
-          <property name="text">
-           <string>localhost</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QLineEdit" name="casparcg_port_box">
+         <widget class="QLineEdit" name="ws_port_box">
           <property name="maximumSize">
            <size>
             <width>50</width>
          </widget>
         </item>
         <item>
-         <widget class="QPushButton" name="casparcg_reconnect_btn">
+         <widget class="QPushButton" name="ws_disconnect_btn">
           <property name="text">
-           <string>Reconnect</string>
+           <string>Change port and disconnect all</string>
           </property>
          </widget>
         </item>
         <item>
-         <widget class="QLabel" name="casparcg_connected_label">
+         <widget class="QLabel" name="ws_connected_label">
           <property name="minimumSize">
            <size>
             <width>100</width>
  </widget>
  <layoutdefault spacing="6" margin="11"/>
  <tabstops>
-  <tabstop>casparcg_host_box</tabstop>
-  <tabstop>casparcg_port_box</tabstop>
-  <tabstop>casparcg_reconnect_btn</tabstop>
+  <tabstop>ws_port_box</tabstop>
+  <tabstop>ws_disconnect_btn</tabstop>
   <tabstop>initials_1_edit</tabstop>
   <tabstop>initials_2_edit</tabstop>
   <tabstop>set_initials_btn</tabstop>
diff --git a/client/ws_server.cpp b/client/ws_server.cpp
new file mode 100644 (file)
index 0000000..914da38
--- /dev/null
@@ -0,0 +1,74 @@
+#include "ws_server.h"
+
+#include <functional>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+
+#include "QtWebSockets/qwebsocketserver.h"
+#include "QtWebSockets/qwebsocket.h"
+
+using namespace std;
+
+WSServer::WSServer(const string &host, int port)
+       : ws_server(new QWebSocketServer("ACMP client", QWebSocketServer::NonSecureMode, this))
+{
+       if (ws_server->listen(QHostAddress::Any, port)) {
+               connect(ws_server, &QWebSocketServer::newConnection, this, &WSServer::on_new_connection);
+       }
+}
+
+void WSServer::change_port(int port)
+{
+       unordered_set<QWebSocket *> old_clients = move(clients);
+       for (QWebSocket *sock : old_clients) {
+               delete sock;
+       }
+       ws_server->close();
+       ws_server->listen(QHostAddress::Any, port);
+}
+
+void WSServer::add_init_command(const string &cmd)
+{
+       init_commands.push_back(cmd + "\r\n");
+}
+
+void WSServer::set_connection_callback(const std::function<void(bool)> &callback)
+{
+       connection_callback = callback;
+}
+
+void WSServer::send_command(const string &cmd)
+{
+       for (QWebSocket *sock : clients) {
+               sock->sendTextMessage(QString::fromStdString(cmd));
+       }
+}
+
+void WSServer::on_new_connection()
+{
+       QWebSocket *sock = ws_server->nextPendingConnection();
+       connect(sock, &QWebSocket::disconnected, this, &WSServer::disconnected);
+
+       for (const string &cmd : init_commands) {
+               sock->sendTextMessage(QString::fromStdString(cmd));
+       }
+       clients.insert(sock);
+       connection_callback(true);
+}
+
+void WSServer::disconnected()
+{
+       QWebSocket *sock = qobject_cast<QWebSocket *>(sender());
+       if (sock) {
+               clients.erase(sock);
+               sock->deleteLater();
+               connection_callback(!clients.empty());
+       }
+}
diff --git a/client/ws_server.h b/client/ws_server.h
new file mode 100644 (file)
index 0000000..17d1309
--- /dev/null
@@ -0,0 +1,41 @@
+#ifndef _WS_SERVER_H
+#define _WS_SERVER_H 1
+
+#include <functional>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <unordered_set>
+#include <vector>
+
+#include <QObject>
+
+class QWebSocket;
+class QWebSocketServer;
+
+class WSServer : public QObject {
+       Q_OBJECT
+
+public:
+       WSServer(const std::string &host, int port);
+
+       void add_init_command(const std::string &cmd);
+       void set_connection_callback(const std::function<void(bool)> &callback);
+       void send_command(const std::string &cmd);
+       void change_port(int port);
+
+private slots:
+       void on_new_connection();
+       void disconnected();
+
+private:
+       void thread_func();
+
+       std::vector<std::string> init_commands;
+       std::function<void(bool)> connection_callback;
+
+       QWebSocketServer *ws_server;
+       std::unordered_set<QWebSocket *> clients;
+};
+
+#endif  // !defined(_WS_SERVER_H)
index deae9d8ad9824a4da5c7e7e514daaeb1049e193f..85ab488b39d9a17f05ee9a81bcb86c787f85c3f7 100644 (file)
--- a/score.js
+++ b/score.js
@@ -241,3 +241,40 @@ update_score();
 
 //play();
 //startclock();
+
+let websocket = null;
+
+function open_ws()
+{
+       console.log("Connecting...");
+       try {
+               if (websocket)
+                       websocket.close();
+               websocket = new WebSocket("ws://127.0.0.1:5250/");
+               websocket.onopen = function(evt) {
+                       console.log("Connected to client.");
+               };
+               websocket.onclose = function(evt) {
+                       console.log("Disconnected from client.");
+                       setTimeout(open_ws, 100);
+               };
+               websocket.onmessage = function(evt) {
+                       let msg = evt.data;
+                       let m = msg.match(/^update (.*)/);
+                       if (m !== null) {
+                               update(m[1]);
+                       }
+                       m = msg.match(/^eval (.*)/);
+                       if (m !== null) {
+                               eval(m[1]);
+                       }
+               };
+               websocket.onerror = function(evt) {
+                       console.log('Error: ' + evt.data);
+               };
+       } catch (exception) {
+               console.log('Error: ' + exception);
+               setTimeout(open_ws, 100);
+       }
+};
+open_ws();