summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
author Christian Brunschen <christian@brunschen.com>2017-07-05 17:27:34 +0100
committer Christian Brunschen <christian@brunschen.com>2017-07-05 17:27:34 +0100
commit0206314395c7dcf7b51311e635588778f6d9cca3 (patch)
treeab242d5e87d9c1f2578edda08c7f150b841831e6
parent2924e3c17470291a520492d68189dda16e99e2a4 (diff)
Add an external panel for the Ensoniq VFX family of keyboards, with a websocket interface and an HTML/Javascript implementation that can be served over HTTP.
-rw-r--r--src/emu/http.cpp369
-rw-r--r--src/emu/http.h140
-rw-r--r--src/emu/machine.cpp10
-rw-r--r--src/lib/util/server_http.hpp5
-rw-r--r--src/lib/util/server_ws.hpp28
-rw-r--r--src/mame/drivers/esq5505.cpp8
-rw-r--r--src/mame/machine/esqpanel.cpp482
-rw-r--r--src/mame/machine/esqpanel.h62
-rw-r--r--src/mame/machine/esqvfd.cpp41
-rw-r--r--src/mame/machine/esqvfd.h4
-rw-r--r--web/esqpanel/vfx/FrontPanel.html25
-rw-r--r--web/esqpanel/vfx/FrontPanel.js1209
12 files changed, 2330 insertions, 53 deletions
diff --git a/src/emu/http.cpp b/src/emu/http.cpp
index 48d45e89fa1..9f911ed1252 100644
--- a/src/emu/http.cpp
+++ b/src/emu/http.cpp
@@ -13,6 +13,10 @@ HTTP server handling
#include "server_http.hpp"
#include <fstream>
+#include <inttypes.h>
+#include <stdint.h>
+
+
const static struct mapping
{
const char* extension;
@@ -107,8 +111,153 @@ static std::string extension_to_type(const std::string& extension)
return "text/plain";
}
+/** An HTTP Request. */
+struct http_request_impl : public http_manager::http_request
+{
+public:
+ std::shared_ptr<webpp::http_server::Request> m_request;
+ std::size_t m_query;
+ std::size_t m_fragment;
+ std::size_t m_path_end;
+ std::size_t m_query_end;
+
+ http_request_impl(std::shared_ptr<webpp::http_server::Request> request) : m_request(request) {
+ std::size_t len = m_request->path.length();
+
+ m_fragment = m_request->path.find('#');
+ m_query_end = m_fragment == std::string::npos ? len : m_fragment;
+
+ m_query = m_request->path.find('?');
+ m_path_end = m_query == std::string::npos ? m_query_end : m_query;
+ }
+
+ /** Retrieves the requested resource. */
+ virtual const std::string get_resource() {
+ // The entire resource: path, query and fragment.
+ return m_request->path;
+ }
+
+ /** Returns the path part of the requested resource. */
+ virtual const std::string get_path() {
+ return m_request->path.substr(0, m_path_end);
+ }
+
+ /** Returns the query part of the requested resource. */
+ virtual const std::string get_query() {
+ return m_query == std::string::npos ? "" : m_request->path.substr(m_query, m_query_end);
+ }
+
+ /** Returns the fragment part of the requested resource. */
+ virtual const std::string get_fragment() {
+ return m_fragment == std::string::npos ? "" : m_request->path.substr(m_fragment);
+ }
+
+ /** Retrieves a header from the HTTP request. */
+ virtual const std::string get_header(const std::string &header_name) {
+ auto i = m_request->header.find(header_name);
+ if (i != m_request->header.end()) {
+ return (*i).second;
+ } else {
+ return "";
+ }
+ }
+
+ /** Retrieves a header from the HTTP request. */
+ virtual const std::list<std::string> get_headers(const std::string &header_name) {
+ std::list<std::string> result;
+ auto range = m_request->header.equal_range(header_name);
+ for (auto i = range.first; i != range.second; i++) {
+ result.push_back((*i).second);
+ }
+ return result;
+ }
+
+ /** Returns the body that was submitted with the HTTP request. */
+ virtual const std::string get_body() {
+ // TODO(cbrunschen): What to return here - http_server::Request has a 'content' feld that is never filled in!
+ return "";
+ }
+};
+
+/** An HTTP response. */
+struct http_response_impl : public http_manager::http_response {
+ std::shared_ptr<webpp::http_server::Response> m_response;
+ int m_status;
+ std::string m_content_type;
+ std::stringstream m_headers;
+ std::stringstream m_body;
+
+ http_response_impl(std::shared_ptr<webpp::http_server::Response> response) : m_response(response) { }
+
+ /** Sets the HTTP status to be returned to the client. */
+ virtual void set_status(int status) {
+ m_status = status;
+ }
+
+ /** Sets the HTTP content type to be returned to the client. */
+ virtual void set_content_type(const std::string &content_type) {
+ m_content_type = content_type;
+ }
+
+ /** Sets the body to be sent to the client. */
+ virtual void set_body(const std::string &body) {
+ m_body.str("");
+ append_body(body);
+ }
+
+ /** Appends something to the body to be sent to the client. */
+ virtual void append_body(const std::string &body) {
+ m_body << body;
+ }
+
+ /** Sends the response to the client. */
+ void send() {
+ m_response->type(m_content_type);
+ m_response->status(m_status);
+ m_response->send(m_body.str());
+ }
+};
+
+struct websocket_endpoint_impl : public http_manager::websocket_endpoint {
+ /** The underlying edpoint. */
+ std::shared_ptr<webpp::ws_server::Endpoint> m_endpoint;
+
+ websocket_endpoint_impl(std::shared_ptr<webpp::ws_server::Endpoint> endpoint,
+ http_manager::websocket_open_handler on_open,
+ http_manager::websocket_message_handler on_message,
+ http_manager::websocket_close_handler on_close,
+ http_manager::websocket_error_handler on_error)
+ : m_endpoint(endpoint) {
+ this->on_open = on_open;
+ this->on_message = on_message;
+ this->on_close = on_close;
+ this->on_error = on_error;
+ }
+};
+
+struct websocket_connection_impl : public http_manager::websocket_connection {
+ /** The server */
+ webpp::ws_server *m_wsserver;
+ /* The underlying Commection. */
+ std::shared_ptr<webpp::ws_server::Connection> m_connection;
+ websocket_connection_impl(webpp::ws_server *server, std::shared_ptr<webpp::ws_server::Connection> connection)
+ : m_wsserver(server), m_connection(connection) { }
+
+ /** Sends a message to the client that is connected on the other end of this Websocket connection. */
+ virtual void send_message(const std::string &payload, int opcode) {
+ std::shared_ptr<webpp::ws_server::SendStream> message_stream = std::make_shared<webpp::ws_server::SendStream>();
+ (*message_stream) << payload;
+ m_wsserver->send(m_connection, message_stream, nullptr, opcode | 0x80);
+ }
+
+ /** Closes this open Websocket connection. */
+ virtual void close() {
+ m_wsserver->send_close(m_connection, 1000 /* normal close */);
+ }
+};
+
http_manager::http_manager(bool active, short port, const char *root)
- : m_io_context(std::make_shared<asio::io_context>())
+ : m_io_context(std::make_shared<asio::io_context>()), m_root(root)
{
if (!active) return;
@@ -117,22 +266,20 @@ http_manager::http_manager(bool active, short port, const char *root)
m_server->set_io_context(m_io_context);
m_wsserver = std::make_unique<webpp::ws_server>();
- auto& endpoint = m_wsserver->endpoint["/"];
+ auto& endpoint = m_wsserver->m_endpoint["/"];
m_server->on_get([this, root](auto response, auto request) {
std::string doc_root = root;
- std::string path = request->path;
+ auto request_impl = std::make_shared<http_request_impl>(request);
+
+ std::string path = request_impl->get_path();
// If path ends in slash (i.e. is a directory) then add "index.html".
if (path[path.size() - 1] == '/')
{
path += "index.html";
}
- std::size_t last_qmark_pos = path.find_last_of("?");
- if (last_qmark_pos != std::string::npos)
- path = path.substr(0, last_qmark_pos - 1);
-
// Determine the file extension.
std::size_t last_slash_pos = path.find_last_of("/");
std::size_t last_dot_pos = path.find_last_of(".");
@@ -195,19 +342,207 @@ http_manager::~http_manager()
m_server_thread.join();
}
+static void on_get(http_manager::http_handler handler, std::shared_ptr<webpp::http_server::Response> response, std::shared_ptr<webpp::http_server::Request> request) {
+ auto request_impl = std::make_shared<http_request_impl>(request);
+ auto response_impl = std::make_shared<http_response_impl>(response);
-void http_manager::update()
-{
- if (!m_server) return;
+ handler(request_impl, response_impl);
- m_server->clear();
- for (auto handler : m_handlers)
+ response_impl->send();
+}
+
+void http_manager::on_open(http_manager::websocket_endpoint_ptr endpoint, void *connection) {
+ std::lock_guard<std::mutex> lock(m_connections_mutex);
+ webpp::ws_server *ws_server = m_wsserver.get();
+ // Keep an oening shared_ptr to the Connection, so it won't go away while we are using it.
+ std::shared_ptr<webpp::ws_server::Connection> conn = (static_cast<webpp::ws_server::Connection *>(connection))->ptr();
+ http_manager::websocket_connection_ptr connection_impl = std::make_shared<websocket_connection_impl>(ws_server, conn);
+ m_connections[connection] = connection_impl;
+
+ if (endpoint->on_open) {
+ endpoint->on_open(connection_impl);
+ }
+}
+
+void http_manager::on_message(http_manager::websocket_endpoint_ptr endpoint, void *connection, const std::string &payload, int opcode) {
+ if (endpoint->on_message) {
+ std::lock_guard<std::mutex> lock(m_connections_mutex);
+ auto i = m_connections.find(connection);
+ if (i != m_connections.end()) {
+ http_manager::websocket_connection_ptr websocket_connection_impl = (*i).second;
+ endpoint->on_message(websocket_connection_impl, payload, opcode);
+ }
+ }
+}
+
+void http_manager::on_close(http_manager::websocket_endpoint_ptr endpoint, void *connection,
+ int status, const std::string& reason) {
+ std::lock_guard<std::mutex> lock(m_connections_mutex);
+ auto i = m_connections.find(connection);
+ if (i != m_connections.end()) {
+ if (endpoint->on_close) {
+ http_manager::websocket_connection_ptr websocket_connection_impl = (*i).second;
+ endpoint->on_close(websocket_connection_impl, status, reason);
+ }
+
+ m_connections.erase(connection);
+ }
+}
+
+void http_manager::on_error(http_manager::websocket_endpoint_ptr endpoint, void *connection,
+ const std::error_code& error_code) {
+ std::lock_guard<std::mutex> lock(m_connections_mutex);
+ auto i = m_connections.find(connection);
+ if (i != m_connections.end()) {
+ if (endpoint->on_error) {
+ http_manager::websocket_connection_ptr websocket_connection_impl = (*i).second;
+ endpoint->on_error(websocket_connection_impl, error_code);
+ }
+
+ m_connections.erase(connection);
+ }
+}
+
+bool http_manager::read_file(std::ostream &os, const std::string &path) {
+ std::ostringstream full_path;
+ full_path << m_root << path;
+ util::core_file::ptr f;
+ osd_file::error e = util::core_file::open(full_path.str(), OPEN_FLAG_READ, f);
+ if (e == osd_file::error::NONE)
+ {
+ int c;
+ while ((c = f->getc()) >= 0)
+ {
+ os.put(c);
+ }
+ }
+ return e == osd_file::error::NONE;
+}
+
+void http_manager::serve_document(http_request_ptr request, http_response_ptr response, const std::string &filename) {
+ std::ostringstream os;
+ if (read_file(os, filename))
+ {
+ response->set_status(200);
+ response->set_body(os.str());
+ }
+ else
+ {
+ response->set_status(500);
+ }
+}
+
+void http_manager::serve_template(http_request_ptr request, http_response_ptr response,
+ const std::string &filename, substitution substitute, char init, char term)
+{
+ // printf("webserver: serving template '%s' at path '%s'\n", filename.c_str(), request->get_path().c_str());
+ std::stringstream ss;
+ if (read_file(ss, filename))
{
- m_server->on_get(handler.first, [handler](auto response, auto request)
+ std::ostringstream os;
+ while (ss.good())
{
- std::tuple<std::string,int, std::string> output = handler.second(request->path);
- response->type(std::get<2>(output));
- response->status(std::get<1>(output)).send(std::get<0>(output).c_str());
- });
+ std::string s;
+ getline(ss, s, init);
+ os << s;
+ if (ss.good())
+ {
+ // printf("webserver: found initiator '%c'\n", init);
+ getline(ss, s, term);
+ if (ss.good())
+ {
+ if (substitute(s))
+ {
+ os << s;
+ }
+ else
+ {
+ os << init << s << term;
+ }
+ }
+ else
+ {
+ // printf("webserver: reached end before terminator\n");
+ os << init;
+ os << s;
+ }
+ }
+ else
+ {
+ // printf("webserver: reached end before initiator\n");
+ }
+ }
+
+ response->set_status(200);
+ response->set_body(os.str());
+ }
+ else
+ {
+ response->set_status(500);
+ }
+}
+
+void http_manager::add_http_handler(const std::string &path, http_manager::http_handler handler)
+{
+ using namespace std::placeholders;
+ m_server->on_get(path, std::bind(on_get, handler, _1, _2));
+
+ std::lock_guard<std::mutex> lock(m_handlers_mutex);
+ m_handlers.emplace(path, handler);
+}
+
+void http_manager::remove_http_handler(const std::string &path) {
+ m_server->remove_handler(path);
+
+ std::lock_guard<std::mutex> lock(m_handlers_mutex);
+ m_handlers.erase(path);
+}
+
+void http_manager::clear() {
+ m_server->clear();
+
+ std::lock_guard<std::mutex> lock(m_handlers_mutex);
+ m_handlers.clear();
+}
+
+http_manager::websocket_endpoint_ptr http_manager::add_endpoint(const std::string &path,
+ http_manager::websocket_open_handler on_open,
+ http_manager::websocket_message_handler on_message,
+ http_manager::websocket_close_handler on_close,
+ http_manager::websocket_error_handler on_error) {
+ auto i = m_endpoints.find(path);
+ if (i == m_endpoints.end()) {
+ using namespace std::placeholders;
+
+ auto &endpoint = m_wsserver->m_endpoint[path];
+ std::shared_ptr<webpp::ws_server::Endpoint> endpoint_ptr(&endpoint);
+ auto endpoint_impl = std::make_shared<websocket_endpoint_impl>(endpoint_ptr, on_open, on_message, on_close, on_error);
+
+ endpoint.on_open = [&, this, endpoint_impl](std::shared_ptr<webpp::ws_server::Connection> conn) {
+ this->on_open(endpoint_impl, conn.get());
+ };
+
+ endpoint.on_message = [&, this, endpoint_impl](std::shared_ptr<webpp::ws_server::Connection> conn, std::shared_ptr<webpp::ws_server::Message> message) {
+ std::string payload = message->string();
+ int opcode = message->fin_rsv_opcode & 0x0f;
+ this->on_message(endpoint_impl, conn.get(), payload, opcode);
+ };
+
+ endpoint.on_close = [&, this, endpoint_impl](std::shared_ptr<webpp::ws_server::Connection> conn, int status, const std::string& reason) {
+ this->on_close(endpoint_impl, conn.get(), status, reason);
+ };
+
+ endpoint.on_error = [&, this, endpoint_impl](std::shared_ptr<webpp::ws_server::Connection> conn, const std::error_code& error_code) {
+ this->on_error(endpoint_impl, conn.get(), error_code);
+ };
+
+ m_endpoints[path] = endpoint_impl;
+ return endpoint_impl;
+ } else {
+ return (*i).second;
}
}
+
+void http_manager::remove_endpoint(const std::string &path) {
+ m_endpoints.erase(path);
+}
diff --git a/src/emu/http.h b/src/emu/http.h
index 9f9f2e739b6..a7e898293d4 100644
--- a/src/emu/http.h
+++ b/src/emu/http.h
@@ -39,18 +39,150 @@ class http_manager
{
DISABLE_COPYING(http_manager);
public:
+
+ /** An HTTP Request. */
+ struct http_request : public std::enable_shared_from_this<http_request>
+ {
+ /** Retrieves the requested resource. */
+ virtual const std::string get_resource() = 0; // The entire resource: path, query and fragment.
+
+ /** Returns the path part of the requested resource. */
+ virtual const std::string get_path() = 0;
+
+ /** Returns the query part of the requested resource. */
+ virtual const std::string get_query() = 0;
+
+ /** Returns the fragment part of the requested resource. */
+ virtual const std::string get_fragment() = 0;
+
+ /** Retrieves a header from the HTTP request. */
+ virtual const std::string get_header(const std::string &header_name) = 0;
+
+ /** Retrieves a header from the HTTP request. */
+ virtual const std::list<std::string> get_headers(const std::string &header_name) = 0;
+
+ /** Returns the body that was submitted with the HTTP request. */
+ virtual const std::string get_body() = 0;
+ };
+ typedef std::shared_ptr<http_request> http_request_ptr;
+
+ /** An HTTP response. */
+ struct http_response : public std::enable_shared_from_this<http_response>
+ {
+ /** Sets the HTTP status to be returned to the client. */
+ virtual void set_status(int status) = 0;
+
+ /** Sets the HTTP content type to be returned to the client. */
+ virtual void set_content_type(const std::string &type) = 0;
+
+ /** Sets the body to be sent to the client. */
+ virtual void set_body(const std::string &body) = 0;
+
+ /** Appends something to the body to be sent to the client. */
+ virtual void append_body(const std::string &body) = 0;
+ };
+ typedef std::shared_ptr<http_response> http_response_ptr;
+
+ /** Identifies a Websocket connection. */
+ struct websocket_connection : public std::enable_shared_from_this<websocket_connection>
+ {
+ /** Sends a message to the client that is connected on the other end of this Websocket connection. */
+ virtual void send_message(const std::string &payload, int opcode) = 0;
+
+ /** Closes this open Websocket connection. */
+ virtual void close() = 0;
+
+ virtual ~websocket_connection() { }
+ };
+ typedef std::shared_ptr<websocket_connection> websocket_connection_ptr;
+
+ /** Handles opening an incoming Websocket connection. */
+ typedef std::function<void(websocket_connection_ptr connection)> websocket_open_handler;
+
+ /** Handles an incoming message on an open Websocket connection. */
+ typedef std::function<void(websocket_connection_ptr connection, const std::string &payload, int opcode)> websocket_message_handler;
+
+ /** Handles when the client has closed a websocket connection. */
+ typedef std::function<void(websocket_connection_ptr connection, int status, const std::string& reason)> websocket_close_handler;
+
+ /** Handles when there has been an error on a websocket connection. */
+ typedef std::function<void(websocket_connection_ptr connection, const std::error_code& error_code)> websocket_error_handler;
+
+ /** Handles an incoming HTTP request, generating an HTTP response. */
+ typedef std::function<void(http_request_ptr, http_response_ptr)> http_handler;
+
+ struct websocket_endpoint : public std::enable_shared_from_this<websocket_endpoint> {
+ websocket_open_handler on_open;
+ websocket_message_handler on_message;
+ websocket_close_handler on_close;
+ websocket_error_handler on_error;
+ };
+ typedef std::shared_ptr<websocket_endpoint> websocket_endpoint_ptr;
+
+ /** Substitutes one string with another, and returns whether the substitution should be performed.
+ * Used when evaluating a template. */
+ typedef std::function<bool(std::string &)> substitution;
+
http_manager(bool active, short port, const char *root);
virtual ~http_manager();
+ void clear();
+
+ /** Adds a template to the web server. When the path is requested, the template will be read and parsed:
+ * strings between each pair of <init, term> characters will be passed to the substitute function and the result
+ * will be used instead.
+ */
+ void add_template(const std::string &path, substitution substitute, char init, char term);
+
+ /** Removes a template from the web server. */
+ void remove_template(const std::string &path);
+
+ /** Serves a template at an explicit path, which may be diffrent from the request path, under the document root.
+ * The template will be read and parsed:
+ * strings between each pair of <init, term> characters will be passed to the substitute function and the result
+ * will be used instead.
+ */
+ void serve_template(http_request_ptr request, http_response_ptr response, const std::string &path, substitution substitute, char init, char term);
+
+ void serve_document(http_request_ptr request, http_response_ptr response, const std::string &path);
+
+ /** Adds an HTTP handler. When the specified path is requested, the specified HTTP handler will be called. */
+ void add_http_handler(const std::string &path, http_handler handler);
+
+ /** Removes the HTTP handler at the specified path. */
+ void remove_http_handler(const std::string &path);
+
+ /** Retrieves a websocket endpoint, possibly adding it if it does not already exist. */
+ websocket_endpoint_ptr add_endpoint(const std::string &path, websocket_open_handler on_open, websocket_message_handler on_message, websocket_close_handler on_close, websocket_error_handler on_error);
+
+ /** Removes the websocket endpoint at the specified path. */
+ void remove_endpoint(const std::string &path);
+
+ void on_open(http_manager::websocket_endpoint_ptr endpoint, void *onnection);
+
+ void on_message(http_manager::websocket_endpoint_ptr endpoint, void *connection, const std::string& payload, int opcode);
+
+ void on_close(http_manager::websocket_endpoint_ptr endpoint, void *connection, int status, const std::string& reason);
- void clear() { m_handlers.clear(); update(); }
- void add(const char *url, std::function<std::tuple<std::string,int,std::string>(std::string)> func) { m_handlers.emplace(url, func); }
- void update();
+ void on_error(http_manager::websocket_endpoint_ptr endpoint, void *connection, const std::error_code& error_code);
+
+ bool read_file(std::ostream &os, const std::string &path);
+
private:
std::shared_ptr<asio::io_context> m_io_context;
std::unique_ptr<webpp::http_server> m_server;
std::unique_ptr<webpp::ws_server> m_wsserver;
std::thread m_server_thread;
- std::unordered_map<const char *, std::function<std::tuple<std::string, int, std::string>(std::string)>> m_handlers;
+ std::string m_root;
+
+ std::unordered_map<std::string, http_handler> m_handlers;
+ std::mutex m_handlers_mutex;
+
+ std::unordered_map<std::string, websocket_endpoint_ptr> m_endpoints;
+ std::mutex m_endpoints_mutex;
+
+ std::unordered_map<void *, websocket_connection_ptr> m_connections; // the keys are really webpp::ws_server::Connection pointers
+ std::mutex m_connections_mutex;
+
};
diff --git a/src/emu/machine.cpp b/src/emu/machine.cpp
index 36e107e838d..1e0f4323a7e 100644
--- a/src/emu/machine.cpp
+++ b/src/emu/machine.cpp
@@ -344,8 +344,6 @@ int running_machine::run(bool quiet)
export_http_api();
- m_manager.http()->update();
-
// run the CPUs until a reset or exit
m_hard_reset_pending = false;
while ((!m_hard_reset_pending && !m_exit_pending) || m_saveload_schedule != saveload_schedule::NONE)
@@ -1193,7 +1191,7 @@ running_machine::logerror_callback_item::logerror_callback_item(logerror_callbac
void running_machine::export_http_api()
{
- m_manager.http()->add("/api/machine", [this](std::string)
+ m_manager.http()->add_http_handler("/api/machine", [this](http_manager::http_request_ptr request, http_manager::http_response_ptr response)
{
rapidjson::StringBuffer s;
rapidjson::Writer<rapidjson::StringBuffer> writer(s);
@@ -1210,8 +1208,10 @@ void running_machine::export_http_api()
writer.EndArray();
writer.EndObject();
-
- return std::make_tuple(std::string(s.GetString()), 200, "application/json");
+
+ response->set_status(200);
+ response->set_content_type("application/json");
+ response->set_body(s.GetString());
});
}
diff --git a/src/lib/util/server_http.hpp b/src/lib/util/server_http.hpp
index d50ccc05bc7..b50161b1e3b 100644
--- a/src/lib/util/server_http.hpp
+++ b/src/lib/util/server_http.hpp
@@ -199,6 +199,7 @@ namespace webpp {
}
void clear()
{
+ std::lock_guard<std::mutex> lock(m_resource_mutex);
m_resource.clear();
}
@@ -206,12 +207,12 @@ namespace webpp {
std::function<void(std::shared_ptr<socket_type> socket, std::shared_ptr<typename ServerBase<socket_type>::Request>)> on_upgrade;
private:
- /// Warning: do not add or remove resources after start() is called
+ /// Warning: do not access (red or write) m_resources without holding m_resource_mutex
std::map<regex_orderable, std::map<std::string, std::tuple<path2regex::Keys, http_handler>>> m_resource;
+ std::mutex m_resource_mutex;
std::map<std::string, http_handler> m_default_resource;
- std::mutex m_resource_mutex;
public:
virtual void start() {
if(!m_io_context)
diff --git a/src/lib/util/server_ws.hpp b/src/lib/util/server_ws.hpp
index b9d4721e96b..41e8cda3287 100644
--- a/src/lib/util/server_ws.hpp
+++ b/src/lib/util/server_ws.hpp
@@ -53,7 +53,7 @@ namespace webpp {
public:
virtual ~SocketServerBase() {}
- class SendStream : public std::ostream {
+ class SendStream : public std::ostream, public std::enable_shared_from_this<SendStream> {
friend class SocketServerBase<socket_type>;
asio::streambuf streambuf;
@@ -65,7 +65,7 @@ namespace webpp {
};
- class Connection {
+ class Connection : public std::enable_shared_from_this<Connection> {
friend class SocketServerBase<socket_type>;
friend class SocketServer<socket_type>;
@@ -80,6 +80,10 @@ namespace webpp {
std::string remote_endpoint_address;
unsigned short remote_endpoint_port;
+
+ std::shared_ptr<Connection> ptr() {
+ return this->shared_from_this();
+ }
private:
explicit Connection(socket_type *socket): remote_endpoint_port(0), socket(socket), strand(socket->get_io_service()), closed(false) { }
@@ -143,7 +147,7 @@ namespace webpp {
}
};
- class Message : public std::istream {
+ class Message : public std::istream, public std::enable_shared_from_this<Message> {
friend class SocketServerBase<socket_type>;
public:
@@ -163,7 +167,7 @@ namespace webpp {
asio::streambuf streambuf;
};
- class Endpoint {
+ class Endpoint : public std::enable_shared_from_this<Endpoint> {
friend class SocketServerBase<socket_type>;
std::unordered_set<std::shared_ptr<Connection> > connections;
std::mutex connections_mutex;
@@ -215,8 +219,9 @@ namespace webpp {
}
};
public:
- /// Warning: do not add or remove endpoints after start() is called
- std::map<regex_orderable, Endpoint> endpoint;
+ /// Warning: do not access (red or write) m_endpoint without holding m_endpoint_mutex
+ std::map<regex_orderable, Endpoint> m_endpoint;
+ std::mutex m_endpoint_mutex;
virtual void start() {
if(!io_context)
@@ -247,7 +252,8 @@ namespace webpp {
acceptor->close();
io_context->stop();
- for(auto& p: endpoint)
+ std::lock_guard<std::mutex> lock(m_endpoint_mutex);
+ for(auto& p: m_endpoint)
p.second.connections.clear();
}
@@ -310,7 +316,8 @@ namespace webpp {
std::unordered_set<std::shared_ptr<Connection> > get_connections() {
std::unordered_set<std::shared_ptr<Connection> > all_connections;
- for(auto& e: endpoint) {
+ std::lock_guard<std::mutex> lock(m_endpoint_mutex);
+ for(auto& e: m_endpoint) {
std::lock_guard<std::mutex> lock(e.second.connections_mutex);
all_connections.insert(e.second.connections.begin(), e.second.connections.end());
}
@@ -425,10 +432,11 @@ namespace webpp {
}
}
}
-
+
void write_handshake(const std::shared_ptr<Connection> &connection, const std::shared_ptr<asio::streambuf> &read_buffer) {
//Find path- and method-match, and generate response
- for (auto &regex_endpoint : endpoint) {
+ std::lock_guard<std::mutex> lock(m_endpoint_mutex);
+ for (auto &regex_endpoint : m_endpoint) {
std::smatch path_match;
if(std::regex_match(connection->path, path_match, regex_endpoint.first)) {
auto write_buffer = std::make_shared<asio::streambuf>();
diff --git a/src/mame/drivers/esq5505.cpp b/src/mame/drivers/esq5505.cpp
index b0463cbcd51..9e7ed769967 100644
--- a/src/mame/drivers/esq5505.cpp
+++ b/src/mame/drivers/esq5505.cpp
@@ -612,7 +612,7 @@ static MACHINE_CONFIG_START( vfx )
MCFG_CPU_ADD("esp", ES5510, XTAL_10MHz)
MCFG_DEVICE_DISABLE()
- MCFG_ESQPANEL2X40_ADD("panel")
+ MCFG_ESQPANEL2X40_VFX_ADD("panel")
MCFG_ESQPANEL_TX_CALLBACK(DEVWRITELINE("duart", mc68681_device, rx_b_w))
MCFG_ESQPANEL_ANALOG_CALLBACK(WRITE16(esq5505_state, analog_w))
@@ -654,7 +654,7 @@ static MACHINE_CONFIG_DERIVED(eps, vfx)
MCFG_CPU_MODIFY( "maincpu" )
MCFG_CPU_PROGRAM_MAP(eps_map)
- MCFG_ESQPANEL_2X40_REMOVE("panel")
+ MCFG_ESQPANEL2X40_VFX_REMOVE("panel")
MCFG_ESQPANEL1X22_ADD("panel")
MCFG_ESQPANEL_TX_CALLBACK(DEVWRITELINE("duart", mc68681_device, rx_b_w))
MCFG_ESQPANEL_ANALOG_CALLBACK(WRITE16(esq5505_state, analog_w))
@@ -689,7 +689,7 @@ static MACHINE_CONFIG_START(vfx32)
MCFG_CPU_ADD("esp", ES5510, XTAL_10MHz)
MCFG_DEVICE_DISABLE()
- MCFG_ESQPANEL2X40_ADD("panel")
+ MCFG_ESQPANEL2X40_VFX_ADD("panel")
MCFG_ESQPANEL_TX_CALLBACK(DEVWRITELINE("duart", mc68681_device, rx_b_w))
MCFG_ESQPANEL_ANALOG_CALLBACK(WRITE16(esq5505_state, analog_w))
@@ -734,7 +734,7 @@ static MACHINE_CONFIG_DERIVED(sq1, vfx)
MCFG_CPU_MODIFY( "maincpu" )
MCFG_CPU_PROGRAM_MAP(sq1_map)
- MCFG_ESQPANEL_2X40_REMOVE("panel")
+ MCFG_ESQPANEL2X40_VFX_REMOVE("panel")
MCFG_ESQPANEL2X16_SQ1_ADD("panel")
MCFG_ESQPANEL_TX_CALLBACK(DEVWRITELINE("duart", mc68681_device, rx_b_w))
MCFG_ESQPANEL_ANALOG_CALLBACK(WRITE16(esq5505_state, analog_w))
diff --git a/src/mame/machine/esqpanel.cpp b/src/mame/machine/esqpanel.cpp
index 57e4a873936..6195f8b8d11 100644
--- a/src/mame/machine/esqpanel.cpp
+++ b/src/mame/machine/esqpanel.cpp
@@ -6,6 +6,378 @@
#include "emu.h"
#include "esqpanel.h"
+#define ESQPANEL_EXTERNAL_TIMER_ID 47000
+
+//**************************************************************************
+// External panel support
+//**************************************************************************
+
+#include <mutex>
+#include <set>
+#include <sstream>
+#include <string>
+#include <thread>
+#include <list>
+
+class external_panel;
+
+using external_panel_ptr = std::shared_ptr<external_panel>;
+typedef std::map<http_manager::websocket_connection_ptr, external_panel_ptr, std::owner_less<http_manager::websocket_connection_ptr>> connection_to_panel_map;
+
+enum message_type {
+ UNKNOWN = 0,
+ ANALOG = 1 << 0,
+ BUTTON = 1 << 1,
+ CONTROL = 1 << 2,
+ DISPLAY = 1 << 3,
+ INFO = 1 << 4
+};
+
+class external_panel : public std::enable_shared_from_this<external_panel>
+{
+public:
+ static int get_message_type(const char c)
+ {
+ switch(c)
+ {
+ case 'A':
+ return message_type::ANALOG;
+ case 'B':
+ return message_type::BUTTON;
+ case 'C':
+ return message_type::CONTROL;
+ case 'D':
+ return message_type::DISPLAY;
+ case 'I':
+ return message_type::INFO;
+ default:
+ return message_type::UNKNOWN;
+ }
+ }
+
+ external_panel() : m_send_message_types(0)
+ {
+ // printf("session: constructed\n");
+ }
+
+ int handle_control_message(const std::string &command)
+ {
+ int old = m_send_message_types;
+ std::istringstream is(command);
+ if (get_message_type(is.get()) != message_type::CONTROL)
+ {
+ return 0;
+ }
+
+ int n;
+ while (!is.eof()) {
+ char c = is.get();
+ int message_type = external_panel::get_message_type(c);
+ is >> n;
+ int send = (n != 0);
+ if (send)
+ {
+ m_send_message_types |= message_type;
+ }
+ else
+ {
+ m_send_message_types &= ~message_type;
+ }
+ }
+
+ return m_send_message_types ^ old;
+ }
+
+ int send_message_types()
+ {
+ return m_send_message_types;
+ }
+
+ bool send_display_data()
+ {
+ return m_send_message_types & message_type::DISPLAY;
+ }
+
+ bool send_analog_values()
+ {
+ return m_send_message_types & message_type::ANALOG;
+ }
+
+ bool send_buttons()
+ {
+ return m_send_message_types & message_type::BUTTON;
+ }
+
+private:
+ int m_send_message_types;
+};
+
+class esqpanel_external_panel_server
+{
+public:
+ enum websocket_opcode {
+ text = 1,
+ binary = 2
+ };
+ esqpanel_external_panel_server(http_manager *webserver) :
+ m_server(webserver),
+ m_keyboard("unknown"),
+ m_version("1")
+ {
+ using namespace std::placeholders;
+ m_server->add_endpoint("/esqpanel/socket",
+ std::bind(&esqpanel_external_panel_server::on_open, this, _1),
+ std::bind(&esqpanel_external_panel_server::on_message, this, _1, _2, _3),
+ std::bind(&esqpanel_external_panel_server::on_close, this, _1, _2, _3),
+ std::bind(&esqpanel_external_panel_server::on_error, this, _1, _2)
+ );
+ }
+
+ virtual ~esqpanel_external_panel_server()
+ {
+ }
+
+ void send_to_all(char c)
+ {
+ // printf("server: send_to_all(%02x)\n", ((unsigned int) c) & 0xff);
+ std::lock_guard<std::recursive_mutex> lock(m_mutex);
+ // printf("server: sending '%02x' to all\n", ((unsigned int) c) & 0xff);
+ m_to_send.str("");
+ m_to_send.put('D');
+ m_to_send.put(c);
+ const std::string &s = m_to_send.str();
+
+ for (auto iter: m_panels)
+ {
+ external_panel_ptr panel = iter.second;
+ if (panel->send_display_data())
+ {
+ send(iter.first, s);
+ }
+ }
+ }
+
+ void on_open(http_manager::websocket_connection_ptr connection)
+ {
+ using namespace std::placeholders;
+
+ std::lock_guard<std::recursive_mutex> lock(m_mutex);
+ m_panels[connection] = std::make_shared<external_panel>();
+ }
+
+ void on_message(http_manager::websocket_connection_ptr connection, const std::string &payload, int opcode)
+ {
+ external_panel_ptr panel = external_panel_for_connection(connection);
+ const std::string &command = payload;
+
+ int t = external_panel::get_message_type(command.front());
+
+ if (t == message_type::CONTROL)
+ {
+ int changed = panel->handle_control_message(command);
+ // printf("server: control message, changed = '%x'\n", changed);
+ if ((changed & message_type::DISPLAY) && panel->send_display_data())
+ {
+ // printf("server: control message, sending contents\n");
+ send_contents(connection);
+ }
+
+ if ((changed & message_type::ANALOG) && panel->send_analog_values())
+ {
+ // printf("server: control message, sending analog values\n");
+ send_analog_values(connection);
+ }
+
+ if ((changed & message_type::BUTTON) && panel->send_buttons())
+ {
+ // printf("server: control message, sending button states\n");
+ send_button_states(connection);
+ }
+ }
+ else if (t == message_type::INFO)
+ {
+ std::ostringstream o;
+ o << "I" << get_keyboard() << "," << get_version();
+ send(connection, o.str());
+ }
+ else
+ {
+ {
+ std::lock_guard<std::recursive_mutex> lock(m_mutex);
+ m_commands.emplace_back(command);
+ }
+
+ // Echo the non-command message to any other connected panels that want it
+ for (auto iter: m_panels)
+ {
+ external_panel_ptr other_panel = iter.second;
+ if (other_panel != panel && (t & other_panel->send_message_types()) != 0)
+ {
+ send(iter.first, command);
+ }
+ }
+ }
+ }
+
+ void on_close(http_manager::websocket_connection_ptr connection, int status, const std::string& reason)
+ {
+ std::lock_guard<std::recursive_mutex> lock(m_mutex);
+ m_panels.erase(connection);
+ }
+
+ void on_error(http_manager::websocket_connection_ptr connection, const std::error_code& error_code)
+ {
+ std::lock_guard<std::recursive_mutex> lock(m_mutex);
+ m_panels.erase(connection);
+ }
+
+ void on_document_request(http_manager::http_request_ptr request, http_manager::http_response_ptr response, const std::string &filename)
+ {
+ m_server->serve_document(request, response, filename);
+ }
+
+ void on_template_request(http_manager::http_request_ptr request, http_manager::http_response_ptr response, const std::string &filename)
+ {
+ using namespace std::placeholders;
+ m_server->serve_template(request, response, filename, std::bind(&esqpanel_external_panel_server::get_template_value, this, _1), '$', '$');
+ }
+
+ external_panel_ptr external_panel_for_connection(http_manager::websocket_connection_ptr connection)
+ {
+ auto it = m_panels.find(connection);
+
+ if (it == m_panels.end()) {
+ // this connection is not in the list. This really shouldn't happen
+ // and probably means something else is wrong.
+ throw std::invalid_argument("No panel avaliable for connection");
+ }
+
+ return it->second;
+ }
+
+ bool has_commands()
+ {
+ // printf("server: has_commands()\n");
+ std::lock_guard<std::recursive_mutex> lock(m_mutex);
+ return !m_commands.empty();
+ }
+
+ std::string get_next_command()
+ {
+ // printf("server: get_next_command()\n");
+ std::lock_guard<std::recursive_mutex> lock(m_mutex);
+ std::string command = std::move(m_commands.front());
+ m_commands.pop_front();
+ return command;
+ }
+
+ void set_index(const std::string &index)
+ {
+ m_index = index;
+ }
+
+ void add_http_document(const std::string &path, const std::string &filename)
+ {
+ m_server->remove_http_handler(path);
+ if (filename != "")
+ {
+ using namespace std::placeholders;
+ m_server->add_http_handler(path, std::bind(&esqpanel_external_panel_server::on_document_request, this, _1, _2, filename));
+ }
+ }
+
+ void add_http_template(const std::string &path, const std::string &filename)
+ {
+ m_server->remove_http_handler(path);
+ if (filename != "")
+ {
+ using namespace std::placeholders;
+ m_server->add_http_handler(path, std::bind(&esqpanel_external_panel_server::on_template_request, this, _1, _2, filename));
+ }
+ }
+
+ void set_content_provider(std::function<bool(std::ostream&)> provider)
+ {
+ m_content_provider = provider;
+ }
+
+ void set_keyboard(const std::string &keyboard)
+ {
+ m_keyboard = keyboard;
+ }
+
+ const std::string &get_keyboard() const
+ {
+ return m_keyboard;
+ }
+
+ const std::string &get_version() const
+ {
+ return m_version;
+ }
+
+ bool get_template_value(std::string &s)
+ {
+ if (s == "keyboard")
+ {
+ s = m_keyboard;
+ return true;
+ }
+ else if (s == "version")
+ {
+ s = m_version;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+private:
+ void send(http_manager::websocket_connection_ptr connection, const std::string &s)
+ {
+ connection->send_message(s, websocket_opcode::binary);
+ }
+
+ void send_contents(http_manager::websocket_connection_ptr connection)
+ {
+ if (m_content_provider)
+ {
+ m_to_send.str("");
+ m_to_send.put('D');
+ if (m_content_provider(m_to_send))
+ {
+ send(connection, m_to_send.str());
+ }
+ }
+ }
+
+ void send_analog_values(http_manager::websocket_connection_ptr connection)
+ {
+ // TODO(cbrunschen): get the current analog values and send them
+ }
+
+ void send_button_states(http_manager::websocket_connection_ptr connection)
+ {
+ // TODO(cbrunschen): track current button states and send them
+ }
+
+ http_manager *m_server;
+ std::recursive_mutex m_mutex;
+
+ connection_to_panel_map m_panels;
+ std::list<std::string> m_commands;
+ std::thread m_working_thread;
+ std::ostringstream m_to_send;
+
+ std::string m_index;
+ std::string m_keyboard;
+ std::string m_version;
+ std::function<bool(std::ostream&)> m_content_provider;
+ std::map<const std::string, const std::string> m_template_values;
+};
+
//**************************************************************************
// MACROS / CONSTANTS
//**************************************************************************
@@ -16,6 +388,7 @@
DEFINE_DEVICE_TYPE(ESQPANEL1X22, esqpanel1x22_device, "esqpanel122", "Ensoniq front panel with 1x22 VFD")
DEFINE_DEVICE_TYPE(ESQPANEL2X40, esqpanel2x40_device, "esqpanel240", "Ensoniq front panel with 2x40 VFD")
+DEFINE_DEVICE_TYPE(ESQPANEL2X40_VFX, esqpanel2x40_vfx_device, "esqpanel240_vfx", "Ensoniq front panel with 2x40 VFD for VFX family")
DEFINE_DEVICE_TYPE(ESQPANEL2X16_SQ1, esqpanel2x16_sq1_device, "esqpanel216_sq1", "Ensoniq front panel with 2x16 LCD")
//**************************************************************************
@@ -29,6 +402,7 @@ DEFINE_DEVICE_TYPE(ESQPANEL2X16_SQ1, esqpanel2x16_sq1_device, "esqpanel216_sq1",
esqpanel_device::esqpanel_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, uint32_t clock) :
device_t(mconfig, type, tag, owner, clock),
device_serial_interface(mconfig, *this),
+ m_light_states(0x3f), // maximum number of lights
m_write_tx(*this),
m_write_analog(*this)
{
@@ -43,6 +417,19 @@ void esqpanel_device::device_start()
{
m_write_tx.resolve_safe();
m_write_analog.resolve_safe();
+
+ m_external_panel_server = new esqpanel_external_panel_server(machine().manager().http());
+ m_external_panel_server->set_keyboard(owner()->shortname());
+ m_external_panel_server->set_index("/esqpanel/FrontPanel.html");
+ m_external_panel_server->add_http_template("/esqpanel/FrontPanel.html", get_front_panel_html_file());
+ m_external_panel_server->add_http_document("/esqpanel/FrontPanel.js", get_front_panel_js_file());
+ m_external_panel_server->set_content_provider([this](std::ostream& o)
+ {
+ return write_contents(o);
+ });
+
+ m_external_timer = timer_alloc(ESQPANEL_EXTERNAL_TIMER_ID);
+ m_external_timer->enable(false);
}
@@ -61,11 +448,36 @@ void esqpanel_device::device_reset()
m_xmit_read = m_xmit_write = 0;
m_bCalibSecondByte = false;
m_bButtonLightSecondByte = false;
+
+ attotime sample_time(0, ATTOSECONDS_PER_MILLISECOND);
+ attotime initial_delay(0, ATTOSECONDS_PER_MILLISECOND);
+
+ m_external_timer->adjust(initial_delay, 0, sample_time);
+ m_external_timer->enable(true);
+}
+
+//-------------------------------------------------
+// device_stop - device-specific stop
+//-------------------------------------------------
+
+void esqpanel_device::device_stop()
+{
+ device_t::device_stop();
+
+ delete m_external_panel_server;
+ m_external_panel_server = nullptr;
}
void esqpanel_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr)
{
- device_serial_interface::device_timer(timer, id, param, ptr);
+ if (ESQPANEL_EXTERNAL_TIMER_ID == id)
+ {
+ check_external_panel_server();
+ }
+ else
+ {
+ device_serial_interface::device_timer(timer, id, param, ptr);
+ }
}
void esqpanel_device::rcv_complete() // Rx completed receiving byte
@@ -76,6 +488,7 @@ void esqpanel_device::rcv_complete() // Rx completed receiving byte
// if (data >= 0xe0) printf("Got %02x from motherboard (second %s)\n", data, m_bCalibSecondByte ? "yes" : "no");
send_to_display(data);
+ m_external_panel_server->send_to_all(data);
if (m_bCalibSecondByte)
{
@@ -107,14 +520,14 @@ void esqpanel_device::rcv_complete() // Rx completed receiving byte
// d Sounds
// e 0
// f Cart
-// int lightNumber = data & 0x3f;
+ int lightNumber = data & 0x3f;
// Light states:
// 0 = Off
// 2 = On
// 3 = Blinking
-// int lightState = (data & 0xc0) >> 6;
-
+ m_light_states[lightNumber] = (data & 0xc0) >> 6;
+
// TODO: do something with the button information!
// printf("Setting light %d to %s\n", lightNumber, lightState == 3 ? "Blink" : lightState == 2 ? "On" : "Off");
m_bButtonLightSecondByte = false;
@@ -193,6 +606,39 @@ void esqpanel_device::xmit_char(uint8_t data)
}
}
+void esqpanel_device::check_external_panel_server() {
+ while (m_external_panel_server->has_commands())
+ {
+ std::string command = m_external_panel_server->get_next_command();
+ int l = command.length();
+ if (l > 0) {
+ std::istringstream is(command);
+ char c;
+ is >> c;
+ if (c == 'B') {
+ // button
+ char ud;
+ is >> ud;
+ int button;
+ is >> button;
+ bool down = ud == 'D';
+ uint8_t sendme = (down ? 0x80 : 0) | (button & 0xff);
+ // printf("button %d %s : sending char to mainboard: %02x\n", button, down ? "down" : "up", sendme);
+ xmit_char(sendme);
+ xmit_char(0x00);
+ } else if (c == 'A') {
+ // analog value from ES5505 OTIS: 10 bits, left-aligned within 16 bits.
+ int channel, value;
+ is >> channel;
+ is >> value;
+ uint16_t analog_value = (value << 6);
+ // printf("analog: channel %d, value %d = %04x\n", channel, value, analog_value);
+ set_analog_value(channel, analog_value);
+ }
+ }
+ }
+}
+
void esqpanel_device::set_analog_value(offs_t offset, uint16_t value)
{
m_write_analog(offset, value);
@@ -212,7 +658,7 @@ esqpanel1x22_device::esqpanel1x22_device(const machine_config &mconfig, const ch
m_eps_mode = true;
}
-/* panel with 2x40 VFD display used in the ESQ-1, VFX-SD, SD-1, and others */
+/* panel with 2x40 VFD display used in the ESQ-1, SQ-80 */
MACHINE_CONFIG_MEMBER(esqpanel2x40_device::device_add_mconfig)
MCFG_ESQ2X40_ADD("vfd")
@@ -226,6 +672,32 @@ esqpanel2x40_device::esqpanel2x40_device(const machine_config &mconfig, const ch
m_eps_mode = false;
}
+/* panel with 2x40 VFD display used in the VFX, VFX-SD, SD-1 series */
+
+MACHINE_CONFIG_MEMBER(esqpanel2x40_vfx_device::device_add_mconfig)
+ MCFG_ESQ2X40_ADD("vfd")
+MACHINE_CONFIG_END
+
+esqpanel2x40_vfx_device::esqpanel2x40_vfx_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) :
+ esqpanel_device(mconfig, ESQPANEL2X40, tag, owner, clock),
+ m_vfd(*this, "vfd")
+{
+ m_eps_mode = false;
+}
+
+bool esqpanel2x40_vfx_device::write_contents(std::ostream &o)
+{
+ m_vfd->write_contents(o);
+ for (int i = 0; i < m_light_states.size(); i++)
+ {
+ o.put(0xff);
+ o.put((m_light_states[i] << 6) | i);
+ }
+ return true;
+}
+
+
+
// --- SQ1 - Parduz --------------------------------------------------------------------------------------------------------------------------
MACHINE_CONFIG_MEMBER(esqpanel2x16_sq1_device::device_add_mconfig)
MCFG_ESQ2X16_SQ1_ADD("vfd")
diff --git a/src/mame/machine/esqpanel.h b/src/mame/machine/esqpanel.h
index 4886da2a1a2..7557bf9494e 100644
--- a/src/mame/machine/esqpanel.h
+++ b/src/mame/machine/esqpanel.h
@@ -8,6 +8,8 @@
#include "machine/esqvfd.h"
#include "machine/esqlcd.h"
+#include <vector>
+
//**************************************************************************
// INTERFACE CONFIGURATION MACROS
//**************************************************************************
@@ -27,7 +29,16 @@
#define MCFG_ESQPANEL2X40_REPLACE(_tag) \
MCFG_DEVICE_REPLACE(_tag, ESQPANEL2X40, 0)
-#define MCFG_ESQPANEL_2X40_REMOVE(_tag) \
+#define MCFG_ESQPANEL2X40_REMOVE(_tag) \
+ MCFG_DEVICE_REMOVE(_tag)
+
+#define MCFG_ESQPANEL2X40_VFX_ADD(_tag) \
+ MCFG_DEVICE_ADD(_tag, ESQPANEL2X40_VFX, 0)
+
+#define MCFG_ESQPANEL2X40_VFX_REPLACE(_tag) \
+ MCFG_DEVICE_REPLACE(_tag, ESQPANEL2X40_VFX, 0)
+
+#define MCFG_ESQPANEL2X40_VFX_REMOVE(_tag) \
MCFG_DEVICE_REMOVE(_tag)
#define MCFG_ESQPANEL2X16_SQ1_ADD(_tag) \
@@ -51,11 +62,20 @@
// ======================> esqpanel_device
+class esqpanel_external_panel_server;
+
class esqpanel_device : public device_t, public device_serial_interface
{
public:
- template <class Object> static devcb_base &set_tx_wr_callback(device_t &device, Object &&cb) { return downcast<esqpanel_device &>(device).m_write_tx.set_callback(std::forward<Object>(cb)); }
- template <class Object> static devcb_base &set_analog_wr_callback(device_t &device, Object &&cb) { return downcast<esqpanel_device &>(device).m_write_analog.set_callback(std::forward<Object>(cb)); }
+ template <class Object>
+ static devcb_base &set_tx_wr_callback(device_t &device, Object &&cb) {
+ return downcast<esqpanel_device &>(device).m_write_tx.set_callback(std::forward<Object>(cb));
+ }
+
+ template <class Object>
+ static devcb_base &set_analog_wr_callback(device_t &device, Object &&cb) {
+ return downcast<esqpanel_device &>(device).m_write_analog.set_callback(std::forward<Object>(cb));
+ }
void xmit_char(uint8_t data);
void set_analog_value(offs_t offset, uint16_t value);
@@ -67,6 +87,7 @@ protected:
// device-level overrides
virtual void device_start() override;
virtual void device_reset() override;
+ virtual void device_stop() override;
virtual void device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr) override;
// serial overrides
@@ -76,7 +97,17 @@ protected:
virtual void send_to_display(uint8_t data) = 0;
- bool m_eps_mode;
+ void check_external_panel_server();
+
+ virtual const std::string get_front_panel_html_file() const { return ""; }
+ virtual const std::string get_front_panel_js_file() const { return ""; }
+ virtual bool write_contents(std::ostream &o) { return false; }
+
+ std::vector<uint8_t> m_light_states;
+
+ bool m_eps_mode;
+
+ esqpanel_external_panel_server *m_external_panel_server;
private:
static const int XMIT_RING_SIZE = 16;
@@ -89,6 +120,8 @@ private:
uint8_t m_xmitring[XMIT_RING_SIZE];
int m_xmit_read, m_xmit_write;
bool m_tx_busy;
+
+ emu_timer *m_external_timer;
};
class esqpanel1x22_device : public esqpanel_device {
@@ -115,6 +148,26 @@ protected:
required_device<esq2x40_device> m_vfd;
};
+class esqpanel2x40_vfx_device : public esqpanel_device {
+public:
+ esqpanel2x40_vfx_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock);
+
+protected:
+ virtual void device_add_mconfig(machine_config &config) override;
+
+ virtual void send_to_display(uint8_t data) override { m_vfd->write_char(data); }
+
+ virtual const std::string get_front_panel_html_file() const override { return "/esqpanel/vfx/FrontPanel.html"; }
+ virtual const std::string get_front_panel_js_file() const override { return "/esqpanel/vfx/FrontPanel.js"; }
+ virtual bool write_contents(std::ostream &o) override;
+
+ required_device<esq2x40_device> m_vfd;
+
+private:
+ static const char *html;
+ static const char *js;
+};
+
class esqpanel2x40_sq1_device : public esqpanel_device {
public:
esqpanel2x40_sq1_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock);
@@ -142,6 +195,7 @@ protected:
DECLARE_DEVICE_TYPE(ESQPANEL1X22, esqpanel1x22_device)
DECLARE_DEVICE_TYPE(ESQPANEL2X40, esqpanel2x40_device)
+DECLARE_DEVICE_TYPE(ESQPANEL2X40_VFX, esqpanel2x40_vfx_device)
DECLARE_DEVICE_TYPE(ESQPANEL2X40_SQ1, esqpanel2x40_sq1_device)
DECLARE_DEVICE_TYPE(ESQPANEL2X16_SQ1, esqpanel2x16_sq1_device)
diff --git a/src/mame/machine/esqvfd.cpp b/src/mame/machine/esqvfd.cpp
index dd627f2b3fb..57965118432 100644
--- a/src/mame/machine/esqvfd.cpp
+++ b/src/mame/machine/esqvfd.cpp
@@ -225,13 +225,17 @@ void esq2x40_device::write_char(int data)
switch (data)
{
case 0xd0: // blink start
- m_curattr = AT_BLINK;
+ m_curattr |= AT_BLINK;
break;
case 0xd1: // blink stop (cancel all attribs on VFX+)
m_curattr = 0; //&= ~AT_BLINK;
break;
+ case 0xd2: // blinking underline on VFX
+ m_curattr |= AT_BLINK | AT_UNDERLINE;
+ break;
+
case 0xd3: // start underline
m_curattr |= AT_UNDERLINE;
break;
@@ -278,6 +282,41 @@ void esq2x40_device::write_char(int data)
update_display();
}
+bool esq2x40_device::write_contents(std::ostream &o)
+{
+ o.put((char) 0xd6); // clear screen
+
+ uint8_t attrs = 0;
+ for (int row = 0; row < 2; row++)
+ {
+ o.put((char) (0x80 + (40 * row))); // move to first column this row
+
+ for (int col = 0; col < 40; col++)
+ {
+ if (m_attrs[row][col] != attrs)
+ {
+ attrs = m_attrs[row][col];
+
+ o.put((char) 0xd1); // all attributes off
+
+ if (attrs & AT_BLINK)
+ {
+ o.put((char) 0xd0); // blink on
+ }
+
+ if (attrs & AT_UNDERLINE)
+ {
+ o.put((char) 0xd3); // underline
+ }
+ }
+
+ o.put((char) (m_chars[row][col] + ' '));
+ }
+ }
+ return true;
+}
+
+
esq2x40_device::esq2x40_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : esqvfd_device(mconfig, ESQ2X40, tag, owner, clock)
{
m_rows = 2;
diff --git a/src/mame/machine/esqvfd.h b/src/mame/machine/esqvfd.h
index 99e1e6c00eb..3b07b013d91 100644
--- a/src/mame/machine/esqvfd.h
+++ b/src/mame/machine/esqvfd.h
@@ -28,7 +28,8 @@ public:
virtual void write_char(int data) = 0;
virtual void update_display();
-
+ virtual bool write_contents(std::ostream &o) { return false; }
+
uint32_t conv_segments(uint16_t segin);
protected:
@@ -71,6 +72,7 @@ public:
esq2x40_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock);
virtual void write_char(int data) override;
+ virtual bool write_contents(std::ostream &o) override;
protected:
virtual void device_add_mconfig(machine_config &config) override;
diff --git a/web/esqpanel/vfx/FrontPanel.html b/web/esqpanel/vfx/FrontPanel.html
new file mode 100644
index 00000000000..bdf99c78167
--- /dev/null
+++ b/web/esqpanel/vfx/FrontPanel.html
@@ -0,0 +1,25 @@
+<html>
+<head>
+ <title>Front Panel</title>
+</head>
+<body onload="prepare();" bgcolor="black" class="notranslate"">
+
+</body>
+
+<script language="javascript" src="FrontPanel.js"></script>
+<script language="javascript">
+function wsroot() {
+ var l = window.location.href;
+ return (l.startsWith("https") ? "wss" : "ws") + l.substring(l.indexOf(':'), l.lastIndexOf('/')) + "/socket";
+}
+
+function prepare() {
+ var panelUrl = wsroot();
+ var panel = new fp.Panel(panelUrl, '$keyboard$', $version$);
+ document.body.appendChild(panel.container);
+ panel.display.clear();
+ panel.display.showString(0, 0, "Waiting to connect...");
+}
+</script>
+
+</html> \ No newline at end of file
diff --git a/web/esqpanel/vfx/FrontPanel.js b/web/esqpanel/vfx/FrontPanel.js
new file mode 100644
index 00000000000..69e1b67ac8f
--- /dev/null
+++ b/web/esqpanel/vfx/FrontPanel.js
@@ -0,0 +1,1209 @@
+var fp = (function() {
+ var my = {};
+
+ if ('indexOf' in Array.prototype) {
+ my.indexOf = function(array, item) {
+ return array.indexOf(item);
+ }
+ } else {
+ my.indexOf = function(array, item) {
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] == item) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ }
+
+ my.Shade = {
+ LIGHT: "#bbbbbb",
+ MEDIUM: "#777777",
+ DARK: "#333333"
+ };
+
+ my.LabelPosition = {
+ ABOVE: 1,
+ ABOVE_CENTERED: 2,
+ BELOW: 3
+ };
+
+ my.Light = {
+ OFF: 0,
+ ON: 1,
+ BLINK: 2
+ };
+
+ my.DisplayBlinkState = {
+ OFF: 0,
+ UNDERLINE: 1,
+ CHAR: 2
+ };
+
+ my.Keyboard = {
+ VFX: 'VFX',
+ VFX_SD: 'VFX-SD',
+ SD1: 'SD-1',
+ SD1_32: 'SD-1/32'
+ }
+
+ my.segmentPaths = [
+ "M1053 705 c-43 19 -57 47 -43 89 23 70 87 106 189 106 38 0 70 8 106 25 79 39 111 41 183 11 80 -34 119 -33 205 6 68 31 78 33 192 33 116 0 123 -1 195 -35 67 -31 87 -35 182 -40 101 -5 108 -7 137 -34 40 -38 50 -89 25 -118 -11 -11 -37 -29 -59 -39 -37 -17 -79 -19 -660 -18 -505 0 -626 2 -652 14z",
+ "M2519 963 c-20 13 -46 47 -63 81 -28 53 -31 69 -37 199 -7 155 -20 211 -75 319 -50 99 -68 199 -54 301 23 167 52 217 126 217 37 0 47 -5 77 -40 53 -63 74 -151 97 -410 5 -63 16 -167 24 -230 42 -326 45 -374 21 -419 -24 -47 -63 -54 -116 -18z",
+ "M2144 1089 c-59 43 -88 78 -135 161 -23 41 -75 112 -115 156 -108 119 -132 188 -136 386 -3 107 -1 118 17 132 11 9 28 16 37 16 25 0 92 -63 154 -145 29 -39 100 -129 158 -200 58 -72 113 -144 121 -162 19 -40 32 -106 41 -214 5 -68 3 -91 -9 -116 -28 -52 -74 -57 -133 -14z",
+ "M1515 1089 c-70 43 -69 41 -77 285 -3 121 -11 259 -18 306 -6 47 -13 142 -17 211 -6 141 5 183 54 195 78 20 124 -53 135 -216 13 -192 26 -274 61 -385 77 -245 76 -359 -3 -400 -39 -20 -99 -19 -135 4z",
+ "M1108 1087 c-32 36 -42 71 -50 163 -5 52 -11 122 -14 156 -6 55 -1 75 41 200 53 152 59 165 87 183 16 10 24 9 44 -4 31 -20 43 -51 55 -135 5 -36 17 -97 26 -137 14 -63 15 -81 3 -145 -37 -205 -43 -222 -88 -271 -30 -32 -80 -37 -104 -10z",
+ "M797 938 c-32 36 -44 102 -67 377 -19 222 -30 337 -42 428 -17 138 12 277 67 313 55 36 123 -6 173 -109 52 -106 54 -167 12 -292 -27 -78 -30 -102 -30 -205 0 -79 7 -147 20 -210 11 -51 20 -111 20 -133 0 -123 -97 -231 -153 -169z",
+ "M1940 2120 c-14 4 -56 8 -94 9 -80 1 -141 26 -181 73 -32 38 -32 78 1 118 48 56 84 67 249 74 146 7 151 7 195 -17 52 -27 99 -89 100 -130 0 -33 -31 -81 -63 -98 -27 -15 -125 -38 -157 -38 -14 1 -36 5 -50 9z",
+ "M1099 2129 c-51 10 -110 43 -132 73 -28 37 -16 88 32 138 36 38 41 40 95 40 64 0 115 -22 159 -68 32 -34 46 -97 28 -130 -23 -43 -109 -68 -182 -53z",
+ "M2279 2467 c-56 50 -69 80 -80 186 -6 51 -16 127 -24 169 -14 83 -10 123 25 213 36 95 44 146 31 203 -14 66 -14 205 -1 254 12 41 70 98 100 98 52 0 75 -100 100 -435 6 -77 22 -241 36 -364 28 -255 27 -268 -37 -325 -54 -49 -94 -49 -150 1z",
+ "M1701 2579 c-24 24 -40 122 -44 261 -2 95 1 112 27 178 15 41 44 94 63 119 19 25 57 92 84 149 58 121 94 164 137 164 38 0 78 -32 90 -73 19 -60 22 -181 7 -238 -20 -77 -116 -277 -180 -376 -30 -46 -66 -106 -80 -133 -35 -69 -69 -86 -104 -51z",
+ "M1372 2456 c-40 28 -52 66 -52 166 0 92 -27 323 -55 468 -21 108 -19 246 3 290 21 44 59 65 96 56 47 -12 123 -92 146 -152 28 -77 28 -203 -1 -281 -21 -55 -22 -62 -10 -218 6 -88 14 -178 16 -201 6 -54 -11 -110 -38 -129 -28 -19 -76 -19 -105 1z",
+ "M1067 2721 c-19 11 -122 161 -156 228 -42 81 -51 129 -51 276 0 113 3 147 18 175 39 80 102 35 199 -141 28 -52 56 -112 62 -134 6 -22 11 -114 11 -205 0 -134 -3 -170 -16 -188 -16 -24 -39 -27 -67 -11z",
+ "M695 2447 c-45 23 -76 54 -91 90 -8 18 -18 101 -24 190 -18 298 -21 328 -52 516 -26 164 -29 194 -18 235 23 91 68 107 130 44 46 -45 59 -86 71 -217 5 -55 13 -143 18 -195 11 -120 37 -199 101 -302 48 -78 50 -85 50 -153 0 -95 -15 -143 -60 -187 -42 -42 -75 -48 -125 -21z",
+ "M1550 3539 c-14 5 -57 24 -97 44 -107 54 -134 56 -218 12 -79 -42 -105 -41 -170 3 -35 23 -53 28 -145 33 -131 8 -181 24 -194 62 -14 39 9 78 54 94 49 17 1278 18 1315 1 51 -23 42 -87 -18 -132 -21 -15 -48 -21 -115 -26 -77 -4 -94 -9 -140 -38 -85 -55 -195 -76 -272 -53z",
+ "M2619 3393 c-19 12 -45 43 -59 67 -36 65 -36 183 0 255 48 93 136 107 207 33 60 -61 76 -152 48 -257 -17 -63 -45 -97 -94 -111 -52 -14 -64 -13 -102 13z",
+ "M512 4422 c-38 8 -46 15 -63 51 -37 83 -18 153 51 181 36 14 127 16 863 16 642 0 827 -3 847 -13 16 -8 31 -31 44 -64 16 -46 17 -57 5 -94 -8 -24 -26 -51 -42 -63 -28 -21 -34 -21 -845 -23 -501 0 -834 3 -860 9z",
+ ];
+ my.charWidth = 342;
+ my.charHeight = 572;
+ my.segmentScale = 0.1;
+
+ my.createElement = function(tag) {
+ return document.createElementNS("http://www.w3.org/2000/svg", tag);
+ }
+
+ my.showElement = function(e) {
+ e.removeAttribute("display");
+ }
+
+ my.hideElement = function(e) {
+ e.setAttribute("display", "none");
+ }
+
+ my.svg = function() {
+ if (my._svg == null) {
+ my._svg = document.getElementsByTagName('svg')[0];
+ }
+ return my._svg;
+ }
+
+ my.pt = function() {
+ if (my._pt == null) {
+ my._pt = my.svg().createSVGPoint();
+ }
+ return my._pt;
+ }
+
+ my.pointIn = function(el, x, y) {
+ var pt = my.pt();
+ pt.x = x; pt.y = y;
+ return pt.matrixTransform(el.getScreenCTM().inverse());
+ }
+
+ my.Display = function(parent, rows, cols) {
+ this.cells = new Array();
+ this.width = my.charWidth * cols;
+ this.height = my.charHeight * rows;
+ this.blinkPhase = true;
+
+ var templateCell = my.createElement("g");
+ templateCell.setAttribute('transform', 'scale(' + my.segmentScale + ',' + my.segmentScale + ')');
+ for (var i = 0; i < my.segmentPaths.length; i++) {
+ var segmentPath = my.createElement("path");
+ segmentPath.setAttribute('d', my.segmentPaths[i]);
+ templateCell.appendChild(segmentPath);
+ }
+
+ for (var row = 0; row < 2; row++) {
+ this.cells[row] = new Array();
+ for (var col = 0; col < 40; col++) {
+ this.cells[row][col] = {
+ char: ' ',
+ blink: false,
+ underline: false,
+ segments: new Array(),
+ };
+ var charCell = templateCell.cloneNode(true);
+ var ctm = "translate(" + col * my.charWidth + ", " + row * my.charHeight + ") " + charCell.getAttribute("transform");
+ charCell.setAttribute("transform", ctm);
+ parent.appendChild(charCell);
+
+ var segs = charCell.getElementsByTagName("path");
+ for (var cc = 0; cc < segs.length; cc++) {
+ this.cells[row][col].segments[cc] = segs[cc];
+ }
+ }
+ }
+
+ parent.setAttribute("viewBox", "0 0 " + this.width + " " + this.height);
+ }
+
+ my.Display.segmentsByCharacter = [
+ 0x0000, // 0000 0000 0000 0000 SPACE
+ 0x7927, // 0011 1001 0010 0111 '0.'
+ 0x0028, // 0000 0000 0010 1000 '"'
+ 0x4408, // 0000 0100 0000 1000 '1.'
+ 0x25e9, // 0010 0101 1110 1001 '$'
+ 0x70c3, // 0011 0000 1100 0011 '2.'
+ 0x0000, // 0000 0000 0000 0000 '&'
+ 0x0010, // 0000 0000 0001 0000 '''
+ 0x61c3, // 0010 0001 1100 0011 '3.'
+ 0x41e2, // 0000 0001 1110 0010 '4.'
+ 0x0edc, // 0000 1110 1101 1100 '*'
+ 0x04c8, // 0000 0100 1100 1000 '+'
+ 0x0000, // 0000 0000 0000 0000 ','
+ 0x00c0, // 0000 0000 1100 0000 '-'
+ 0x4000, // 0100 0000 0000 0000 '.'
+ 0x0804, // 0000 1000 0000 0100 '/'
+ 0x3927, // 0011 1001 0010 0111 '0'
+ 0x0408, // 0000 0100 0000 1000 '1'
+ 0x30c3, // 0011 0000 1100 0011 '2'
+ 0x21c3, // 0010 0001 1100 0011 '3'
+ 0x01e2, // 0000 0001 1110 0010 '4'
+ 0x21e1, // 0010 0001 1110 0001 '5'
+ 0x31e1, // 0011 0001 1110 0001 '6'
+ 0x0103, // 0000 0001 0000 0011 '7'
+ 0x31e3, // 0011 0001 1110 0011 '8'
+ 0x21e3, // 0010 0001 1110 0011 '9'
+ 0x0000, // 0000 0000 0000 0000 ':'
+ 0x71e1, // 0011 0001 1110 0001 '6.'
+ 0x0204, // 0000 0010 0000 0100 '('
+ 0x20c0, // 0010 0000 1100 0000 '='
+ 0x0810, // 0000 1000 0001 0000 ')'
+ 0x0000, // 0000 0000 0000 0000 '?'
+ 0x3583, // 0011 0101 1000 0011 '@'
+ 0x11e3, // 0001 0001 1110 0011 'A'
+ 0x254b, // 0010 0101 0100 1011 'B'
+ 0x3021, // 0011 0000 0010 0001 'C'
+ 0x250b, // 0010 0101 0000 1011 'D'
+ 0x30e1, // 0011 0000 1110 0001 'E'
+ 0x10e1, // 0001 0000 1110 0001 'F'
+ 0x3161, // 0011 0001 0110 0001 'G'
+ 0x11e2, // 0001 0001 1110 0010 'H'
+ 0x2409, // 0010 0100 0000 1001 'I'
+ 0x3102, // 0011 0001 0000 0010 'J'
+ 0x12a4, // 0001 0010 1010 0100 'K'
+ 0x3020, // 0011 0000 0010 0000 'L'
+ 0x1136, // 0001 0001 0011 0110 'M'
+ 0x1332, // 0001 0011 0011 0010 'N'
+ 0x3123, // 0011 0001 0010 0011 'O'
+ 0x10e3, // 0001 0000 1110 0011 'P'
+ 0x3323, // 0011 0011 0010 0011 'Q'
+ 0x12e3, // 0001 0010 1110 0011 'R'
+ 0x21e1, // 0010 0001 1110 0001 'S'
+ 0x0409, // 0000 0100 0000 1001 'T'
+ 0x3122, // 0011 0001 0010 0010 'U'
+ 0x1824, // 0001 1000 0010 0100 'V'
+ 0x1b22, // 0001 1011 0010 0010 'W'
+ 0x0a14, // 0000 1010 0001 0100 'X'
+ 0x0414, // 0000 0100 0001 0100 'Y'
+ 0x2805, // 0010 1000 0000 0101 'Z'
+ 0x3021, // 0011 0000 0010 0001 '['
+ 0x71e3, // 0011 0001 1110 0011 '8.'
+ 0x2103, // 0010 0001 0000 0011 ']'
+ 0x0a00, // 0000 1010 0000 0000 '^'
+ 0x2000, // 0010 0000 0000 0000 '_'
+ 0x0010, // 0000 0000 0001 0000 '`'
+ 0x11e3, // 0001 0001 1110 0011 'a'
+ 0x254b, // 0010 0101 0100 1011 'b'
+ 0x3021, // 0011 0000 0010 0001 'c'
+ 0x250b, // 0010 0101 0000 1011 'd'
+ 0x30e1, // 0011 0000 1110 0001 'e'
+ 0x10e1, // 0001 0000 1110 0001 'f'
+ 0x3161, // 0011 0001 0110 0001 'g'
+ 0x11e2, // 0001 0001 1110 0010 'h'
+ 0x2409, // 0010 0100 0000 1001 'i'
+ 0x3102, // 0011 0001 0000 0010 'j'
+ 0x12a4, // 0001 0010 1010 0100 'k'
+ 0x3020, // 0011 0000 0010 0000 'l'
+ 0x1136, // 0001 0001 0011 0110 'm'
+ 0x1332, // 0001 0011 0011 0010 'n'
+ 0x3123, // 0011 0001 0010 0011 'o'
+ 0x10e3, // 0001 0000 1110 0011 'p'
+ 0x3323, // 0011 0011 0010 0011 'q'
+ 0x12e3, // 0001 0010 1110 0011 'r'
+ 0x21e1, // 0010 0001 1110 0001 's'
+ 0x0409, // 0000 0100 0000 1001 't'
+ 0x3122, // 0011 0001 0010 0010 'u'
+ 0x1824, // 0001 1000 0010 0100 'v'
+ 0x1b22, // 0001 1011 0010 0010 'w'
+ 0x0a14, // 0000 1010 0001 0100 'x'
+ 0x0414, // 0000 0100 0001 0100 'y'
+ 0x2805, // 0010 1000 0000 0101 'z'
+ 0x3021, // 0011 0000 0010 0001 '{'
+ 0x0408, // 0000 0100 0000 1000 '|'
+ 0x2103, // 0010 0001 0000 0011 '}'
+ 0x0a00, // 0000 1010 0000 0000 '~'
+ 0x0000, // 0000 0000 0000 0000 DEL
+ ];
+
+ my.Display.colorOn = "#00ffbb";
+ my.Display.colorOff = "#002211";
+ my.Display.overdraw = 0;
+
+ my.Display.prototype.showSegments = function(segments, lit) {
+ // debugger;
+ var mask = 1;
+ var i;
+ for (i = 0; i < 16; i++) {
+ var on = (lit & mask) != 0;
+ segments[i].setAttribute("fill", on ? my.Display.colorOn : my.Display.colorOff);
+ if (my.Display.overdraw) {
+ segments[i].setAttribute("stroke-width", my.Display.overdraw);
+ if (on) {
+ segments[i].setAttribute("stroke", my.Display.colorOn);
+ } else {
+ segments[i].setAttribute("stroke", "none");
+ }
+ } else {
+ segments[i].setAttribute("stroke", "none");
+ }
+ mask <<= 1;
+ }
+ }
+
+ my.Display.segmentsForCharacter = function(c, underline, blink, blinkPhase) {
+ var lit = (c < 32 || 127 < c) ? 0 : my.Display.segmentsByCharacter[c - 32];
+ if (blink && !blinkPhase) {
+ if (underline) {
+ return lit;
+ } else {
+ return 0;
+ }
+ } else {
+ if (underline) {
+ return lit | 0x8000;
+ } else {
+ return lit;
+ }
+ }
+ }
+
+ my.Display.prototype.setChar = function(y, x, c, underline, blink) {
+ var cell = this.cells[y][x];
+ cell.char = c;
+ cell.underline = underline;
+ cell.blink = blink;
+
+ this.showSegments(cell.segments, my.Display.segmentsForCharacter(c, underline, blink, this.blinkPhase));
+ }
+
+ my.Display.prototype.showString = function(y, x, s) {
+ for (var i = 0; i < s.length; i++) {
+ this.setChar(y, x, s.charCodeAt(i), false, false);
+ x++;
+ if (x >= this.cells[y].length) {
+ x = 0;
+ y++;
+ }
+ if (y >= this.cells.length) {
+ y = 0;
+ }
+ }
+ }
+
+ my.Display.prototype.clear = function() {
+ for (var row = 0; row < this.cells.length; row++) {
+ var line = this.cells[row];
+ for (var col = 0; col < line.length; col++) {
+ this.setChar(row, col, ' ', false, false);
+ }
+ }
+ }
+
+ my.Display.prototype.blink = function(y, x) {
+ return this.cells[y][x].blink;
+ }
+
+ my.Display.prototype.underline = function(y, x) {
+ return this.cells[y][x].underline;
+ }
+
+ my.Display.prototype.setBlinkPhase = function(phase) {
+ this.blinkPhase = phase;
+ for (var row = 0; row < this.cells.length; row++) {
+ var line = this.cells[row];
+ for (var col = 0; col < line.length; col++) {
+ var cell = line[col];
+ if (cell.blink) {
+ this.showSegments(cell.segments,
+ my.Display.segmentsForCharacter(cell.char, cell.underline, cell.blink, this.blinkPhase));
+ }
+ }
+ }
+ }
+
+ my.Rect = function(x, y, w, h) {
+ this.x = x;
+ this.y = y;
+ this.w = w;
+ this.h = h;
+ }
+
+ my.Rect.prototype.union = function(other) {
+ if (this.w == 0 || this.h == 0) {
+ return other;
+ } else if (other.w == 0 || other.h == 0) {
+ return this;
+ } else {
+ minX = Math.min(this.x, other.x);
+ maxX = Math.max(this.x+this.w, other.x+other.w);
+ minY = Math.min(this.y, other.y);
+ maxY = Math.max(this.y+this.h, other.y+other.h);
+ return new my.Rect(minX, minY, maxX-minX, maxY-minY);
+ }
+ }
+
+ my.Rect.prototype.inset = function(dx, dy) {
+ return new my.Rect(this.x + dx, this.y + dy, this.w - 2*dx, this.h - 2*dy);
+ }
+
+ my.Rect.prototype.offset = function(dx, dy) {
+ return new my.Rect(this.x+dx, this.y+dy, this.w, this.h);
+ }
+
+ my.Rect.prototype.toPath = function(r) {
+ var rect = my.createElement("rect");
+ rect.setAttribute("x", this.x);
+ rect.setAttribute("y", this.y);
+ rect.setAttribute("width", this.w);
+ rect.setAttribute("height", this.h);
+ if (r != null) {
+ rect.setAttribute("rx", r);
+ rect.setAttribute("ry", r);
+ }
+ return rect;
+ }
+
+ my.Rect.prototype.getX = function(d) {
+ return this.x + d * this.w;
+ }
+
+ my.Rect.prototype.getY = function(d) {
+ return this.y + d * this.h;
+ }
+
+ my.displayRect = new my.Rect(15, 7, 82, 13);
+
+ my.Button = function(x, y, w, h, label, labelPosition, value, color, multiPage, lightId) {
+ var that = this;
+ this.rect = new my.Rect(x, y, w, h);
+
+ var rect = this.rect.inset(0.1, 0.1);
+ var translation = "translate(" + x + "," + y + ")";
+ this.halo = rect.toPath(0.5);
+ this.halo.setAttribute("stroke", "#666666");
+ this.halo.setAttribute("stroke-width", "2");
+ this.halo.setAttribute("fill", "none");
+ my.hideElement(this.halo);
+
+ rect = rect.offset(-x, -y);
+ this.outline = rect.toPath(0.5);
+ this.outline.setAttribute("fill", color);
+ this.outline.setAttribute("stroke", "none");
+
+ this.group = my.createElement("g");
+ this.group.setAttribute("transform", translation);
+ this.group.appendChild(this.outline);
+
+ this.label = label;
+ this.labelPosition = labelPosition;
+ this.value = value;
+ this.color = color;
+ this.multiPage = multiPage;
+ this.lightId = lightId;
+
+ if (label != undefined) {
+ var labelLines = label.split("\n");
+ var fontSize = 1.4;
+ var labelText = my.createElement("text");
+ labelText.setAttribute('fill', 'white');
+ labelText.setAttribute('stroke', 'none');
+ labelText.setAttribute('font-size', fontSize);
+ labelText.setAttribute('font-family', 'Helvetica');
+ labelText.setAttribute('font-style', 'italic');
+ labelText.setAttribute('width', w);
+ labelText.setAttribute('dominant-baseline', 'bottom');
+ var x0 = 0;
+ var y0 = (1 - labelLines.length) * fontSize;
+ switch(labelPosition) {
+ case my.LabelPosition.ABOVE_CENTERED:
+ labelText.setAttribute('text-anchor', 'middle');
+ x0 = w/2;
+ // fall through
+ case my.LabelPosition.ABOVE:
+ y0 = (1 - labelLines.length) * fontSize - 0.3;
+ break;
+ case my.LabelPosition.BELOW:
+ y0 = h + fontSize - 0.3;
+ break;
+ }
+ for (var i = 0; i < labelLines.length; i++) {
+ var tspan = my.createElement("tspan");
+ tspan.setAttribute('x', x0);
+ tspan.setAttribute('y', y0 + i * fontSize);
+ tspan.appendChild(document.createTextNode(labelLines[i]));
+ labelText.appendChild(tspan);
+ }
+ this.group.appendChild(labelText);
+ }
+
+ if (lightId >= 0) {
+ this.lightOn = my.createElement("path");
+ this.lightOn.setAttribute("d", "M" + (rect.w/3) + "," + (rect.y+rect.h/25) + " h" + (rect.w/3) + " v" + (rect.h/3) + " h" + (-rect.w/3) + " z");
+ this.lightOff = this.lightOn.cloneNode(true);
+ this.lightOn.setAttribute("fill", "#22ff22");
+ this.lightOff.setAttribute("fill", "#112211");
+ my.hideElement(this.lightOn);
+
+ this.group.appendChild(this.lightOn);
+ this.group.appendChild(this.lightOff);
+ }
+
+ this.group.addEventListener("touchstart", function(e) { that.press(e); }, true);
+ this.group.addEventListener("touchend", function(e) { that.release(e); }, true);
+ this.group.addEventListener("mousedown", function(e) { that.press(e); }, true);
+ this.group.addEventListener("mouseout", function(e) { that.release(e); }, true);
+ this.group.addEventListener("mouseup", function(e) { that.release(e); }, true);
+
+ this.isPressed = false;
+ this.lightState = my.Light.OFF;
+ this.lightIsOn = false;
+ this.blinkPhase = true;
+
+ this.onPress = undefined;
+ this.onRelease = undefined;
+ }
+
+ my.Button.prototype.showPressed = function(isPressed) {
+ if (isPressed) {
+ my.showElement(this.halo);
+ } else {
+ my.hideElement(this.halo);
+ }
+ }
+
+ my.Button.prototype.press = function(e) {
+ e.preventDefault();
+
+ if (!this.isPressed) {
+ this.isPressed = true;
+ this.showPressed(true);
+
+ if (this.onPress != undefined) {
+ this.onPress(this);
+ }
+ }
+
+ return false;
+ }
+
+ my.Button.prototype.release = function(e) {
+ e.preventDefault();
+
+ if (this.isPressed) {
+ this.isPressed = false;
+ this.showPressed(false);
+
+ if (this.onRelease != undefined) {
+ this.onRelease(this);
+ }
+ }
+
+ return false;
+ }
+
+ my.Button.prototype.updateLight = function() {
+ var on = this.lightState == my.Light.ON || (this.blinkPhase && this.lightState == my.Light.BLINK);
+ if (on != this.lightIsOn) {
+ my.hideElement(this.lightIsOn ? this.lightOn : this.lightOff);
+ this.lightIsOn = on;
+ my.showElement(this.lightIsOn ? this.lightOn : this.lightOff);
+ }
+ }
+
+ my.Button.prototype.setLight = function(state) {
+ this.lightState = state;
+ this.updateLight();
+ }
+
+ my.Button.prototype.setBlinkPhase = function(phase) {
+ this.blinkPhase = phase;
+ this.updateLight();
+ }
+
+ my.Touch = function(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ my.makeTouch = function(e) {
+ return new my.Touch(e.clientX, e.clientY);
+ }
+
+ my.Slider = function(x, y, w, h, channel, value) {
+
+ function makeRectPath(x, y, w, h, color) {
+ path = new my.Rect(x, y, w, h).toPath();
+ path.setAttribute("fill", color);
+ return path;
+ }
+
+ var that = this;
+ this.channel = channel;
+ this.value = value;
+
+ this.rect = new my.Rect(x, y, w, h);
+ var rect = this.rect.offset(-x, -y);
+ var translation = "translate(" + x + "," + y + ")";
+ this.group = my.createElement("g");
+ this.group.setAttribute("transform", translation);
+
+ this.frameColor = "#333333";
+ this.frameActiveColor = "#666666";
+ this.frame = rect.inset(0.25, 0.25).toPath();
+ this.frame.setAttribute("stroke", this.frameColor);
+ this.frame.setAttribute("stroke-width", "0.5");
+ this.group.appendChild(this.frame);
+
+ this.handleX = 0.75;
+ this.handleW = w - 1.5;
+ this.handleH = 4;
+ this.handleMinY = 0.75;
+ this.handleMaxY = h - 0.75 - this.handleH;
+
+ this.handle = my.createElement("g");
+ this.handle.appendChild(makeRectPath(0, 0, this.handleW, this.handleH, "#333333"));
+ this.handle.appendChild(makeRectPath(0, 0, this.handleW, 0.75, "#444444"));
+ this.handle.appendChild(makeRectPath(0, 1.75, this.handleW, 0.25, "#222222"));
+ this.handle.appendChild(makeRectPath(0, 2, this.handleW, 0.25, "#444444"));
+ this.handle.appendChild(makeRectPath(0, 3.25, this.handleW, 0.75, "#222222"));
+ this.group.appendChild(this.handle);
+
+ this.setValue(value);
+
+ this.handle.addEventListener("touchstart", function(e) { that.touchstart(e); }, true);
+ this.group.addEventListener("touchmove", function(e) { that.touchmove(e); }, true);
+ this.group.addEventListener("touchend", function(e) { that.touchend(e); }, true);
+ this.group.addEventListener("touchcancel", function(e) { that.touchend(e); }, true);
+
+ this.handle.addEventListener("mousedown", function(e) { that.grab(e.clientX, e.clientY); }, true);
+ this.group.addEventListener("mousemove", function(e) { that.drag(e.clientX, e.clientY); }, true);
+ this.group.addEventListener("mouseup", function(e) { that.release(); }, true);
+
+ this.onValueChanged = undefined;
+ this.isGrabbed = false;
+ this.activeTouches = new Map();
+
+ }
+
+ my.Slider.prototype.setValue = function(value) {
+ this.value = Math.max(0.0, Math.min((1.0, value)));
+ this.handleY = this.handleMinY + (1.0 - value) * (this.handleMaxY - this.handleMinY);
+ this.handle.setAttribute("transform", "translate(" + this.handleX + "," + this.handleY + ")");
+ }
+
+ my.Slider.prototype.setHandleY = function(handleY) {
+ this.handleY = Math.max(this.handleMinY, Math.min(this.handleMaxY, handleY));
+ // console.log("Setting handleY to " + handleY + " => " + this.handleY);
+ this.value = 1.0 - (this.handleY - this.handleMinY) / (this.handleMaxY - this.handleMinY);
+ this.handle.setAttribute("transform", "translate(" + this.handleX + "," + this.handleY + ")");
+ }
+
+ my.Slider.prototype.grab = function(x, y) {
+ this.isGrabbed = true;
+ this.frame.setAttribute("stroke", this.frameActiveColor);
+ var p = my.pointIn(this.group, x, y);
+ this.dragOffset = p.y - this.handleY;
+ // console.log("Grabbing with handleY=" + this.handleY + ", p.y=" + p.y + " => dragOffset=" + this.dragOffset);
+ }
+
+ my.Slider.prototype.drag = function(x, y) {
+ if (this.isGrabbed) {
+ var p = my.pointIn(this.group, x, y);
+ var newHandleY = p.y - this.dragOffset;
+ // console.log("Dragged with p.y=" + p.y + ", dragOffset=" + this.dragOffset + " => new handleY=" + newHandleY);
+ this.setHandleY(newHandleY);
+ if (this.onValueChanged != null) {
+ this.onValueChanged(this);
+ }
+ }
+ }
+
+ my.Slider.prototype.release = function(e) {
+ this.isGrabbed = false;
+ this.frame.setAttribute("stroke", this.frameColor);
+ }
+
+ my.Slider.prototype.activeTouchCenter = function() {
+ var n = this.activeTouches.size;
+ if (n <= 0) {
+ return undefined;
+ }
+ var x = 0;
+ var y = 0;
+
+ for (const touch of this.activeTouches.values()) {
+ x += touch.x;
+ y += touch.y;
+ }
+
+ return new my.Touch(x / n, y / n);
+ }
+
+ my.Slider.prototype.touchstart = function(e) {
+ e.preventDefault();
+
+ var wasEmpty = this.activeTouches.size == 0;
+ for (var i = 0; i < e.targetTouches.length; i++) {
+ var touch = e.targetTouches.item(i);
+ this.activeTouches.set(touch.identifier, my.makeTouch(touch));
+ }
+
+ center = this.activeTouchCenter();
+ if (center != null) {
+ this.grab(center.x, center.y);
+ }
+
+ return false;
+ }
+
+ my.Slider.prototype.touchmove = function(e) {
+ e.preventDefault();
+
+ for (var i = 0; i < e.changedTouches.length; i++) {
+ var touch = e.changedTouches.item(i);
+ if (this.activeTouches.has(touch.identifier)) {
+ this.activeTouches.set(touch.identifier, my.makeTouch(touch));
+ }
+ }
+ center = this.activeTouchCenter();
+ if (center != null) {
+ this.drag(center.x, center.y);
+ }
+
+ return false;
+ }
+
+ my.Slider.prototype.touchend = function(e) {
+ e.preventDefault();
+
+ for (var i = 0; i < e.changedTouches.length; i++) {
+ var touch = e.changedTouches.item(i);
+ this.activeTouches.delete(touch.identifier)
+ }
+ if (this.activeTouches.size == 0) {
+ this.release();
+ } else {
+ center = this.activeTouchCenter();
+ if (center != null) {
+ this.grab(center.x, center.y);
+ }
+ }
+
+ return false;
+ }
+
+ my.Panel = function(serverUrl, keyboard, version) {
+ this.serverUrl = serverUrl;
+ this.keyboard = keyboard;
+ this.version = version;
+
+ this.container = my.createElement("svg");
+ this.container.setAttribute("preserveAspectRatio", "xMidYMid meet");
+ this.container.setAttribute("width", "2000");
+ this.container.setAttribute("height", "375");
+ this.container.setAttribute("overflow", "scroll");
+
+ this.haloContainer = my.createElement("g");
+ this.container.appendChild(this.haloContainer);
+
+ this.mainContainer = my.createElement("g");
+ this.container.appendChild(this.mainContainer);
+
+ this.displayContainer = my.createElement("svg");
+ this.display = new my.Display(this.displayContainer, 2, 40);
+ this.displayContainer.setAttribute("preserveAspectRatio", "xMidYMid meet");
+ this.displayContainer.setAttribute("x", my.displayRect.x);
+ this.displayContainer.setAttribute("y", my.displayRect.y);
+ this.displayContainer.setAttribute("width", my.displayRect.w);
+ this.displayContainer.setAttribute("height", my.displayRect.h);
+ this.container.appendChild(this.displayContainer);
+
+ this.buttons = new Array();
+ this.lightButtons = new Array();
+ this.analogControls = new Array();
+ this.addControls(keyboard);
+
+ this.cursorX = 0;
+ this.cursorY = 0;
+ this.savedCursorX = 0;
+ this.savedCursorY = 0;
+ this.blink = false;
+ this.underline = false;
+
+ this.serverUrl = serverUrl;
+ try {
+ this.connect();
+ } catch (e) {
+ console.log("Unable to connect to '" + serverUrl + "': " + e);
+ }
+
+ var that = this;
+ this.blinkPhase = 0;
+ setInterval(function() { that.updateBlink(); }, 250);
+ }
+
+ my.Panel.prototype.updateBlink = function() {
+ this.blinkPhase = (this.blinkPhase + 1) % 4;
+ this.display.setBlinkPhase(this.blinkPhase < 2);
+ var buttonPhase = (this.blinkPhase % 2) == 0;
+ for (var i = 0; i < this.lightButtons.length; i++) {
+ this.lightButtons[i].setBlinkPhase(buttonPhase);
+ }
+ }
+
+ my.Panel.prototype.connect = function() {
+ var that = this;
+ var panel = this;
+ var reconnect = function() {
+ that.connect();
+ }
+
+ this.socket = new WebSocket(this.serverUrl);
+ this.socket.binaryType = "arraybuffer";
+
+ this.socket.onopen = function(event) {
+ console.log("opened: " + event);
+ panel.sendString("I"); // Request server information
+ };
+
+ this.socket.onmessage = function(event) {
+ var data = new Uint8Array(event.data);
+ var c = String.fromCharCode(data[0]);
+
+ if (c == 'A') {
+ console.log("handling analog value")
+ panel.handleAnalogValue(data.slice(1));
+ } else if (c == 'B') {
+ console.log("handling button state")
+ panel.handleButtonState(data.slice(1));
+ } else if (c == 'D') {
+ console.log("handling display data")
+ panel.handleDisplayData(data.slice(1));
+ } else if (c == 'I') {
+ console.log("handling server information");
+ panel.handleServerInformation(data.slice(1));
+ }
+ };
+
+ this.socket.onclose = function(event) {
+ console.log("closed: ", event);
+ // reconnect after 1 second
+ setTimeout(reconnect, 1000);
+ };
+
+ this.socket.onerror = function(event) {
+ console.log("error: ", event);
+ };
+ }
+
+ my.Panel.prototype.addButton = function(x, y, w, h, label, labelPosition, value, color, multiPage, lightId) {
+ var that = this;
+ var button = new my.Button(x, y, w, h, label, labelPosition, value, color, multiPage, lightId);
+ this.haloContainer.appendChild(button.halo);
+
+ if (lightId >= 0) {
+ if (lightId >= this.lightButtons.length) {
+ this.lightButtons.length = lightId + 1;
+ }
+ this.lightButtons[lightId] = button;
+ }
+
+ this.mainContainer.appendChild(button.group);
+ this.buttons[value] = button;
+
+ button.onPress = function(b) {
+ that.onButtonPressed(b);
+ }
+ button.onRelease = function(b) {
+ that.onButtonReleased(b);
+ }
+
+ return button;
+ }
+
+ my.Panel.prototype.addSlider = function(x, y, w, h, channel, value) {
+ var that = this;
+ var slider = new my.Slider(x, y, w, h, channel, value);
+
+ this.mainContainer.appendChild(slider.group);
+ this.analogControls[channel] = slider;
+
+ slider.onValueChanged = function(s) {
+ that.onAnalogValueChanged(s);
+ }
+
+ return slider;
+ }
+
+ my.Panel.prototype.addButtonBelowDisplay = function(x, y, label, value, shade) {
+ return this.addButton(x, y, 6, 4, label, my.LabelPosition.BELOW, value, shade, false, -1);
+ }
+
+ my.Panel.prototype.addButtonWithLightBelowDisplay = function(x, y, label, value, shade, lightId) {
+ return this.addButton(x, y, 6, 4, label, my.LabelPosition.BELOW, value, shade, false, lightId);
+ }
+
+ my.Panel.prototype.addLargeButton = function(x, y, label, value, shade) {
+ return this.addButton(x, y, 6, 4, label, my.LabelPosition.ABOVE, value, shade, false, -1);
+ }
+
+ my.Panel.prototype.addLargeButtonWithLight = function(x, y, label, value, shade, lightId) {
+ return this.addButton(x, y, 6, 4, label, my.LabelPosition.ABOVE, value, shade, false, lightId);
+ }
+
+ my.Panel.prototype.addSmallButton = function(x, y, label, value, shade, multiPage) {
+ return this.addButton(x, y, 6, 2, label, my.LabelPosition.ABOVE, value, shade, multiPage, -1);
+ }
+
+ my.Panel.prototype.addIncDecButton = function(x, y, label, value, shade, multiPage) {
+ return this.addButton(x, y, 6, 2, label, my.LabelPosition.ABOVE_CENTERED, value, shade, multiPage, -1);
+ }
+
+ my.Panel.prototype.addControls = function(keyboard) {
+ console.log("keyboard is '" + keyboard + "'");
+
+ // Normalize the keyboard string.
+ var hasSeq = false;
+ var hasBankSet = false;
+ keyboard = keyboard.toLowerCase();
+ if (keyboard.indexOf('sd') != -1) {
+ hasSeq = true;
+
+ if (keyboard.indexOf('1') != -1) {
+ hasBankSet = true;
+
+ if (keyboard.indexOf('32') != -1) {
+ keyboard = my.Keyboard.SD1_32;
+ } else {
+ keyboard = my.Keyboard.SD1;
+ }
+ } else {
+ keyboard = my.Keyboard.VFX_SD;
+ }
+ } else {
+ keyboard = my.Keyboard.VFX;
+ }
+
+ console.log("normalized keyboard is '" + keyboard + "'");
+
+ var cartString = hasBankSet ? "BankSet" : "Cart";
+
+ this.addButtonWithLightBelowDisplay(10, 29, cartString, 52, my.Shade.LIGHT, 0xf);
+ this.addButtonWithLightBelowDisplay(16, 29, "Sounds", 53, my.Shade.LIGHT, 0xd);
+ this.addButtonWithLightBelowDisplay(22, 29, "Presets", 54, my.Shade.LIGHT, 0x7);
+ if (hasSeq) {
+ this.addButtonBelowDisplay (28, 29, "Seq", 51, my.Shade.LIGHT);
+ }
+
+ this.addButtonWithLightBelowDisplay(42, 29, "0", 55, my.Shade.MEDIUM, 0xe);
+ this.addButtonWithLightBelowDisplay(48, 29, "1", 56, my.Shade.MEDIUM, 0x6);
+ this.addButtonWithLightBelowDisplay(54, 29, "2", 57, my.Shade.MEDIUM, 0x4);
+ this.addButtonWithLightBelowDisplay(60, 29, "3", 46, my.Shade.MEDIUM, 0xc);
+ this.addButtonWithLightBelowDisplay(66, 29, "4", 47, my.Shade.MEDIUM, 0x3);
+ this.addButtonWithLightBelowDisplay(72, 29, "5", 48, my.Shade.MEDIUM, 0xb);
+ this.addButtonWithLightBelowDisplay(78, 29, "6", 49, my.Shade.MEDIUM, 0x2);
+ this.addButtonWithLightBelowDisplay(84, 29, "7", 35, my.Shade.MEDIUM, 0xa);
+ this.addButtonWithLightBelowDisplay(90, 29, "8", 34, my.Shade.MEDIUM, 0x1);
+ this.addButtonWithLightBelowDisplay(96, 29, "9", 25, my.Shade.MEDIUM, 0x9);
+
+ // Large buttons on the main panel part
+ this.addLargeButton (108, 29, "Replace\nProgram", 29, my.Shade.MEDIUM);
+ this.addLargeButtonWithLight(114, 29, "1-6", 30, my.Shade.MEDIUM, 0x0);
+ this.addLargeButtonWithLight(120, 29, "7-12", 31, my.Shade.MEDIUM, 0x8);
+
+ this.addLargeButton (154, 29, "Select\nVoice", 5, my.Shade.MEDIUM);
+ this.addLargeButton (160, 29, "Copy", 9, my.Shade.MEDIUM);
+ this.addLargeButton (166, 29, "Write", 3, my.Shade.MEDIUM);
+ this.addLargeButtonWithLight(172, 29, "Compare", 8, my.Shade.MEDIUM, 0x5);
+
+ // Small buttons, main panel
+ // -- Performance:
+ this.addSmallButton(108, 20, "Patch\nSelect", 26, my.Shade.DARK, true);
+ this.addSmallButton(114, 20, "MIDI", 27, my.Shade.DARK, true);
+ this.addSmallButton(120, 20, "Effects", 28, my.Shade.DARK, true);
+
+ this.addSmallButton(108, 13, "Key\nZone", 39, my.Shade.DARK, false);
+ this.addSmallButton(114, 13, "Trans-\npose", 40, my.Shade.DARK, false);
+ this.addSmallButton(120, 13, "Release", 41, my.Shade.DARK, false);
+
+ this.addSmallButton(108, 6, "Volume", 36, my.Shade.DARK, false);
+ this.addSmallButton(114, 6, "Pan", 37, my.Shade.DARK, false);
+ this.addSmallButton(120, 6, "Timbre", 38, my.Shade.DARK, false);
+
+ // Sequencer / System, both large and small buttons:
+ if (hasSeq) {
+ // The 'Master', 'Storage' and 'MIDI Control' buttons are small & at the to,
+ // the sequencer buttons are big and at the bottom.
+ this.addLargeButton(131, 29, "Rec", 19, my.Shade.MEDIUM);
+ this.addLargeButton(137, 29, "Stop\n/Cont", 22, my.Shade.MEDIUM);
+ this.addLargeButton(143, 29, "Play", 23, my.Shade.MEDIUM);
+
+ this.addSmallButton(131, 20, "Click", 32, my.Shade.DARK, false);
+ this.addSmallButton(137, 20, "Seq\nControl", 18, my.Shade.DARK, true);
+ this.addSmallButton(143, 20, "Locate", 33, my.Shade.DARK, true);
+
+ this.addSmallButton(131, 13, "Song", 60, my.Shade.DARK, false);
+ this.addSmallButton(137, 13, "Seq", 59, my.Shade.DARK, false);
+ this.addSmallButton(143, 13, "Track", 61, my.Shade.DARK, false);
+
+ this.addSmallButton(131, 6, "Master", 20, my.Shade.LIGHT, true);
+ this.addSmallButton(137, 6, "Storage", 21, my.Shade.LIGHT, false);
+ this.addSmallButton(143, 6, "MIDI\nControl", 24, my.Shade.LIGHT, true);
+ } else {
+ // The 'Master', 'Storage' and 'MIDI Control' buttons are large & at the bottom,
+ // and there are no sequencer buttons
+ this.addLargeButton(131, 29, "Master", 20, my.Shade.LIGHT, true);
+ this.addLargeButton(137, 29, "Storage", 21, my.Shade.LIGHT, false);
+ this.addLargeButton(143, 29, "MIDI\nControl", 24, my.Shade.LIGHT, true);
+ }
+
+ // -- Programming:
+ this.addSmallButton(154, 20, "Wave", 4, my.Shade.DARK, false);
+ this.addSmallButton(160, 20, "Mod\nMixer", 6, my.Shade.DARK, false);
+ this.addSmallButton(166, 20, "Program\nControl", 2, my.Shade.DARK, false);
+ this.addSmallButton(172, 20, "Effects", 7, my.Shade.DARK, true);
+
+ this.addSmallButton(154, 13, "Pitch", 11, my.Shade.DARK, false);
+ this.addSmallButton(160, 13, "Pitch\nMod", 13, my.Shade.DARK, false);
+ this.addSmallButton(166, 13, "Filters", 15, my.Shade.DARK, true);
+ this.addSmallButton(172, 13, "Output", 17, my.Shade.DARK, true);
+
+ this.addSmallButton(154, 6, "LFO", 10, my.Shade.DARK, true);
+ this.addSmallButton(160, 6, "Env1", 12, my.Shade.DARK, true);
+ this.addSmallButton(166, 6, "Env2", 14, my.Shade.DARK, true);
+ this.addSmallButton(172, 6, "Env3", 16, my.Shade.DARK, true);
+
+ // Display buttons - approximate:
+ this.addSmallButton(32, 21, "", 50, my.Shade.DARK, false);
+ this.addSmallButton(57, 21, "", 44, my.Shade.DARK, false);
+ this.addSmallButton(82, 21, "", 45, my.Shade.DARK, false);
+
+ this.addSmallButton(32, 4, "", 58, my.Shade.DARK, false);
+ this.addSmallButton(57, 4, "", 42, my.Shade.DARK, false);
+ this.addSmallButton(82, 4, "", 43, my.Shade.DARK, false);
+
+ // Value slider
+ var valueSlider = this.addSlider(-2.75, 4, 7, 22, 3, 0.7);
+
+ // Increment and Decrement
+ this.addIncDecButton(-12.5, 22, "\u25BC", 63, my.Shade.DARK, false);
+ this.addIncDecButton(-12.5, 12, "\u25B2", 62, my.Shade.DARK, false);
+
+ // Volume slider
+ var volumeSlider = this.addSlider(-30, 4, 7, 22, 5, 1.0);
+
+ var r = undefined;
+ for (var i = 1; i < this.buttons.length; i++) {
+ var button = this.buttons[i];
+ if (button != null) {
+ if (r != null) {
+ r = r.union(button.rect);
+ } else {
+ r = button.rect;
+ }
+ }
+ }
+ r = r.union(valueSlider.rect);
+ r = r.union(volumeSlider.rect);
+
+ r.x -= 2;
+ r.y -= 2;
+ r.w += 4;
+ r.h += 4;
+
+ var viewBox = "" + r.x + " " + r.y + " " + r.w + " " + r.h;
+ this.container.setAttribute("viewBox", viewBox);
+ }
+
+ my.Panel.prototype.sendString = function(s) {
+ if (this.socket != undefined && this.socket.readyState == WebSocket.OPEN) {
+ var b = new Uint8Array(s.length);
+ for (var i = 0; i < s.length; i++) {
+ b[i] = s.charCodeAt(i);
+ }
+ this.socket.send(b);
+ }
+ }
+
+ my.Panel.prototype.onButtonPressed = function(button) {
+ this.sendString("BD " + button.value);
+ }
+
+ my.Panel.prototype.onButtonReleased = function(button) {
+ this.sendString("BU " + button.value);
+ }
+
+ my.Panel.prototype.onAnalogValueChanged = function(slider) {
+ // 0.05 == 0; 0.95 == 760
+ var value = (slider.value - 0.05) / 0.9;
+ value = 760 * value;
+ value = Math.round(Math.max(0, Math.min(1023, value)));
+ var s = "A" + slider.channel + " " + value;
+
+ // console.log(s);
+ this.sendString(s);
+ }
+
+ my.Panel.prototype.handleDisplayData = function(data) {
+ console.log("Handling display data " + data.length + " : " + data);
+ for (var i = 0; i < data.length; i++) {
+ var received = data[i];
+
+ if (this.ignoreNext > 0) {
+ console.log("skipping byte: 0x" + received.toString(16));
+ this.ignoreNext--;
+ continue;
+ }
+
+ console.log("handling byte: 0x" + received.toString(16));
+ if (this.light) {
+ var whichLight = received & 0x3f;
+ var button = this.lightButtons[whichLight];
+ if (button != null) {
+ var state = (received & 0xc0);
+ if (state == 0x80) {
+ button.setLight(my.Light.ON);
+ } else if (state == 0xc0) {
+ button.setLight(my.Light.BLINK);
+ } else {
+ button.setLight(my.Light.OFF);
+ }
+ }
+ this.light = false;
+ } else if ((received >= 0x80) && (received < 0xd0)) {
+ this.cursorY = ((received & 0x7f) >= 40) ? 1 : 0;
+ this.cursorX = (received & 0x7f) % 40;
+ this.blink = this.display.blink(this.cursorY, this.cursorX);
+ this.underline = this.display.underline(this.cursorY, this.cursorX);
+ console.log("moving to row " + this.cursorY + ", column " + this.cursorX);
+ } else if (received >= 0xd0) {
+ switch (received) {
+ case 0xd0: // blink start
+ console.log("blink on");
+ this.blink = true;
+ break;
+
+ case 0xd1: // blink stop (cancel all attribs on VFX+)
+ console.log("attrs off");
+ this.blink = false;
+ this.underline = false;
+ break;
+
+ case 0xd2: // blinking underline?
+ console.log("blinking underline on");
+ this.blink = true;
+ this.underline = true;
+ break;
+
+ case 0xd3: // start underline
+ console.log("underline on");
+ this.underline = true;
+ break;
+
+ case 0xd6: // clear screen
+ console.log("clear screen");
+ this.cursorX = this.cursorY = 0;
+ this.blink = this.underline = false;
+ this.display.clear();
+ break;
+
+ case 0xf5: // save cursor position
+ this.savedCursorX = this.cursorX;
+ this.savedCursorY = this.cursorY;
+ console.log("saving cursor position (row " + this.savedCursorY + ", col " + this.savedCursorX + ")");
+ break;
+
+ case 0xf6: // restore cursor position
+ this.cursorX = this.savedCursorX;
+ this.cursorY = this.savedCursorY;
+ this.blink = this.display.blink(this.cursorY, this.cursorX);
+ this.underline = this.display.underline(this.cursorY, this.cursorX);
+ console.log("restored cursor position (row " + this.savedCursorY + ", col " + this.savedCursorX + ")");
+ break;
+
+ case 0xff: // Specify a button light state
+ this.light = true;
+ break;
+
+ default:
+ console.log("Unexpected control code: " + received);
+ break;
+ }
+ } else if ((received >= 0x20) && (received <= 0x5f)) {
+ // var attrs = this.blink ? this.underline ? " with blink & underline" : " with blink" : this.underline ? " with underline" : "";
+ // console.log("at (" + this.cursorY + ", " + this.cursorX + ") char " + received + attrs);
+ this.display.setChar(this.cursorY, this.cursorX, received, this.underline, this.blink);
+ this.cursorX = Math.min(this.cursorX + 1, 39);
+ } else {
+ console.log("Unexpected byte: " + received.toString(16));
+ }
+ }
+ }
+
+ my.Panel.prototype.handleAnalogValue = function(data) {
+ var s = String.fromCharCode.apply(null, data);
+ console.log("Handling analog value: '" + s + "'");
+ var parts = s.split(" ");
+ if (parts.length == 2) {
+ var channel = parseInt(parts[0]);
+ var value = parseInt(parts[1]);
+
+ var analogControl = this.analogControls[channel];
+ if (analogControl != null) {
+ if (analogControl instanceof my.Slider) {
+ // 0.05 == 0; 0.95 == 760
+ value = value / 760.0;
+ value = 0.05 + 0.9 * value;
+ analogControl.setValue(value);
+ }
+ }
+ }
+ }
+
+ my.Panel.prototype.handleButtonState = function(data) {
+ var s = String.fromCharCode.apply(null, data);
+ var parts = s.split(" ");
+ if (parts.length == 2) {
+ var pressed = parts[0] == 'D';
+ var number = parseInt(parts[1]);
+ var button = this.buttons[number];
+ if (button != null && button instanceof my.Button) {
+ button.showPressed(pressed);
+ }
+ }
+ }
+
+ my.Panel.prototype.handleServerInformation = function(data) {
+ var s = String.fromCharCode.apply(null, data);
+ var parts = s.split(",");
+ var keyboard = "none";
+ var version = -1;
+ if (parts.length == 2) {
+ keyboard = parts[0];
+ version = parseInt(parts[1]);
+ }
+ if (keyboard == this.keyboard && version == this.version) {
+ // same keyboard type version - proceed!
+ this.sendString("CA1B1D1"); // Send me analog data, buttons, and display data
+ } else {
+ // we need to reload, forcing a refresh from the server.
+ document.location.reload(true);
+ }
+ }
+
+ return my;
+ })();
+ \ No newline at end of file