From 0206314395c7dcf7b51311e635588778f6d9cca3 Mon Sep 17 00:00:00 2001 From: Christian Brunschen Date: Wed, 5 Jul 2017 17:27:34 +0100 Subject: 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. --- src/emu/http.cpp | 369 +++++++++++- src/emu/http.h | 140 ++++- src/emu/machine.cpp | 10 +- src/lib/util/server_http.hpp | 5 +- src/lib/util/server_ws.hpp | 28 +- src/mame/drivers/esq5505.cpp | 8 +- src/mame/machine/esqpanel.cpp | 482 ++++++++++++++- src/mame/machine/esqpanel.h | 62 +- src/mame/machine/esqvfd.cpp | 41 +- src/mame/machine/esqvfd.h | 4 +- web/esqpanel/vfx/FrontPanel.html | 25 + web/esqpanel/vfx/FrontPanel.js | 1209 ++++++++++++++++++++++++++++++++++++++ 12 files changed, 2330 insertions(+), 53 deletions(-) create mode 100644 web/esqpanel/vfx/FrontPanel.html create mode 100644 web/esqpanel/vfx/FrontPanel.js 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 +#include +#include + + 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 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 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 get_headers(const std::string &header_name) { + std::list 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 m_response; + int m_status; + std::string m_content_type; + std::stringstream m_headers; + std::stringstream m_body; + + http_response_impl(std::shared_ptr 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 m_endpoint; + + websocket_endpoint_impl(std::shared_ptr 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 m_connection; + websocket_connection_impl(webpp::ws_server *server, std::shared_ptr 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 message_stream = std::make_shared(); + (*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()) + : m_io_context(std::make_shared()), 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(); - 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(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 response, std::shared_ptr request) { + auto request_impl = std::make_shared(request); + auto response_impl = std::make_shared(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 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 conn = (static_cast(connection))->ptr(); + http_manager::websocket_connection_ptr connection_impl = std::make_shared(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 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 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 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 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 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 lock(m_handlers_mutex); + m_handlers.erase(path); +} + +void http_manager::clear() { + m_server->clear(); + + std::lock_guard 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 endpoint_ptr(&endpoint); + auto endpoint_impl = std::make_shared(endpoint_ptr, on_open, on_message, on_close, on_error); + + endpoint.on_open = [&, this, endpoint_impl](std::shared_ptr conn) { + this->on_open(endpoint_impl, conn.get()); + }; + + endpoint.on_message = [&, this, endpoint_impl](std::shared_ptr conn, std::shared_ptr 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 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 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 + { + /** 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 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_ptr; + + /** An HTTP response. */ + struct http_response : public std::enable_shared_from_this + { + /** 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_ptr; + + /** Identifies a Websocket connection. */ + struct websocket_connection : public std::enable_shared_from_this + { + /** 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_ptr; + + /** Handles opening an incoming Websocket connection. */ + typedef std::function websocket_open_handler; + + /** Handles an incoming message on an open Websocket connection. */ + typedef std::function websocket_message_handler; + + /** Handles when the client has closed a websocket connection. */ + typedef std::function websocket_close_handler; + + /** Handles when there has been an error on a websocket connection. */ + typedef std::function websocket_error_handler; + + /** Handles an incoming HTTP request, generating an HTTP response. */ + typedef std::function http_handler; + + struct websocket_endpoint : public std::enable_shared_from_this { + 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_ptr; + + /** Substitutes one string with another, and returns whether the substitution should be performed. + * Used when evaluating a template. */ + typedef std::function 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 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 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::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 m_io_context; std::unique_ptr m_server; std::unique_ptr m_wsserver; std::thread m_server_thread; - std::unordered_map(std::string)>> m_handlers; + std::string m_root; + + std::unordered_map m_handlers; + std::mutex m_handlers_mutex; + + std::unordered_map m_endpoints; + std::mutex m_endpoints_mutex; + + std::unordered_map 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 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 lock(m_resource_mutex); m_resource.clear(); } @@ -206,12 +207,12 @@ namespace webpp { std::function socket, std::shared_ptr::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>> m_resource; + std::mutex m_resource_mutex; std::map 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 { friend class SocketServerBase; asio::streambuf streambuf; @@ -65,7 +65,7 @@ namespace webpp { }; - class Connection { + class Connection : public std::enable_shared_from_this { friend class SocketServerBase; friend class SocketServer; @@ -80,6 +80,10 @@ namespace webpp { std::string remote_endpoint_address; unsigned short remote_endpoint_port; + + std::shared_ptr 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 { friend class SocketServerBase; public: @@ -163,7 +167,7 @@ namespace webpp { asio::streambuf streambuf; }; - class Endpoint { + class Endpoint : public std::enable_shared_from_this { friend class SocketServerBase; std::unordered_set > 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 endpoint; + /// Warning: do not access (red or write) m_endpoint without holding m_endpoint_mutex + std::map 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 lock(m_endpoint_mutex); + for(auto& p: m_endpoint) p.second.connections.clear(); } @@ -310,7 +316,8 @@ namespace webpp { std::unordered_set > get_connections() { std::unordered_set > all_connections; - for(auto& e: endpoint) { + std::lock_guard lock(m_endpoint_mutex); + for(auto& e: m_endpoint) { std::lock_guard 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, const std::shared_ptr &read_buffer) { //Find path- and method-match, and generate response - for (auto ®ex_endpoint : endpoint) { + std::lock_guard lock(m_endpoint_mutex); + for (auto ®ex_endpoint : m_endpoint) { std::smatch path_match; if(std::regex_match(connection->path, path_match, regex_endpoint.first)) { auto write_buffer = std::make_shared(); 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 +#include +#include +#include +#include +#include + +class external_panel; + +using external_panel_ptr = std::shared_ptr; +typedef std::map> 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 +{ +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 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 lock(m_mutex); + m_panels[connection] = std::make_shared(); + } + + 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 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 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 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 lock(m_mutex); + return !m_commands.empty(); + } + + std::string get_next_command() + { + // printf("server: get_next_command()\n"); + std::lock_guard 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 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 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 m_content_provider; + std::map 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 + //************************************************************************** // 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 static devcb_base &set_tx_wr_callback(device_t &device, Object &&cb) { return downcast(device).m_write_tx.set_callback(std::forward(cb)); } - template static devcb_base &set_analog_wr_callback(device_t &device, Object &&cb) { return downcast(device).m_write_analog.set_callback(std::forward(cb)); } + template + static devcb_base &set_tx_wr_callback(device_t &device, Object &&cb) { + return downcast(device).m_write_tx.set_callback(std::forward(cb)); + } + + template + static devcb_base &set_analog_wr_callback(device_t &device, Object &&cb) { + return downcast(device).m_write_analog.set_callback(std::forward(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 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 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 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 @@ + + + Front Panel + + + + + + + + + \ 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 -- cgit v1.2.3 From 3bcb2546db1ae754cf96612c0a9b5e0bb5edb20c Mon Sep 17 00:00:00 2001 From: Christian Brunschen Date: Thu, 6 Jul 2017 00:19:04 +0100 Subject: better handle the case where HTTP is not enabled. --- src/emu/http.cpp | 16 ++++++++++++++- src/emu/http.h | 8 +++++++- src/emu/machine.cpp | 38 +++++++++++++++++----------------- src/mame/machine/esqpanel.cpp | 47 ++++++++++++++++++++++++++----------------- 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/emu/http.cpp b/src/emu/http.cpp index 9f911ed1252..541c049ef19 100644 --- a/src/emu/http.cpp +++ b/src/emu/http.cpp @@ -257,7 +257,7 @@ struct websocket_connection_impl : public http_manager::websocket_connection { }; http_manager::http_manager(bool active, short port, const char *root) - : m_io_context(std::make_shared()), m_root(root) + : m_active(active), m_io_context(std::make_shared()), m_root(root) { if (!active) return; @@ -420,6 +420,8 @@ bool http_manager::read_file(std::ostream &os, const std::string &path) { } void http_manager::serve_document(http_request_ptr request, http_response_ptr response, const std::string &filename) { + if (!m_active) return; + std::ostringstream os; if (read_file(os, filename)) { @@ -435,6 +437,8 @@ void http_manager::serve_document(http_request_ptr request, http_response_ptr re void http_manager::serve_template(http_request_ptr request, http_response_ptr response, const std::string &filename, substitution substitute, char init, char term) { + if (!m_active) return; + // 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)) @@ -484,6 +488,8 @@ void http_manager::serve_template(http_request_ptr request, http_response_ptr re void http_manager::add_http_handler(const std::string &path, http_manager::http_handler handler) { + if (!m_active) return; + using namespace std::placeholders; m_server->on_get(path, std::bind(on_get, handler, _1, _2)); @@ -492,6 +498,8 @@ void http_manager::add_http_handler(const std::string &path, http_manager::http_ } void http_manager::remove_http_handler(const std::string &path) { + if (!m_active) return; + m_server->remove_handler(path); std::lock_guard lock(m_handlers_mutex); @@ -499,6 +507,8 @@ void http_manager::remove_http_handler(const std::string &path) { } void http_manager::clear() { + if (!m_active) return; + m_server->clear(); std::lock_guard lock(m_handlers_mutex); @@ -510,6 +520,8 @@ http_manager::websocket_endpoint_ptr http_manager::add_endpoint(const std::strin http_manager::websocket_message_handler on_message, http_manager::websocket_close_handler on_close, http_manager::websocket_error_handler on_error) { + if (!m_active) return std::shared_ptr(nullptr); + auto i = m_endpoints.find(path); if (i == m_endpoints.end()) { using namespace std::placeholders; @@ -544,5 +556,7 @@ http_manager::websocket_endpoint_ptr http_manager::add_endpoint(const std::strin } void http_manager::remove_endpoint(const std::string &path) { + if (!m_active) return; + m_endpoints.erase(path); } diff --git a/src/emu/http.h b/src/emu/http.h index a7e898293d4..8da265c8893 100644 --- a/src/emu/http.h +++ b/src/emu/http.h @@ -156,7 +156,12 @@ public: /** Removes the websocket endpoint at the specified path. */ void remove_endpoint(const std::string &path); + + bool is_active() { + return m_active; + } +private: 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); @@ -167,7 +172,8 @@ public: bool read_file(std::ostream &os, const std::string &path); -private: + bool m_active; + std::shared_ptr m_io_context; std::unique_ptr m_server; std::unique_ptr m_wsserver; diff --git a/src/emu/machine.cpp b/src/emu/machine.cpp index 1e0f4323a7e..68b4b170b6c 100644 --- a/src/emu/machine.cpp +++ b/src/emu/machine.cpp @@ -1191,28 +1191,30 @@ running_machine::logerror_callback_item::logerror_callback_item(logerror_callbac void running_machine::export_http_api() { - 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 writer(s); - writer.StartObject(); - writer.Key("name"); - writer.String(m_basename.c_str()); + if (m_manager.http()->is_active()) { + 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 writer(s); + writer.StartObject(); + writer.Key("name"); + writer.String(m_basename.c_str()); - writer.Key("devices"); - writer.StartArray(); + writer.Key("devices"); + writer.StartArray(); - device_iterator iter(this->root_device()); - for (device_t &device : iter) - writer.String(device.tag()); + device_iterator iter(this->root_device()); + for (device_t &device : iter) + writer.String(device.tag()); - writer.EndArray(); - writer.EndObject(); + writer.EndArray(); + writer.EndObject(); - response->set_status(200); - response->set_content_type("application/json"); - response->set_body(s.GetString()); - }); + response->set_status(200); + response->set_content_type("application/json"); + response->set_body(s.GetString()); + }); + } } //************************************************************************** diff --git a/src/mame/machine/esqpanel.cpp b/src/mame/machine/esqpanel.cpp index 6195f8b8d11..0010a126658 100644 --- a/src/mame/machine/esqpanel.cpp +++ b/src/mame/machine/esqpanel.cpp @@ -125,16 +125,21 @@ public: 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) - ); + if (m_server->is_active()) { + 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() { + if (m_server->is_active()) { + m_server->remove_endpoint("/esqpanel/socket"); + } } void send_to_all(char c) @@ -417,19 +422,21 @@ 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); - }); + if (machine().manager().http()->is_active()) { + 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); + m_external_timer = timer_alloc(ESQPANEL_EXTERNAL_TIMER_ID); + m_external_timer->enable(false); + } } @@ -452,8 +459,10 @@ void esqpanel_device::device_reset() 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); + if (m_external_timer) { + m_external_timer->adjust(initial_delay, 0, sample_time); + m_external_timer->enable(true); + } } //------------------------------------------------- -- cgit v1.2.3