Initial re-upload of spice2x-24-08-24

This commit is contained in:
2024-08-28 11:10:34 -04:00
commit caa9e02285
1181 changed files with 380065 additions and 0 deletions

459
api/controller.cpp Normal file
View File

@@ -0,0 +1,459 @@
#include <winsock2.h>
#include <ws2tcpip.h>
#include "controller.h"
#include <utility>
#include "cfg/configurator.h"
#include "external/rapidjson/document.h"
#include "util/crypt.h"
#include "util/logging.h"
#include "util/utils.h"
#include "module.h"
#include "modules/analogs.h"
#include "modules/buttons.h"
#include "modules/card.h"
#include "modules/capture.h"
#include "modules/coin.h"
#include "modules/control.h"
#include "modules/drs.h"
#include "modules/iidx.h"
#include "modules/info.h"
#include "modules/keypads.h"
#include "modules/lcd.h"
#include "modules/lights.h"
#include "modules/memory.h"
#include "modules/touch.h"
#include "request.h"
#include "response.h"
using namespace rapidjson;
using namespace api;
Controller::Controller(unsigned short port, std::string password, bool pretty)
: port(port), password(std::move(password)), pretty(pretty)
{
if (!crypt::INITIALIZED && !this->password.empty()) {
log_fatal("api", "API server with password cannot be used without crypt module");
}
// WSA startup
WSADATA wsa_data;
int error;
if ((error = WSAStartup(MAKEWORD(2, 2), &wsa_data)) != 0) {
log_warning("api", "WSAStartup() returned {}", error);
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
}
return;
}
// create socket
this->server = socket(AF_INET, SOCK_STREAM, 0);
if (this->server == INVALID_SOCKET) {
log_warning("api", "could not create listener socket: {}", get_last_error_string());
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
}
return;
}
// configure socket
int opt_enable = 1;
if (setsockopt(this->server, SOL_SOCKET, SO_REUSEADDR,
reinterpret_cast<const char *>(&opt_enable), sizeof(int)) == -1)
{
log_warning("api", "could not set socket option SO_REUSEADDR: {}", get_last_error_string());
}
if (setsockopt(this->server, IPPROTO_TCP, TCP_NODELAY,
reinterpret_cast<const char *>(&opt_enable), sizeof(int)) == -1)
{
log_warning("api", "could not set socket option TCP_NODELAY: {}", get_last_error_string());
}
// create address
sockaddr_in server_address{};
server_address.sin_family = AF_INET;
server_address.sin_port = htons(this->port);
server_address.sin_addr.s_addr = INADDR_ANY;
memset(&server_address.sin_zero, 0, sizeof(server_address.sin_zero));
// bind socket to address
if (bind(this->server, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
log_warning("api", "could not bind socket on port {}: {}", port, get_last_error_string());
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
}
return;
}
// set socket to listen
if (listen(this->server, server_backlog) == -1) {
log_warning("api", "could not listen to socket on port {}: {}", port, get_last_error_string());
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
}
return;
}
// start workers
this->server_running = true;
for (int i = 0; i < server_worker_count; i++) {
this->server_workers.emplace_back(std::thread([this] {
this->server_worker();
}));
}
// log success
log_info("api", "API server is listening on port: {}", this->port);
log_info("api", "Using password: {}", this->password.empty() ? "no" : "yes");
// start websocket on next port
this->websocket = new WebSocketController(this, port + 1);
}
Controller::~Controller() {
// stop websocket
delete this->websocket;
// stop serial controllers
for (auto &s : this->serial) {
delete s;
}
// mark server stop
this->server_running = false;
// close socket
if (this->server != INVALID_SOCKET) {
closesocket(this->server);
}
// lock handlers
std::lock_guard<std::mutex> handlers_guard(this->server_handlers_m);
// join threads
for (auto &worker : this->server_workers) {
worker.join();
}
for (auto &handler : this->server_handlers) {
handler.join();
}
// cleanup WSA
WSACleanup();
}
void Controller::listen_serial(std::string port, DWORD baud) {
this->serial.push_back(new SerialController(this, port, baud));
}
void Controller::server_worker() {
// connection loop
while (this->server_running) {
// create client state
ClientState client_state {};
// accept connection
int socket_in_size = sizeof(sockaddr_in);
client_state.socket = accept(this->server, (sockaddr *) &client_state.address, &socket_in_size);
if (client_state.socket == INVALID_SOCKET) {
continue;
}
// lock handlers
std::lock_guard<std::mutex> handlers_guard(this->server_handlers_m);
// check connection limit
if (this->server_handlers.size() >= server_connection_limit) {
log_warning("api", "connection limit hit");
closesocket(client_state.socket);
continue;
}
// handle connection
this->server_handlers.emplace_back(std::thread([this, client_state] {
this->connection_handler(client_state);
}));
}
}
void Controller::connection_handler(api::ClientState client_state) {
// get address string
char client_address_str_data[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_state.address.sin_addr, client_address_str_data, INET_ADDRSTRLEN);
std::string client_address_str(client_address_str_data);
// log connection
log_info("api", "client connected: {}", client_address_str);
client_states_m.lock();
client_states.emplace_back(&client_state);
client_states_m.unlock();
// init state
init_state(&client_state);
// listen loop
std::vector<char> message_buffer;
char receive_buffer[server_receive_buffer_size];
while (this->server_running && !client_state.close) {
// receive data
int received_length = recv(client_state.socket, receive_buffer, server_receive_buffer_size, 0);
if (received_length < 0) {
// if the received length is < 0, we've got an error
log_warning("api", "receive error: {}", WSAGetLastError());
break;
} else if (received_length == 0) {
// if the received length is 0, the connection is closed
break;
}
// cipher
if (client_state.cipher != nullptr) {
client_state.cipher->crypt(
(uint8_t *) receive_buffer,
(size_t) received_length
);
}
// put into buffer
for (int i = 0; i < received_length; i++) {
// check for escape byte
if (receive_buffer[i] == 0) {
// get response
std::vector<char> send_buffer;
this->process_request(&client_state, &message_buffer, &send_buffer);
// clear message buffer
message_buffer.clear();
// check send buffer for content
if (!send_buffer.empty()) {
// cipher
if (client_state.cipher != nullptr) {
client_state.cipher->crypt(
(uint8_t *) send_buffer.data(),
(size_t) send_buffer.size()
);
}
// send data
send(client_state.socket, send_buffer.data(), (int) send_buffer.size(), 0);
// check for password change
process_password_change(&client_state);
}
} else {
// append to message
message_buffer.push_back(receive_buffer[i]);
// check buffer size
if (message_buffer.size() > server_message_buffer_max_size) {
message_buffer.clear();
client_state.close = true;
break;
}
}
}
}
// log disconnect
log_info("api", "client disconnected: {}", client_address_str);
client_states_m.lock();
client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
client_states_m.unlock();
// close connection
closesocket(client_state.socket);
// free state
free_state(&client_state);
}
bool Controller::process_request(ClientState *state, std::vector<char> *in, std::vector<char> *out) {
return this->process_request(state, &(*in)[0], in->size(), out);
}
bool Controller::process_request(ClientState *state, const char *in, size_t in_size, std::vector<char> *out) {
// parse document
Document document;
document.Parse(in, in_size);
// check for parse error
if (document.HasParseError()) {
// return empty response and close connection
out->push_back(0);
state->close = true;
return false;
}
// build request and response
Request request(document);
Response response(request.id);
bool success = true;
// check if request has parse error
if (request.parse_error) {
Value module_error("Request parse error (invalid message format?).");
response.add_error(module_error);
success = false;
} else {
// find module
bool module_found = false;
for (auto module : state->modules) {
if (module->name == request.module) {
module_found = true;
// check password force
if (module->password_force && this->password.empty() && request.function != "session_refresh") {
Value err("Module requires the password to be set.");
response.add_error(err);
break;
}
// handle request
module->handle(request, response);
break;
}
}
// check if module wasn't found
if (!module_found) {
Value module_error("Unknown module.");
response.add_error(module_error);
}
// check for password change
if (response.password_changed) {
state->password = response.password;
state->password_change = true;
}
}
// write response
auto response_out = response.get_string(this->pretty);
out->insert(out->end(), response_out.begin(), response_out.end());
out->push_back(0);
return success;
}
void Controller::process_password_change(api::ClientState *state) {
// check for password change
if (state->password_change) {
state->password_change = false;
delete state->cipher;
if (state->password.empty()) {
state->cipher = nullptr;
} else {
state->cipher = new util::RC4(
(uint8_t *) state->password.c_str(),
state->password.size());
}
}
}
void Controller::init_state(api::ClientState *state) {
// check if already initialized
if (!state->modules.empty()) {
log_fatal("api", "client state double initialization");
}
// cipher
state->cipher = nullptr;
state->password = this->password;
if (!this->password.empty()) {
state->cipher = new util::RC4((uint8_t *) this->password.c_str(), this->password.size());
}
// create module instances
state->modules.push_back(new modules::Analogs());
state->modules.push_back(new modules::Buttons());
state->modules.push_back(new modules::Card());
state->modules.push_back(new modules::Capture());
state->modules.push_back(new modules::Coin());
state->modules.push_back(new modules::Control());
state->modules.push_back(new modules::DRS());
state->modules.push_back(new modules::IIDX());
state->modules.push_back(new modules::Info());
state->modules.push_back(new modules::Keypads());
state->modules.push_back(new modules::LCD());
state->modules.push_back(new modules::Lights());
state->modules.push_back(new modules::Memory());
state->modules.push_back(new modules::Touch());
}
void Controller::free_state(api::ClientState *state) {
// free modules
for (auto module : state->modules) {
delete module;
}
// free cipher
delete state->cipher;
}
void Controller::free_socket() {
if (this->server != INVALID_SOCKET) {
closesocket(this->server);
this->server = INVALID_SOCKET;
}
this->websocket->free_socket();
for (auto &s : this->serial) {
s->free_port();
}
}
void Controller::obtain_client_states(std::vector<ClientState> *vec) {
std::lock_guard<std::mutex> lock(this->client_states_m);
for (auto &state : this->client_states) {
vec->push_back(*state);
}
}
std::string Controller::get_ip_address(sockaddr_in addr) {
switch (addr.sin_family) {
default:
case AF_INET: {
char buf[INET_ADDRSTRLEN];
auto ret = inet_ntop(AF_INET, &(addr.sin_addr), buf, sizeof(buf));
if (ret != nullptr) {
return std::string(ret);
} else {
return "unknown (" + to_string(WSAGetLastError()) + ")";
}
}
case AF_INET6: {
char buf[INET6_ADDRSTRLEN];
auto ret = inet_ntop(AF_INET6, &(addr.sin_addr), buf, sizeof(buf));
if (ret != nullptr) {
return std::string(ret);
} else {
return "unknown (" + to_string(WSAGetLastError()) + ")";
}
}
}
}

82
api/controller.h Normal file
View File

@@ -0,0 +1,82 @@
#pragma once
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <winsock2.h>
#include "util/rc4.h"
#include "module.h"
#include "websocket.h"
#include "serial.h"
namespace api {
struct ClientState {
SOCKADDR_IN address;
SOCKET socket;
bool close = false;
std::vector<Module*> modules;
std::string password;
bool password_change = false;
util::RC4 *cipher = nullptr;
};
class Controller {
private:
// configuration
const static int server_backlog = 16;
const static int server_receive_buffer_size = 64 * 1024;
const static int server_message_buffer_max_size = 64 * 1024;
const static int server_worker_count = 2;
const static int server_connection_limit = 4096;
// settings
unsigned short port;
std::string password;
bool pretty;
// server
WebSocketController *websocket;
std::vector<SerialController *> serial;
std::vector<std::thread> server_workers;
std::vector<std::thread> server_handlers;
std::mutex server_handlers_m;
std::vector<api::ClientState *> client_states;
std::mutex client_states_m;
SOCKET server;
void server_worker();
void connection_handler(ClientState client_state);
public:
// state
bool server_running;
// constructor / destructor
Controller(unsigned short port, std::string password, bool pretty);
~Controller();
void listen_serial(std::string port, DWORD baud);
bool process_request(ClientState *state, std::vector<char> *in, std::vector<char> *out);
bool process_request(ClientState *state, const char *in, size_t in_size, std::vector<char> *out);
static void process_password_change(ClientState *state);
void init_state(ClientState *state);
static void free_state(ClientState *state);
void free_socket();
void obtain_client_states(std::vector<ClientState> *output);
std::string get_ip_address(sockaddr_in addr);
inline const std::string &get_password() const {
return this->password;
}
};
}

43
api/module.cpp Normal file
View File

@@ -0,0 +1,43 @@
#include <utility>
#include "util/logging.h"
#include "module.h"
using namespace rapidjson;
namespace api {
// logging setting
bool LOGGING = false;
Module::Module(std::string name, bool password_force) {
this->name = std::move(name);
this->password_force = password_force;
}
void Module::handle(Request &req, Response &res) {
// log module access
if (LOGGING)
log_info("api::" + this->name, "handling request");
// find function
auto pos = functions.find(req.function);
if (pos == functions.end())
return error_function_unknown(res);
// call function
pos->second(req, res);
}
void Module::error(Response &res, std::string err) {
// log the warning
log_warning("api::" + this->name, "error: {}", err);
// add error to response
Value val(err.c_str(), res.doc()->GetAllocator());
res.add_error(val);
}
}

67
api/module.h Normal file
View File

@@ -0,0 +1,67 @@
#pragma once
#include <functional>
#include <map>
#include <string>
#include <sstream>
#include <external/robin_hood.h>
#include "response.h"
#include "request.h"
namespace api {
// logging setting
extern bool LOGGING;
// callback
typedef std::function<void(Request &, Response &)> ModuleFunctionCallback;
class Module {
protected:
// map of available functions
robin_hood::unordered_map<std::string, ModuleFunctionCallback> functions;
// default constructor
explicit Module(std::string name, bool password_force=false);
public:
// virtual deconstructor
virtual ~Module() = default;
// name of the module (should match namespace)
std::string name;
bool password_force;
// the magic
void handle(Request &req, Response &res);
/*
* Error definitions.
*/
void error(Response &res, std::string err);
void error_type(Response &res, const std::string &field, const std::string &type) {
std::ostringstream s;
s << field << " must be a " << type;
error(res, s.str());
};
void error_size(Response &res, const std::string &field, size_t size) {
std::ostringstream s;
s << field << " must be of size " << size;
error(res, s.str());
}
void error_unknown(Response &res, const std::string &field, const std::string &name) {
std::ostringstream s;
s << "Unknown " << field << ": " << name;
error(res, s.str());
}
#define ERR(name, err) void error_##name(Response &res) { error(res, err); }
ERR(function_unknown, "Unknown function.");
ERR(params_insufficient, "Insufficient number of parameters.");
#undef ERR
};
}

177
api/modules/analogs.cpp Normal file
View File

@@ -0,0 +1,177 @@
#include "analogs.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "misc/eamuse.h"
#include "cfg/analog.h"
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
Analogs::Analogs() : Module("analogs") {
functions["read"] = std::bind(&Analogs::read, this, _1, _2);
functions["write"] = std::bind(&Analogs::write, this, _1, _2);
functions["write_reset"] = std::bind(&Analogs::write_reset, this, _1, _2);
analogs = games::get_analogs(eamuse_get_game());
}
/**
* read()
*/
void Analogs::read(api::Request &req, Response &res) {
// check analog cache
if (!analogs) {
return;
}
// add state for each analog
for (auto &analog : *this->analogs) {
Value state(kArrayType);
Value analog_name(analog.getName().c_str(), res.doc()->GetAllocator());
Value analog_state(GameAPI::Analogs::getState(RI_MGR, analog));
Value analog_enabled(analog.override_enabled);
state.PushBack(analog_name, res.doc()->GetAllocator());
state.PushBack(analog_state, res.doc()->GetAllocator());
state.PushBack(analog_enabled, res.doc()->GetAllocator());
res.add_data(state);
}
}
/**
* write([name: str, state: float], ...)
*/
void Analogs::write(Request &req, Response &res) {
// check analog cache
if (!analogs)
return;
// loop parameters
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsArray()) {
error(res, "parameters must be arrays");
return;
}
if (param.Size() < 2) {
error_params_insufficient(res);
continue;
}
if (!param[0].IsString()) {
error_type(res, "name", "string");
continue;
}
if (!param[1].IsFloat() && !param[1].IsInt()) {
error_type(res, "state", "float");
continue;
}
// get params
auto analog_name = param[0].GetString();
auto analog_state = param[1].GetFloat();
// write analog state
if (!this->write_analog(analog_name, analog_state)) {
error_unknown(res, "analog", analog_name);
continue;
}
}
}
/**
* write_reset()
* write_reset([name: str], ...)
*/
void Analogs::write_reset(Request &req, Response &res) {
// check analog cache
if (!analogs)
return;
// get params
auto params = req.params.GetArray();
// write_reset()
if (params.Size() == 0) {
if (analogs != nullptr) {
for (auto &analog : *this->analogs) {
analog.override_enabled = false;
}
}
return;
}
// loop parameters
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsArray()) {
error(res, "parameters must be arrays");
return;
}
if (param.Size() < 1) {
error_params_insufficient(res);
continue;
}
if (!param[0].IsString()) {
error_type(res, "name", "string");
continue;
}
// get params
auto analog_name = param[0].GetString();
// write analog state
if (!this->write_analog_reset(analog_name)) {
error_unknown(res, "analog", analog_name);
continue;
}
}
}
bool Analogs::write_analog(std::string name, float state) {
// check analog cache
if (!this->analogs) {
return false;
}
// find analog
for (auto &analog : *this->analogs) {
if (analog.getName() == name) {
analog.override_state = CLAMP(state, 0.f, 1.f);
analog.override_enabled = true;
return true;
}
}
// unknown analog
return false;
}
bool Analogs::write_analog_reset(std::string name) {
// check analog cache
if (!analogs) {
return false;
}
// find analog
for (auto &analog : *this->analogs) {
if (analog.getName() == name) {
analog.override_enabled = false;
return true;
}
}
// unknown analog
return false;
}
}

28
api/modules/analogs.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <vector>
#include "api/module.h"
#include "api/request.h"
#include "cfg/api.h"
namespace api::modules {
class Analogs : public Module {
public:
Analogs();
private:
// state
std::vector<Analog> *analogs;
// function definitions
void read(Request &req, Response &res);
void write(Request &req, Response &res);
void write_reset(Request &req, Response &res);
// helper
bool write_analog(std::string name, float state);
bool write_analog_reset(std::string name);
};
}

182
api/modules/buttons.cpp Normal file
View File

@@ -0,0 +1,182 @@
#include "buttons.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "misc/eamuse.h"
#include "cfg/button.h"
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
Buttons::Buttons() : Module("buttons") {
functions["read"] = std::bind(&Buttons::read, this, _1, _2);
functions["write"] = std::bind(&Buttons::write, this, _1, _2);
functions["write_reset"] = std::bind(&Buttons::write_reset, this, _1, _2);
buttons = games::get_buttons(eamuse_get_game());
}
/**
* read()
*/
void Buttons::read(api::Request &req, Response &res) {
// check button cache
if (!this->buttons) {
return;
}
// add state for each button
for (auto &button : *this->buttons) {
Value state(kArrayType);
Value button_name(button.getName().c_str(), res.doc()->GetAllocator());
Value button_state(GameAPI::Buttons::getVelocity(RI_MGR, button));
Value button_enabled(button.override_enabled);
state.PushBack(button_name, res.doc()->GetAllocator());
state.PushBack(button_state, res.doc()->GetAllocator());
state.PushBack(button_enabled, res.doc()->GetAllocator());
res.add_data(state);
}
}
/**
* write([name: str, state: bool/float], ...)
*/
void Buttons::write(Request &req, Response &res) {
// check button cache
if (!buttons) {
return;
}
// loop parameters
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsArray()) {
error(res, "parameters must be arrays");
return;
}
if (param.Size() < 2) {
error_params_insufficient(res);
continue;
}
if (!param[0].IsString()) {
error_type(res, "name", "string");
continue;
}
if (!param[1].IsBool() && !param[1].IsFloat() && !param[1].IsInt()) {
error_type(res, "state", "bool or float");
continue;
}
// get params
auto button_name = param[0].GetString();
auto button_state = param[1].IsBool() ? param[1].GetBool() : param[1].GetFloat() > 0;
auto button_velocity = param[1].IsFloat() ? param[1].GetFloat() : (button_state ? 1.f : 0.f);
// write button state
if (!this->write_button(button_name, button_velocity)) {
error_unknown(res, "button", button_name);
continue;
}
}
}
/**
* write_reset()
* write_reset([name: str], ...)
*/
void Buttons::write_reset(Request &req, Response &res) {
// check button cache
if (!this->buttons) {
return;
}
// get params
auto params = req.params.GetArray();
// write_reset()
if (params.Size() == 0) {
if (buttons != nullptr) {
for (auto &button : *this->buttons) {
button.override_enabled = false;
}
}
return;
}
// loop parameters
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsArray()) {
error(res, "parameters must be arrays");
return;
}
if (param.Size() < 1) {
error_params_insufficient(res);
continue;
}
if (!param[0].IsString()) {
error_type(res, "name", "string");
continue;
}
// get params
auto button_name = param[0].GetString();
// write button state
if (!this->write_button_reset(button_name)) {
error_unknown(res, "button", button_name);
continue;
}
}
}
bool Buttons::write_button(std::string name, float state) {
// check button cache
if (!this->buttons) {
return false;
}
// find button
for (auto &button : *this->buttons) {
if (button.getName() == name) {
button.override_state = state > 0.f ?
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
button.override_velocity = CLAMP(state, 0.f, 1.f);
button.override_enabled = true;
return true;
}
}
// unknown button
return false;
}
bool Buttons::write_button_reset(std::string name) {
// check button cache
if (!this->buttons) {
return false;
}
// find button
for (auto &button : *this->buttons) {
if (button.getName() == name) {
button.override_enabled = false;
return true;
}
}
// unknown button
return false;
}
}

28
api/modules/buttons.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <vector>
#include "api/module.h"
#include "api/request.h"
#include "cfg/api.h"
namespace api::modules {
class Buttons : public Module {
public:
Buttons();
private:
// state
std::vector<Button> *buttons;
// function definitions
void read(Request &req, Response &res);
void write(Request &req, Response &res);
void write_reset(Request &req, Response &res);
// helper
bool write_button(std::string name, float state);
bool write_button_reset(std::string name);
};
}

82
api/modules/capture.cpp Normal file
View File

@@ -0,0 +1,82 @@
#include "capture.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "hooks/graphics/graphics.h"
#include "util/crypt.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
static thread_local std::vector<uint8_t> CAPTURE_BUFFER;
Capture::Capture() : Module("capture") {
functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2);
functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2);
}
/**
* get_screens()
*/
void Capture::get_screens(Request &req, Response &res) {
// aquire screens
std::vector<int> screens;
graphics_screens_get(screens);
// add screens to response
for (auto &screen : screens) {
res.add_data(screen);
}
}
/**
* get_jpg([screen=0, quality=70, downscale=0, divide=1])
* screen: uint specifying the window
* quality: uint in range [0, 100]
* reduce: uint for dividing image size
*/
void Capture::get_jpg(Request &req, Response &res) {
CAPTURE_BUFFER.reserve(1024 * 128);
// settings
int screen = 0;
int quality = 70;
int divide = 1;
if (req.params.Size() > 0 && req.params[0].IsUint())
screen = req.params[0].GetUint();
if (req.params.Size() > 1 && req.params[1].IsUint())
quality = req.params[1].GetUint();
if (req.params.Size() > 2 && req.params[2].IsUint())
divide = req.params[2].GetUint();
// receive JPEG data
uint64_t timestamp = 0;
int width = 0;
int height = 0;
graphics_capture_trigger(screen);
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
CAPTURE_BUFFER.push_back(byte);
}, true, quality, true, divide, &timestamp, &width, &height);
if (!success) {
return;
}
// encode to base64
auto encoded = crypt::base64_encode(
CAPTURE_BUFFER.data(),
CAPTURE_BUFFER.size());
// clear buffer
CAPTURE_BUFFER.clear();
// add data to response
Value data;
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
res.add_data(timestamp);
res.add_data(width);
res.add_data(height);
res.add_data(data);
}
}

18
api/modules/capture.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class Capture : public Module {
public:
Capture();
private:
// function definitions
void get_screens(Request &req, Response &res);
void get_jpg(Request &req, Response &res);
};
}

53
api/modules/card.cpp Normal file
View File

@@ -0,0 +1,53 @@
#include "card.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "util/logging.h"
#include "util/utils.h"
#include "misc/eamuse.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
Card::Card() : Module("card") {
functions["insert"] = std::bind(&Card::insert, this, _1, _2);
}
/**
* insert(index, card_id)
* index: uint in range [0, 1]
* card_id: hex string of length 16
*/
void Card::insert(Request &req, Response &res) {
// check params
if (req.params.Size() < 2)
return error_params_insufficient(res);
if (!req.params[0].IsUint())
return error_type(res, "index", "uint");
if (!req.params[1].IsString())
return error_type(res, "card_id", "hex string");
if (req.params[1].GetStringLength() != 16)
return error_size(res, "card_id", 16);
// get params
auto index = req.params[0].GetUint();
auto card_hex = req.params[1].GetString();
// convert to binary
uint8_t card_bin[8] {};
if (!hex2bin(card_hex, card_bin)) {
return error_type(res, "card_id", "hex string");
}
// log
if (LOGGING) {
log_info("api::card", "inserting card: {}", card_hex);
}
// insert card
eamuse_card_insert(index & 1, card_bin);
}
}

17
api/modules/card.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class Card : public Module {
public:
Card();
private:
// function definitions
void insert(Request &req, Response &res);
};
}

79
api/modules/coin.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include "coin.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "misc/eamuse.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
Coin::Coin() : Module("coin") {
functions["get"] = std::bind(&Coin::get, this, _1, _2);
functions["set"] = std::bind(&Coin::set, this, _1, _2);
functions["insert"] = std::bind(&Coin::insert, this, _1, _2);
functions["blocker_get"] = std::bind(&Coin::blocker_get, this, _1, _2);
}
/**
* get()
*/
void Coin::get(api::Request &req, api::Response &res) {
// get coin stock
auto coin_stock = eamuse_coin_get_stock();
// insert value
Value coin_stock_val(coin_stock);
res.add_data(coin_stock_val);
}
/**
* set(amount: int)
*/
void Coin::set(api::Request &req, api::Response &res) {
// check params
if (req.params.Size() < 1)
return error_params_insufficient(res);
if (!req.params[0].IsInt())
return error_type(res, "amount", "int");
// set coin stock
eamuse_coin_set_stock(req.params[0].GetInt());
}
/**
* insert()
* insert(amount: int)
*/
void Coin::insert(api::Request &req, api::Response &res) {
// insert()
if (req.params.Size() == 0) {
eamuse_coin_add();
return;
}
// check params
if (!req.params[0].IsInt())
return error_type(res, "amount", "int");
// add to coin stock
eamuse_coin_set_stock(eamuse_coin_get_stock() + std::max(0, req.params[0].GetInt()));
}
/*
* blocker_get()
*/
void Coin::blocker_get(api::Request &req, api::Response &res) {
// get block status
auto block_status = eamuse_coin_get_block();
// insert value
Value block_val(block_status);
res.add_data(block_val);
}
}

20
api/modules/coin.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class Coin : public Module {
public:
Coin();
private:
// function definitions
void get(Request &req, Response &res);
void set(Request &req, Response &res);
void insert(Request &req, Response &res);
void blocker_get(Request &req, Response &res);
};
}

152
api/modules/control.cpp Normal file
View File

@@ -0,0 +1,152 @@
#include "control.h"
#include <csignal>
#include <functional>
#include "external/rapidjson/document.h"
#include "launcher/shutdown.h"
#include "util/logging.h"
#include "util/crypt.h"
#include "util/utils.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
struct SignalMapping {
int signum;
const char* name;
};
static SignalMapping SIGNAL_MAPPINGS[] = {
{ SIGABRT, "SIGABRT" },
{ SIGFPE, "SIGFPE" },
{ SIGILL, "SIGILL" },
{ SIGINT, "SIGINT" },
{ SIGSEGV, "SIGSEGV" },
{ SIGTERM, "SIGTERM" },
};
Control::Control() : Module("control", true) {
functions["raise"] = std::bind(&Control::raise, this, _1, _2);
functions["exit"] = std::bind(&Control::exit, this, _1, _2);
functions["restart"] = std::bind(&Control::restart, this, _1, _2);
functions["session_refresh"] = std::bind(&Control::session_refresh, this, _1, _2);
functions["shutdown"] = std::bind(&Control::shutdown, this, _1, _2);
functions["reboot"] = std::bind(&Control::reboot, this, _1, _2);
}
/**
* raise(signal: str)
*/
void Control::raise(Request &req, Response &res) {
// check args
if (req.params.Size() < 1)
return error_params_insufficient(res);
if (!req.params[0].IsString())
return error_type(res, "signal", "string");
// get signal
auto signal_str = req.params[0].GetString();
int signal_val = -1;
for (auto mapping : SIGNAL_MAPPINGS) {
if (_stricmp(mapping.name, signal_str) == 0) {
signal_val = mapping.signum;
break;
}
}
// check if not found
if (signal_val < 0)
return error_unknown(res, "signal", signal_str);
// raise signal
if (::raise(signal_val))
return error(res, "Failed to raise signo " + to_string(signal_val));
}
/**
* exit()
* exit(code: int)
*/
void Control::exit(Request &req, Response &res) {
// exit()
if (req.params.Size() == 0) {
launcher::shutdown();
}
// check code
if (!req.params[0].IsInt())
return error_type(res, "code", "int");
// exit
launcher::shutdown(req.params[0].GetInt());
}
/**
* restart()
*/
void Control::restart(Request &req, Response &res) {
// restart launcher
launcher::restart();
}
/**
* session_refresh()
*/
void Control::session_refresh(Request &req, Response &res) {
// generate new password
uint8_t password_bin[128];
crypt::random_bytes(password_bin, std::size(password_bin));
std::string password = bin2hex(&password_bin[0], std::size(password_bin));
// add to response
Value password_val(password.c_str(), res.doc()->GetAllocator());
res.add_data(password_val);
// change password
res.password_change(password);
}
/**
* shutdown()
*/
void Control::shutdown(Request &req, Response &res) {
// acquire privileges
if (!acquire_shutdown_privs())
return error(res, "Unable to acquire shutdown privileges");
// exit windows
if (!ExitWindowsEx(EWX_SHUTDOWN | EWX_HYBRID_SHUTDOWN | EWX_FORCE,
SHTDN_REASON_MAJOR_APPLICATION |
SHTDN_REASON_MINOR_MAINTENANCE))
return error(res, "Unable to shutdown system");
// terminate this process
launcher::shutdown(0);
}
/**
* reboot()
*/
void Control::reboot(Request &req, Response &res) {
// acquire privileges
if (!acquire_shutdown_privs())
return error(res, "Unable to acquire shutdown privileges");
// exit windows
if (!ExitWindowsEx(EWX_REBOOT | EWX_FORCE,
SHTDN_REASON_MAJOR_APPLICATION |
SHTDN_REASON_MINOR_MAINTENANCE))
return error(res, "Unable to reboot system");
// terminate this process
launcher::shutdown(0);
}
}

22
api/modules/control.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class Control : public Module {
public:
Control();
private:
// function definitions
void raise(Request &req, Response &res);
void exit(Request &req, Response &res);
void restart(Request &req, Response &res);
void session_refresh(Request &req, Response &res);
void shutdown(Request &req, Response &res);
void reboot(Request &req, Response &res);
};
}

91
api/modules/drs.cpp Normal file
View File

@@ -0,0 +1,91 @@
#include "drs.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "games/drs/drs.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
DRS::DRS() : Module("drs") {
functions["tapeled_get"] = std::bind(&DRS::tapeled_get, this, _1, _2);
functions["touch_set"] = std::bind(&DRS::touch_set, this, _1, _2);
}
/**
* ticker_get()
*/
void DRS::tapeled_get(Request &req, Response &res) {
// copy data to array
Value tapeled(kArrayType);
const size_t tape_len = sizeof(games::drs::DRS_TAPELED);
const uint8_t *tape_raw = (uint8_t*) games::drs::DRS_TAPELED;
tapeled.Reserve(tape_len, res.doc()->GetAllocator());
for (size_t i = 0; i < tape_len; i++) {
tapeled.PushBack(tape_raw[i], res.doc()->GetAllocator());
}
// add to response
res.add_data(tapeled);
}
void DRS::touch_set(Request &req, Response &res) {
// get all touch points
games::drs::drs_touch_t touches[16];
size_t i = 0;
for (Value &param : req.params.GetArray()) {
// check params
if (param.Size() < 6) {
error_params_insufficient(res);
continue;
}
if (!param[0].IsUint()) {
error_type(res, "type", "uint");
continue;
}
if (!param[1].IsUint()) {
error_type(res, "id", "uint");
continue;
}
if (!param[2].IsDouble()) {
error_type(res, "x", "double");
continue;
}
if (!param[3].IsDouble()) {
error_type(res, "y", "double");
continue;
}
if (!param[4].IsDouble()) {
error_type(res, "width", "double");
continue;
}
if (!param[5].IsDouble()) {
error_type(res, "height", "double");
continue;
}
// get params
auto touch_type = param[0].GetUint();
auto touch_id = param[1].GetUint();
auto touch_x = param[2].GetDouble();
auto touch_y = param[3].GetDouble();
auto width = param[4].GetDouble();
auto height = param[5].GetDouble();
touches[i].type = touch_type;
touches[i].id = touch_id;
touches[i].x = touch_x;
touches[i].y = touch_y;
touches[i].width = width;
touches[i].height = height;
i++;
}
// apply touch points
games::drs::fire_touches(touches, i);
}
}

19
api/modules/drs.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <vector>
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class DRS : public Module {
public:
DRS();
private:
// function definitions
void tapeled_get(Request &req, Response &res);
void touch_set(Request &req, Response &res);
};
}

72
api/modules/iidx.cpp Normal file
View File

@@ -0,0 +1,72 @@
#include "iidx.h"
#include <functional>
#include <vector>
#include "games/iidx/iidx.h"
#include "external/rapidjson/document.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
// settings
static const size_t TICKER_SIZE = 9;
IIDX::IIDX() : Module("iidx") {
functions["ticker_get"] = std::bind(&IIDX::ticker_get, this, _1, _2);
functions["ticker_set"] = std::bind(&IIDX::ticker_set, this, _1, _2);
functions["ticker_reset"] = std::bind(&IIDX::ticker_reset, this, _1, _2);
}
/**
* ticker_get()
*/
void IIDX::ticker_get(api::Request &req, Response &res) {
// get led ticker
games::iidx::IIDX_LED_TICKER_LOCK.lock();
Value led_ticker(StringRef(games::iidx::IIDXIO_LED_TICKER, TICKER_SIZE), res.doc()->GetAllocator());
games::iidx::IIDX_LED_TICKER_LOCK.unlock();
// add to response
res.add_data(led_ticker);
}
/**
* ticker_set(text: str)
*/
void IIDX::ticker_set(api::Request &req, api::Response &res) {
// check param
if (req.params.Size() < 1)
return error_params_insufficient(res);
if (!req.params[0].IsString())
return error_type(res, "text", "str");
// get param
auto text = req.params[0].GetString();
auto text_len = req.params[0].GetStringLength();
// lock
std::lock_guard<std::mutex> ticker_lock(games::iidx::IIDX_LED_TICKER_LOCK);
// set to read only
games::iidx::IIDXIO_LED_TICKER_READONLY = true;
// set led ticker
memset(games::iidx::IIDXIO_LED_TICKER, ' ', TICKER_SIZE);
for (size_t i = 0; i < TICKER_SIZE && i < text_len; i++) {
games::iidx::IIDXIO_LED_TICKER[i] = text[i];
}
}
void IIDX::ticker_reset(api::Request &req, api::Response &res) {
// lock
std::lock_guard<std::mutex> ticker_lock(games::iidx::IIDX_LED_TICKER_LOCK);
// disable read only
games::iidx::IIDXIO_LED_TICKER_READONLY = false;
}
}

19
api/modules/iidx.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class IIDX : public Module {
public:
IIDX();
private:
// function definitions
void ticker_get(Request &req, Response &res);
void ticker_set(Request &req, Response &res);
void ticker_reset(Request &req, Response &res);
};
}

98
api/modules/info.cpp Normal file
View File

@@ -0,0 +1,98 @@
#include "info.h"
#include <functional>
#include <iomanip>
#include "external/rapidjson/document.h"
#include "avs/game.h"
#include "avs/ea3.h"
#include "util/logging.h"
#include "util/utils.h"
#include "util/memutils.h"
#include "build/defs.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
Info::Info() : Module("info") {
functions["avs"] = std::bind(&Info::avs, this, _1, _2);
functions["launcher"] = std::bind(&Info::launcher, this, _1, _2);
functions["memory"] = std::bind(&Info::memory, this, _1, _2);
}
/**
* avs()
*/
void Info::avs(Request &req, Response &res) {
// get allocator
auto &alloc = res.doc()->GetAllocator();
// build info object
Value info(kObjectType);
info.AddMember("model", StringRef(avs::game::MODEL, 3), alloc);
info.AddMember("dest", StringRef(avs::game::DEST, 1), alloc);
info.AddMember("spec", StringRef(avs::game::SPEC, 1), alloc);
info.AddMember("rev", StringRef(avs::game::REV, 1), alloc);
info.AddMember("ext", StringRef(avs::game::EXT, 10), alloc);
info.AddMember("services", StringRef(avs::ea3::EA3_BOOT_URL.c_str()), alloc);
// add info object
res.add_data(info);
}
/**
* launcher()
*/
void Info::launcher(Request &req, Response &res) {
// get allocator
auto &alloc = res.doc()->GetAllocator();
// build args
Value args(kArrayType);
for (int count = 0; count < LAUNCHER_ARGC; count++) {
auto arg = LAUNCHER_ARGV[count];
args.PushBack(StringRef(arg), alloc);
}
// get system time
auto t_now = std::time(nullptr);
auto tm_now = *std::gmtime(&t_now);
auto tm_str = to_string(std::put_time(&tm_now, "%Y-%m-%dT%H:%M:%SZ"));
Value system_time(tm_str.c_str(), alloc);
// build info object
Value info(kObjectType);
info.AddMember("version", StringRef(VERSION_STRING), alloc);
info.AddMember("compile_date", StringRef(__DATE__), alloc);
info.AddMember("compile_time", StringRef(__TIME__), alloc);
info.AddMember("system_time", system_time, alloc);
info.AddMember("args", args, alloc);
// add info object
res.add_data(info);
}
/**
* memory()
*/
void Info::memory(Request &req, Response &res) {
// get allocator
auto &alloc = res.doc()->GetAllocator();
// build info object
Value info(kObjectType);
info.AddMember("mem_total", memutils::mem_total(), alloc);
info.AddMember("mem_total_used", memutils::mem_total_used(), alloc);
info.AddMember("mem_used", memutils::mem_used(), alloc);
info.AddMember("vmem_total", memutils::vmem_total(), alloc);
info.AddMember("vmem_total_used", memutils::vmem_total_used(), alloc);
info.AddMember("vmem_used", memutils::vmem_used(), alloc);
// add info object
res.add_data(info);
}
}

19
api/modules/info.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class Info : public Module {
public:
Info();
private:
// function definitions
void avs(Request &req, Response &res);
void launcher(Request &req, Response &res);
void memory(Request &req, Response &res);
};
}

176
api/modules/keypads.cpp Normal file
View File

@@ -0,0 +1,176 @@
#include "keypads.h"
#include <functional>
#include <windows.h>
#include "avs/game.h"
#include "external/rapidjson/document.h"
#include "misc/eamuse.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
struct KeypadMapping {
char character;
uint16_t state;
};
static KeypadMapping KEYPAD_MAPPINGS[] = {
{ '0', 1 << EAM_IO_KEYPAD_0 },
{ '1', 1 << EAM_IO_KEYPAD_1 },
{ '2', 1 << EAM_IO_KEYPAD_2 },
{ '3', 1 << EAM_IO_KEYPAD_3 },
{ '4', 1 << EAM_IO_KEYPAD_4 },
{ '5', 1 << EAM_IO_KEYPAD_5 },
{ '6', 1 << EAM_IO_KEYPAD_6 },
{ '7', 1 << EAM_IO_KEYPAD_7 },
{ '8', 1 << EAM_IO_KEYPAD_8 },
{ '9', 1 << EAM_IO_KEYPAD_9 },
{ 'A', 1 << EAM_IO_KEYPAD_00 },
{ 'D', 1 << EAM_IO_KEYPAD_DECIMAL },
};
Keypads::Keypads() : Module("keypads") {
functions["write"] = std::bind(&Keypads::write, this, _1, _2);
functions["set"] = std::bind(&Keypads::set, this, _1, _2);
functions["get"] = std::bind(&Keypads::get, this, _1, _2);
}
/**
* write(keypad: uint, input: str)
*/
void Keypads::write(Request &req, Response &res) {
// check params
if (req.params.Size() < 2) {
return error_params_insufficient(res);
}
if (!req.params[0].IsUint()) {
return error_type(res, "keypad", "uint");
}
if (!req.params[1].IsString()) {
return error_type(res, "input", "string");
}
// get params
auto keypad = req.params[0].GetUint();
auto input = std::string(req.params[1].GetString());
// process all chars
for (auto c : input) {
uint16_t state = 0;
// find mapping
bool mapping_found = false;
for (auto &mapping : KEYPAD_MAPPINGS) {
if (_strnicmp(&mapping.character, &c, 1) == 0) {
state |= mapping.state;
mapping_found = true;
break;
}
}
// check for error
if (!mapping_found) {
return error_unknown(res, "char", std::string("") + c);
}
/*
* Write input to keypad.
* We try to make sure it was accepted by waiting a bit more than two frames.
*/
DWORD sleep_time = 70;
if (avs::game::is_model("MDX")) {
// cuz fuck DDR
sleep_time = 150;
}
// set
eamuse_set_keypad_overrides(keypad, state);
Sleep(sleep_time);
// unset
eamuse_set_keypad_overrides(keypad, 0);
Sleep(sleep_time);
}
}
/**
* set(keypad: uint, key: char, ...)
*/
void Keypads::set(Request &req, Response &res) {
// check keypad
if (req.params.Size() < 1) {
return error_params_insufficient(res);
}
if (!req.params[0].IsUint()) {
return error_type(res, "keypad", "uint");
}
auto keypad = req.params[0].GetUint();
// iterate params
uint16_t state = 0;
auto params = req.params.GetArray();
for (size_t i = 1; i < params.Size(); i++) {
auto &param = params[i];
// check key
if (!param.IsString()) {
error_type(res, "key", "char");
}
if (param.GetStringLength() < 1) {
error_size(res, "key", 1);
}
// find mapping
auto key = param.GetString();
bool mapping_found = false;
for (auto &mapping : KEYPAD_MAPPINGS) {
if (_strnicmp(&mapping.character, key, 1) == 0) {
state |= mapping.state;
mapping_found = true;
break;
}
}
// check for error
if (!mapping_found) {
return error_unknown(res, "key", key);
}
}
// set keypad state
eamuse_set_keypad_overrides(keypad, state);
}
/**
* get(keypad: uint)
*/
void Keypads::get(Request &req, Response &res) {
// check keypad
if (req.params.Size() < 1) {
return error_params_insufficient(res);
}
if (!req.params[0].IsUint()) {
return error_type(res, "keypad", "uint");
}
auto keypad = req.params[0].GetUint();
// get keypad state
auto state = eamuse_get_keypad_state(keypad);
// add keys to response
for (auto &mapping : KEYPAD_MAPPINGS) {
if (state & mapping.state) {
Value val(&mapping.character, 1);
res.add_data(val);
}
}
}
}

19
api/modules/keypads.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class Keypads : public Module {
public:
Keypads();
private:
// function definitions
void write(Request &req, Response &res);
void set(Request &req, Response &res);
void get(Request &req, Response &res);
};
}

36
api/modules/lcd.cpp Normal file
View File

@@ -0,0 +1,36 @@
#include "lcd.h"
#include "external/rapidjson/document.h"
#include "games/shared/lcdhandle.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
LCD::LCD() : Module("lcd") {
functions["info"] = std::bind(&LCD::info, this, _1, _2);
}
/*
* info()
*/
void LCD::info(Request &req, Response &res) {
// get allocator
auto &alloc = res.doc()->GetAllocator();
// build info object
Value info(kObjectType);
info.AddMember("enabled", games::shared::LCD_ENABLED, alloc);
info.AddMember("csm", StringRef(games::shared::LCD_CSM.c_str()), alloc);
info.AddMember("bri", games::shared::LCD_BRI, alloc);
info.AddMember("con", games::shared::LCD_CON, alloc);
info.AddMember("bl", games::shared::LCD_BL, alloc);
info.AddMember("red", games::shared::LCD_RED, alloc);
info.AddMember("green", games::shared::LCD_GREEN, alloc);
info.AddMember("blue", games::shared::LCD_BLUE, alloc);
// add info object
res.add_data(info);
}
}

17
api/modules/lcd.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class LCD : public Module {
public:
LCD();
private:
// function definitions
void info(Request &req, Response &res);
};
}

191
api/modules/lights.cpp Normal file
View File

@@ -0,0 +1,191 @@
#include "lights.h"
#include <functional>
#include <cfg/configurator.h>
#include "external/rapidjson/document.h"
#include "misc/eamuse.h"
#include "cfg/light.h"
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
Lights::Lights() : Module("lights") {
functions["read"] = std::bind(&Lights::read, this, _1, _2);
functions["write"] = std::bind(&Lights::write, this, _1, _2);
functions["write_reset"] = std::bind(&Lights::write_reset, this, _1, _2);
lights = games::get_lights(eamuse_get_game());
}
/**
* read()
*/
void Lights::read(api::Request &req, Response &res) {
// check light cache
if (!this->lights) {
return;
}
// add state for each light
for (auto &light : *this->lights) {
Value state(kArrayType);
Value light_name(light.getName().c_str(), res.doc()->GetAllocator());
Value light_state(GameAPI::Lights::readLight(RI_MGR, light));
Value light_enabled(light.override_enabled);
state.PushBack(light_name, res.doc()->GetAllocator());
state.PushBack(light_state, res.doc()->GetAllocator());
state.PushBack(light_enabled, res.doc()->GetAllocator());
res.add_data(state);
}
}
/**
* write([name: str, state: float], ...)
*/
void Lights::write(Request &req, Response &res) {
// check light cache
if (!this->lights) {
return;
}
// loop parameters
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsArray()) {
error(res, "parameters must be arrays");
return;
}
if (param.Size() < 2) {
error_params_insufficient(res);
continue;
}
if (!param[0].IsString()) {
error_type(res, "name", "string");
continue;
}
if (!param[1].IsFloat() && !param[1].IsInt()) {
error_type(res, "state", "float");
continue;
}
// get params
auto light_name = param[0].GetString();
auto light_state = param[1].GetFloat();
// write light state
if (!this->write_light(light_name, light_state)) {
error_unknown(res, "light", light_name);
continue;
}
}
}
/**
* write_reset()
* write_reset([name: str], ...)
*/
void Lights::write_reset(Request &req, Response &res) {
// check light cache
if (!this->lights) {
return;
}
// get params
auto params = req.params.GetArray();
// write_reset()
if (params.Size() == 0) {
if (lights != nullptr) {
for (auto &light : *this->lights) {
if (light.override_enabled) {
if (cfg::CONFIGURATOR_STANDALONE) {
GameAPI::Lights::writeLight(RI_MGR, light, light.last_state);
}
light.override_enabled = false;
}
}
}
return;
}
// loop parameters
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsArray()) {
error(res, "parameters must be arrays");
return;
}
if (param.Size() < 1) {
error_params_insufficient(res);
continue;
}
if (!param[0].IsString()) {
error_type(res, "name", "string");
continue;
}
// get params
auto light_name = param[0].GetString();
// write analog state
if (!this->write_light_reset(light_name)) {
error_unknown(res, "analog", light_name);
continue;
}
}
}
bool Lights::write_light(std::string name, float state) {
// check light cache
if (!this->lights) {
return false;
}
// find light
for (auto &light : *this->lights) {
if (light.getName() == name) {
light.override_state = CLAMP(state, 0.f, 1.f);
light.override_enabled = true;
if (cfg::CONFIGURATOR_STANDALONE) {
GameAPI::Lights::writeLight(RI_MGR, light, state);
}
return true;
}
}
// unknown light
return false;
}
bool Lights::write_light_reset(std::string name) {
// check light cache
if (!this->lights) {
return false;
}
// find light
for (auto &light : *this->lights) {
if (light.getName() == name) {
light.override_enabled = false;
return true;
}
}
// unknown light
return false;
}
}

28
api/modules/lights.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <vector>
#include "api/module.h"
#include "api/request.h"
#include "cfg/api.h"
namespace api::modules {
class Lights : public Module {
public:
Lights();
private:
// state
std::vector<Light> *lights;
// function definitions
void read(Request &req, Response &res);
void write(Request &req, Response &res);
void write_reset(Request &req, Response &res);
// helper
bool write_light(std::string name, float state);
bool write_light_reset(std::string name);
};
}

233
api/modules/memory.cpp Normal file
View File

@@ -0,0 +1,233 @@
#include "memory.h"
#include <functional>
#include <mutex>
#include "external/rapidjson/document.h"
#include "util/fileutils.h"
#include "util/libutils.h"
#include "util/memutils.h"
#include "util/sigscan.h"
#include "util/utils.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
// global lock to prevent simultaneous access to memory
static std::mutex MEMORY_LOCK;
Memory::Memory() : Module("memory", true) {
functions["write"] = std::bind(&Memory::write, this, _1, _2);
functions["read"] = std::bind(&Memory::read, this, _1, _2);
functions["signature"] = std::bind(&Memory::signature, this, _1, _2);
}
/**
* write(dll_name: str, data: hex, offset: uint)
*/
void Memory::write(Request &req, Response &res) {
std::lock_guard<std::mutex> lock(MEMORY_LOCK);
// check params
if (req.params.Size() < 3) {
return error_params_insufficient(res);
}
if (!req.params[0].IsString()) {
return error_type(res, "dll_name", "str");
}
if (!req.params[1].IsString() || (req.params[1].GetStringLength() & 1)) {
return error_type(res, "data", "hex string");
}
if (!req.params[2].IsUint()) {
return error_type(res, "offset", "uint");
}
// get params
auto dll_name = req.params[0].GetString();
auto dll_path = MODULE_PATH / dll_name;
auto data = req.params[1].GetString();
intptr_t offset = req.params[2].GetUint();
// convert data to bin
size_t data_bin_size = strlen(data) / 2;
auto data_bin = std::make_unique<uint8_t[]>(data_bin_size);
hex2bin(data, data_bin.get());
// check if file exists in modules
if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string());
}
// get module
auto module = libutils::try_module(dll_name);
if (!module) {
return error(res, "Couldn't find module.");
}
// convert offset to RVA
offset = libutils::offset2rva(dll_path, offset);
if (offset == ~0) {
return error(res, "Couldn't convert offset to RVA.");
}
// get module information
MODULEINFO module_info {};
if (!GetModuleInformation(GetCurrentProcess(), module, &module_info, sizeof(MODULEINFO))) {
return error(res, "Couldn't get module information.");
}
// check bounds
if (offset + data_bin_size >= (size_t) module_info.lpBaseOfDll + module_info.SizeOfImage) {
return error(res, "Data out of bounds.");
}
auto data_pos = reinterpret_cast<uint8_t *>(module_info.lpBaseOfDll) + offset;
// replace data
memutils::VProtectGuard guard(data_pos, data_bin_size);
memcpy(data_pos, data_bin.get(), data_bin_size);
}
/**
* read(dll_name: str, offset: uint, size: uint)
*/
void Memory::read(Request &req, Response &res) {
std::lock_guard<std::mutex> lock(MEMORY_LOCK);
// check params
if (req.params.Size() < 3) {
return error_params_insufficient(res);
}
if (!req.params[0].IsString()) {
return error_type(res, "dll_name", "str");
}
if (!req.params[1].IsUint()) {
return error_type(res, "offset", "uint");
}
if (!req.params[2].IsUint()) {
return error_type(res, "size", "uint");
}
// get params
auto dll_name = req.params[0].GetString();
auto dll_path = MODULE_PATH / dll_name;
intptr_t offset = req.params[1].GetUint();
auto size = req.params[2].GetUint();
// check if file exists in modules
if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string());
}
// get module
auto module = libutils::try_module(dll_name);
if (!module) {
return error(res, "Couldn't find module.");
}
// convert offset to RVA
offset = libutils::offset2rva(dll_path, offset);
if (offset == ~0) {
return error(res, "Couldn't convert offset to RVA.");
}
// get module information
MODULEINFO module_info {};
if (!GetModuleInformation(GetCurrentProcess(), module, &module_info, sizeof(MODULEINFO))) {
return error(res, "Couldn't get module information.");
}
// check bounds
auto max = offset + size;
if ((size_t) max >= (size_t) module_info.lpBaseOfDll + module_info.SizeOfImage) {
return error(res, "Data out of bounds.");
}
// read memory to hex (without virtual protect)
std::string hex = bin2hex((uint8_t*) module_info.lpBaseOfDll + offset, size);
Value hex_val(hex.c_str(), res.doc()->GetAllocator());
res.add_data(hex_val);
}
/**
* signature(
* dll_name: str,
* signature: hex,
* replacement: hex,
* offset: uint,
* usage: uint)
*
* Both signature and replacement will ignore bytes specified as "??" in the hex string.
* The offset specifies the offset between the found signature and the position to write the replacement to.
* The resulting integer is the file offset where the replacement was written to.
*/
void Memory::signature(Request &req, Response &res) {
std::lock_guard<std::mutex> lock(MEMORY_LOCK);
// check params
if (req.params.Size() < 5) {
return error_params_insufficient(res);
}
if (!req.params[0].IsString()) {
return error_type(res, "dll_name", "string");
}
if (!req.params[1].IsString() || (req.params[1].GetStringLength() & 1)) {
return error_type(res, "signature", "hex string");
}
if (!req.params[2].IsString() || (req.params[2].GetStringLength() & 1)) {
return error_type(res, "replacement", "hex string");
}
if (!req.params[3].IsUint()) {
return error_type(res, "offset", "uint");
}
if (!req.params[4].IsUint()) {
return error_type(res, "usage", "uint");
}
// get params
auto dll_name = req.params[0].GetString();
auto dll_path = MODULE_PATH / dll_name;
auto signature = req.params[1].GetString();
auto replacement = req.params[2].GetString();
auto offset = req.params[3].GetUint();
auto usage = req.params[4].GetUint();
// check if file exists in modules
if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string());
}
// get module
auto module = libutils::try_module(dll_name);
if (!module) {
return error(res, "Couldn't find module.");
}
// execute
auto result = replace_pattern(
module,
signature,
replacement,
offset,
usage
);
// check result
if (!result) {
return error(res, std::string("Pattern not found in memory of ") + dll_name);
}
// convert to offset
auto rva = result - reinterpret_cast<intptr_t>(module);
result = libutils::rva2offset(dll_path, rva);
if (result == -1) {
return error(res, "Couldn't convert RVA to file offset.");
}
// add result
Value result_val(result);
res.add_data(result_val);
}
}

19
api/modules/memory.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class Memory : public Module {
public:
Memory();
private:
// function definitions
void write(Request &req, Response &res);
void read(Request &req, Response &res);
void signature(Request &req, Response &res);
};
}

145
api/modules/touch.cpp Normal file
View File

@@ -0,0 +1,145 @@
#include "touch.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "misc/eamuse.h"
#include "launcher/launcher.h"
#include "touch/touch.h"
#include "util/utils.h"
#include "games/iidx/iidx.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
Touch::Touch() : Module("touch") {
is_sdvx = avs::game::is_model("KFC");
is_tdj_fhd = (avs::game::is_model("LDJ") && games::iidx::is_tdj_fhd());
// special case: when windowed subscreen is in use, use the original coords
if (GRAPHICS_IIDX_WSUB) {
is_tdj_fhd = false;
}
functions["read"] = std::bind(&Touch::read, this, _1, _2);
functions["write"] = std::bind(&Touch::write, this, _1, _2);
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
}
/**
* read()
*/
void Touch::read(api::Request &req, Response &res) {
// get touch points
std::vector<TouchPoint> touch_points;
touch_get_points(touch_points);
// add state for each touch point
for (auto &touch : touch_points) {
Value state(kArrayType);
Value id((uint64_t) touch.id);
Value x((int64_t) touch.x);
Value y((int64_t) touch.y);
Value mouse((bool) touch.mouse);
state.PushBack(id, res.doc()->GetAllocator());
state.PushBack(x, res.doc()->GetAllocator());
state.PushBack(y, res.doc()->GetAllocator());
state.PushBack(mouse, res.doc()->GetAllocator());
res.add_data(state);
}
}
/**
* write([id: uint, x: int, y: int], ...)
*/
void Touch::write(Request &req, Response &res) {
// get all touch points
std::vector<TouchPoint> touch_points;
for (Value &param : req.params.GetArray()) {
// check params
if (param.Size() < 3) {
error_params_insufficient(res);
continue;
}
if (!param[0].IsUint()) {
error_type(res, "id", "uint");
continue;
}
if (!param[1].IsInt()) {
error_type(res, "x", "int");
continue;
}
if (!param[2].IsInt()) {
error_type(res, "y", "int");
continue;
}
// TODO: optional mouse parameter
// get params
auto touch_id = param[0].GetUint();
auto touch_x = param[1].GetInt();
auto touch_y = param[2].GetInt();
apply_touch_errata(touch_x, touch_y);
touch_points.emplace_back(TouchPoint {
.id = touch_id,
.x = touch_x,
.y = touch_y,
.mouse = false,
});
}
// apply touch points
touch_write_points(&touch_points);
}
/**
* write_reset(id: uint, ...)
*/
void Touch::write_reset(Request &req, Response &res) {
// get all IDs
std::vector<DWORD> touch_point_ids;
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsUint()) {
error_type(res, "id", "uint");
continue;
}
// remember touch ID
auto touch_id = param.GetUint();
touch_point_ids.emplace_back(touch_id);
}
// remove all IDs
touch_remove_points(&touch_point_ids);
}
void Touch::apply_touch_errata(int &x, int &y) {
int x_raw = x;
int y_raw = y;
if (is_tdj_fhd) {
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p)
// we don't know what screen is being shown on the companion and the API doesn't specify
// the target of the touch events so just assume it's the sub screen
x = x_raw * 1920 / 1280;
y = y_raw * 1080 / 720;
} else if (is_sdvx) {
// for exceed gear, they are both 1080p screens, but need to apply transformation
x = 1080 - y_raw;
y = x_raw;
}
}
}

24
api/modules/touch.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <vector>
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class Touch : public Module {
public:
Touch();
private:
bool is_sdvx;
bool is_tdj_fhd;
// function definitions
void read(Request &req, Response &res);
void write(Request &req, Response &res);
void write_reset(Request &req, Response &res);
void apply_touch_errata(int &x, int &y);
};
}

55
api/request.cpp Normal file
View File

@@ -0,0 +1,55 @@
#include "request.h"
#include "../util/logging.h"
#include "module.h"
using namespace rapidjson;
namespace api {
Request::Request(rapidjson::Document &document) {
Value::MemberIterator it;
this->parse_error = false;
// get ID
it = document.FindMember("id");
if (it == document.MemberEnd() || !(*it).value.IsUint64()) {
log_warning("api", "Request ID is invalid");
this->parse_error = true;
return;
}
this->id = (*it).value.GetUint64();
// get module
it = document.FindMember("module");
if (it == document.MemberEnd() || !(*it).value.IsString()) {
log_warning("api", "Request module is invalid");
this->parse_error = true;
return;
}
this->module = (*it).value.GetString();
// get function
it = document.FindMember("function");
if (it == document.MemberEnd() || !(*it).value.IsString()) {
log_warning("api", "Request function is invalid");
this->parse_error = true;
return;
}
this->function = (*it).value.GetString();
// get params
it = document.FindMember("params");
if (it == document.MemberEnd() || !(*it).value.IsArray()) {
log_warning("api", "Request params is invalid");
this->parse_error = true;
return;
}
this->params = document["params"];
// log request
if (LOGGING) {
log_info("api", "new request > id: {}, module: {}, function: {}",
this->id, this->module, this->function);
}
}
}

21
api/request.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include <string>
#include <stdint.h>
#include "external/rapidjson/document.h"
namespace api {
class Request {
public:
uint64_t id;
std::string module;
std::string function;
rapidjson::Value params;
bool parse_error;
Request(rapidjson::Document &document);
};
}

View File

@@ -0,0 +1,4 @@
# SpiceAPI Arduino Library
This library is still a bit experimental and might contain bugs.
To use this library, it's recommended to just copy the Arduino project and start from that.

View File

@@ -0,0 +1,169 @@
#ifndef SPICEAPI_CONNECTION_H
#define SPICEAPI_CONNECTION_H
#include <stdint.h>
#include "rc4.h"
#ifndef SPICEAPI_INTERFACE
#define SPICEAPI_INTERFACE Serial
#endif
namespace spiceapi {
class Connection {
private:
uint8_t* receive_buffer;
size_t receive_buffer_size;
const char* password;
RC4* cipher;
public:
Connection(size_t receive_buffer_size, const char* password = "");
~Connection();
void reset();
bool check();
void cipher_alloc(const char *session_key = nullptr);
void change_pass(const char* password, bool session = false);
const char* request(const char* json, size_t timeout = 1000);
const char* request(char* json, size_t timeout = 1000);
};
}
spiceapi::Connection::Connection(size_t receive_buffer_size, const char* password) {
this->receive_buffer = new uint8_t[receive_buffer_size];
this->receive_buffer_size = receive_buffer_size;
this->password = password;
this->cipher = nullptr;
this->reset();
}
spiceapi::Connection::~Connection() {
// clean up
if (this->cipher != nullptr)
delete this->cipher;
}
void spiceapi::Connection::reset() {
// drop all input
while (SPICEAPI_INTERFACE.available()) {
SPICEAPI_INTERFACE.read();
}
#ifdef SPICEAPI_INTERFACE_WIFICLIENT
// reconnect TCP client
SPICEAPI_INTERFACE.stop();
this->check();
#else
// 8 zeroes reset the password/session on serial
for (size_t i = 0; i < 8; i++) {
SPICEAPI_INTERFACE.write((int) 0);
}
#endif
// reset password
this->cipher_alloc();
}
void spiceapi::Connection::cipher_alloc(const char *session_key) {
// delete old cipher
if (this->cipher != nullptr) {
delete this->cipher;
this->cipher = nullptr;
}
// create new cipher if password is set
session_key = session_key ? session_key : this->password;
if (strlen(session_key) > 0) {
this->cipher = new RC4(
(uint8_t *) session_key,
strlen(session_key));
}
}
bool spiceapi::Connection::check() {
#ifdef SPICEAPI_INTERFACE_WIFICLIENT
if (!SPICEAPI_INTERFACE.connected()) {
return SPICEAPI_INTERFACE.connect(
SPICEAPI_INTERFACE_WIFICLIENT_HOST,
SPICEAPI_INTERFACE_WIFICLIENT_PORT);
} else {
return true;
}
#else
// serial is always valid
return true;
#endif
}
void spiceapi::Connection::change_pass(const char* password, bool session) {
if (!session) {
this->password = password;
}
this->cipher_alloc(password);
}
const char* spiceapi::Connection::request(const char* json, size_t timeout) {
auto json_len = strlen(json);
strncpy((char*) receive_buffer, json, receive_buffer_size);
return request((char*) receive_buffer, timeout);
}
const char* spiceapi::Connection::request(char* json_data, size_t timeout) {
// check connection
if (!this->check())
return "";
// crypt
auto json_len = strlen(json_data) + 1;
if (this->cipher != nullptr)
this->cipher->crypt((uint8_t*) json_data, json_len);
// send
auto send_result = SPICEAPI_INTERFACE.write((const char*) json_data, (int) json_len);
SPICEAPI_INTERFACE.flush();
if (send_result < (int) json_len) {
return "";
}
// receive
size_t receive_data_len = 0;
auto t_start = millis();
while (SPICEAPI_INTERFACE) {
// check for timeout
if (millis() - t_start > timeout) {
this->reset();
return "";
}
// read single byte
auto b = SPICEAPI_INTERFACE.read();
if (b < 0) continue;
receive_buffer[receive_data_len++] = b;
// check for buffer overflow
if (receive_data_len >= receive_buffer_size) {
this->reset();
return "";
}
// crypt
if (this->cipher != nullptr)
this->cipher->crypt(&receive_buffer[receive_data_len - 1], 1);
// check for message end
if (receive_buffer[receive_data_len - 1] == 0)
break;
}
// return resulting json
return (const char*) &receive_buffer[0];
}
#endif //SPICEAPI_CONNECTION_H

View File

@@ -0,0 +1,65 @@
#ifndef SPICEAPI_RC4_H
#define SPICEAPI_RC4_H
#include <stdint.h>
#include <stddef.h>
namespace spiceapi {
class RC4 {
private:
uint8_t s_box[256];
size_t a = 0, b = 0;
public:
RC4(uint8_t *key, size_t key_size);
void crypt(uint8_t *data, size_t size);
};
}
spiceapi::RC4::RC4(uint8_t *key, size_t key_size) {
// initialize S-BOX
for (size_t i = 0; i < sizeof(s_box); i++)
s_box[i] = (uint8_t) i;
// check key size
if (!key_size)
return;
// KSA
size_t j = 0;
for (size_t i = 0; i < sizeof(s_box); i++) {
// update
j = (j + s_box[i] + key[i % key_size]) % sizeof(s_box);
// swap
auto tmp = s_box[i];
s_box[i] = s_box[j];
s_box[j] = tmp;
}
}
void spiceapi::RC4::crypt(uint8_t *data, size_t size) {
// iterate all bytes
for (size_t pos = 0; pos < size; pos++) {
// update
a = (a + 1) % sizeof(s_box);
b = (b + s_box[a]) % sizeof(s_box);
// swap
auto tmp = s_box[a];
s_box[a] = s_box[b];
s_box[b] = tmp;
// crypt
data[pos] ^= s_box[(s_box[a] + s_box[b]) % sizeof(s_box)];
}
}
#endif //SPICEAPI_RC4_H

View File

@@ -0,0 +1,172 @@
/*
* SpiceAPI Arduino Example Project
*
* To enable it in SpiceTools, use "-api 1337 -apipass changeme -apiserial COM1" or similar.
*/
/*
* SpiceAPI Wrapper Buffer Sizes
*
* They should be as big as possible to be able to create/parse
* some of the bigger requests/responses. Due to dynamic memory
* limitations of some weaker devices, if you set them too high
* you will probably experience crashes/bugs/problems, one
* example would be "Request ID is invalid" in the log.
*/
#define SPICEAPI_WRAPPER_BUFFER_SIZE 256
#define SPICEAPI_WRAPPER_BUFFER_SIZE_STR 256
/*
* WiFi Support
* Uncomment to enable the wireless API interface.
*/
//#define ENABLE_WIFI
/*
* WiFi Settings
* You can ignore these if you don't plan on using WiFi
*/
#ifdef ENABLE_WIFI
#include <ESP8266WiFi.h>
WiFiClient client;
#define SPICEAPI_INTERFACE client
#define SPICEAPI_INTERFACE_WIFICLIENT
#define SPICEAPI_INTERFACE_WIFICLIENT_HOST "192.168.178.143"
#define SPICEAPI_INTERFACE_WIFICLIENT_PORT 1337
#define WIFI_SSID "MySSID"
#define WIFI_PASS "MyWifiPassword"
#endif
/*
* This is the interface a serial connection will use.
* You can change this to another Serial port, e.g. with an
* Arduino Mega you can use Serial1/Serial2/Serial3.
*/
#ifndef ENABLE_WIFI
#define SPICEAPI_INTERFACE Serial
#endif
/*
* SpiceAPI Includes
*
* If you have the JSON strings beforehands or want to craft them
* manually, you don't have to import the wrappers at all and can
* use Connection::request to send and receive raw JSON strings.
*/
#include "connection.h"
#include "wrappers.h"
/*
* This global object represents the API connection.
* The first parameter is the buffer size of the JSON string
* we're receiving. So a size of 512 will only be able to
* hold a JSON of 512 characters maximum.
*
* An empty password string means no password is being used.
* This is the recommended when using Serial only.
*/
spiceapi::Connection CON(512, "changeme");
void setup() {
#ifdef ENABLE_WIFI
/*
* When using WiFi, we can use the Serial interface for debugging.
* You can open Serial Monitor and see what IP it gets assigned to.
*/
Serial.begin(57600);
// set WiFi mode to station (disables integrated AP)
WiFi.mode(WIFI_STA);
// now try connecting to our Router/AP
Serial.print("Connecting");
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// print debug info over serial
Serial.print("\nLocal IP: ");
Serial.println(WiFi.localIP());
#else
/*
* Since the API makes use of the Serial module, we need to
* set it up using our preferred baud rate manually.
*/
SPICEAPI_INTERFACE.begin(57600);
while (!SPICEAPI_INTERFACE);
#endif
}
void loop() {
/*
* Here's a few tests/examples on how to make use of the wrappers.
*/
// insert cards for P1/P2
spiceapi::card_insert(CON, 0, "E004012345678901");
spiceapi::card_insert(CON, 1, "E004012345678902");
// insert a single coin / multiple coins
spiceapi::coin_insert(CON);
spiceapi::coin_insert(CON, 3);
// get the IIDX led ticker text
char ticker[9];
if (spiceapi::iidx_ticker_get(CON, ticker)) {
// if a function returns true, that means success
// now we can do something with the ticker as if it was a string
//Serial1.println(ticker);
}
// get AVS info
spiceapi::InfoAvs avs_info {};
if (spiceapi::info_avs(CON, avs_info)) {
//Serial1.println(avs_info.model);
}
// enter some keys on P1 keypad (blocks until sequence is entered fully)
spiceapi::keypads_write(CON, 0, "1234");
// get light states
spiceapi::LightState lights[8];
size_t lights_size = spiceapi::lights_read(CON, lights, 8);
for (size_t i = 0; i < lights_size; i++) {
auto &light = lights[i];
//Serial1.println(light.name);
//Serial1.println(light.value);
// modify value to full bright
light.value = 1.f;
}
// send back modified light states
spiceapi::lights_write(CON, lights, lights_size);
// refresh session (generates new crypt key, not that important for serial)
spiceapi::control_session_refresh(CON);
// you can also manually send requests without the wrappers
// this avoids json generation, but you still need to parse it in some way
const char *answer_json = CON.request(
"{"
"\"id\": 0,"
"\"module\":\"coin\","
"\"function\":\"insert\","
"\"params\":[]"
"}"
);
/*
* For more functions/information, just check out wrappers.h yourself.
* Have fun :)
*/
delay(5000);
}

View File

@@ -0,0 +1,716 @@
#ifndef SPICEAPI_WRAPPERS_H
#define SPICEAPI_WRAPPERS_H
#define ARDUINOJSON_USE_LONG_LONG 1
#include "ArduinoJson.h"
#include <Arduino.h>
#include "connection.h"
// default buffer sizes
#ifndef SPICEAPI_WRAPPER_BUFFER_SIZE
#define SPICEAPI_WRAPPER_BUFFER_SIZE 256
#endif
#ifndef SPICEAPI_WRAPPER_BUFFER_SIZE_STR
#define SPICEAPI_WRAPPER_BUFFER_SIZE_STR 256
#endif
namespace spiceapi {
/*
* Structs
*/
struct AnalogState {
String name = "";
float value = 0.f;
bool enabled = false;
};
struct ButtonState {
String name = "";
float value = 0.f;
bool enabled = false;
};
struct LightState {
String name = "";
float value = 0.f;
bool enabled = false;
};
struct InfoAvs {
String model, dest, spec, rev, ext;
};
struct InfoLauncher {
String version;
String compile_date, compile_time, system_time;
String args;
};
struct InfoMemory {
uint64_t mem_total, mem_total_used, mem_used;
uint64_t vmem_total, vmem_total_used, vmem_used;
};
struct TouchState {
uint64_t id;
int64_t x, y;
};
// static storage
char JSON_BUFFER_STR[SPICEAPI_WRAPPER_BUFFER_SIZE_STR];
/*
* Helpers
*/
uint64_t msg_gen_id() {
static uint64_t id_global = 0;
return ++id_global;
}
char *doc2str(DynamicJsonDocument *doc) {
char *buf = JSON_BUFFER_STR;
serializeJson(*doc, buf, SPICEAPI_WRAPPER_BUFFER_SIZE_STR);
return buf;
}
DynamicJsonDocument *request_gen(const char *module, const char *function) {
// create document
auto doc = new DynamicJsonDocument(SPICEAPI_WRAPPER_BUFFER_SIZE);
// add attributes
(*doc)["id"] = msg_gen_id();
(*doc)["module"] = module;
(*doc)["function"] = function;
// add params
(*doc).createNestedArray("params");
// return document
return doc;
}
DynamicJsonDocument *response_get(Connection &con, const char *json) {
// parse document
DynamicJsonDocument *doc = new DynamicJsonDocument(SPICEAPI_WRAPPER_BUFFER_SIZE);
auto err = deserializeJson(*doc, (char *) json);
// check for parse error
if (err) {
// reset cipher
con.cipher_alloc();
delete doc;
return nullptr;
}
// check id
if (!(*doc)["id"].is<int64_t>()) {
delete doc;
return nullptr;
}
// check errors
auto errors = (*doc)["errors"];
if (!errors.is<JsonArray>()) {
delete doc;
return nullptr;
}
// check error count
if (errors.as<JsonArray>().size() > 0) {
delete doc;
return nullptr;
}
// check data
if (!(*doc)["data"].is<JsonArray>()) {
delete doc;
return nullptr;
}
// return document
return doc;
}
/*
* Wrappers
*/
size_t analogs_read(Connection &con, AnalogState *buffer, size_t buffer_elements) {
auto req = request_gen("analogs", "read");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return 0;
auto data = (*res)["data"].as<JsonArray>();
size_t buffer_count = 0;
for (auto val : data) {
if (buffer_count >= buffer_elements) {
delete res;
return buffer_count;
}
buffer[buffer_count].name = (const char*) val[0];
buffer[buffer_count].value = val[1];
buffer[buffer_count].enabled = val[2];
buffer_count++;
}
delete res;
return buffer_count;
}
bool analogs_write(Connection &con, AnalogState *buffer, size_t buffer_elements) {
auto req = request_gen("analogs", "write");
auto params = (*req)["params"].as<JsonArray>();
for (size_t i = 0; i < buffer_elements; i++) {
auto &state = buffer[i];
auto data = params.createNestedArray();
data.add(state.name);
data.add(state.value);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool analogs_write_reset(Connection &con, AnalogState *buffer, size_t buffer_elements) {
auto req = request_gen("analogs", "write_reset");
auto params = (*req)["params"].as<JsonArray>();
for (size_t i = 0; i < buffer_elements; i++) {
auto &state = buffer[i];
auto data = params.createNestedArray();
data.add(state.name);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
size_t buttons_read(Connection &con, ButtonState *buffer, size_t buffer_elements) {
auto req = request_gen("buttons", "read");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return 0;
auto data = (*res)["data"].as<JsonArray>();
size_t buffer_count = 0;
for (auto val : data) {
if (buffer_count >= buffer_elements) {
delete res;
return buffer_count;
}
buffer[buffer_count].name = (const char*) val[0];
buffer[buffer_count].value = val[1];
buffer[buffer_count].enabled = val[2];
buffer_count++;
}
delete res;
return buffer_count;
}
bool buttons_write(Connection &con, ButtonState *buffer, size_t buffer_elements) {
auto req = request_gen("buttons", "write");
auto params = (*req)["params"].as<JsonArray>();
for (size_t i = 0; i < buffer_elements; i++) {
auto &state = buffer[i];
auto data = params.createNestedArray();
data.add(state.name);
data.add(state.value);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool buttons_write_reset(Connection &con, ButtonState *buffer, size_t buffer_elements) {
auto req = request_gen("buttons", "write_reset");
auto params = (*req)["params"].as<JsonArray>();
for (size_t i = 0; i < buffer_elements; i++) {
auto &state = buffer[i];
auto data = params.createNestedArray();
data.add(state.name);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool card_insert(Connection &con, size_t index, const char *card_id) {
auto req = request_gen("card", "insert");
auto params = (*req)["params"].as<JsonArray>();
params.add(index);
params.add(card_id);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool coin_get(Connection &con, int &coins) {
auto req = request_gen("coin", "insert");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
coins = (*res)["data"][0];
delete res;
return true;
}
bool coin_set(Connection &con, int coins) {
auto req = request_gen("coin", "set");
auto params = (*req)["params"].as<JsonArray>();
params.add(coins);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool coin_insert(Connection &con, int coins=1) {
auto req = request_gen("coin", "insert");
if (coins != 1) {
auto params = (*req)["params"].as<JsonArray>();
params.add(coins);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool control_raise(Connection &con, const char *signal) {
auto req = request_gen("control", "raise");
auto params = (*req)["params"].as<JsonArray>();
params.add(signal);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool control_exit(Connection &con) {
auto req = request_gen("control", "exit");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool control_exit(Connection &con, int exit_code) {
auto req = request_gen("control", "exit");
auto params = (*req)["params"].as<JsonArray>();
params.add(exit_code);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool control_restart(Connection &con) {
auto req = request_gen("control", "restart");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool control_session_refresh(Connection &con) {
auto req = request_gen("control", "session_refresh");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
const char *key = (*res)["data"][0];
con.change_pass(key, true);
delete res;
return true;
}
bool control_shutdown(Connection &con) {
auto req = request_gen("control", "shutdown");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool control_reboot(Connection &con) {
auto req = request_gen("control", "reboot");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool iidx_ticker_get(Connection &con, char *ticker) {
auto req = request_gen("iidx", "ticker_get");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
const char *data = (*res)["data"][0];
strncpy(ticker, data, 9);
ticker[9] = 0x00;
delete res;
return true;
}
bool iidx_ticker_set(Connection &con, const char *ticker) {
auto req = request_gen("iidx", "ticker_set");
auto params = (*req)["params"].as<JsonArray>();
params.add(ticker);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool iidx_ticker_reset(Connection &con) {
auto req = request_gen("iidx", "ticker_reset");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool info_avs(Connection &con, InfoAvs &info) {
auto req = request_gen("info", "avs");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
auto data = (*res)["data"][0];
info.model = (const char*) data["model"];
info.dest = (const char*) data["dest"];
info.spec = (const char*) data["spec"];
info.rev = (const char*) data["rev"];
info.ext = (const char*) data["ext"];
delete res;
return true;
}
bool info_launcher(Connection &con, InfoLauncher &info) {
auto req = request_gen("info", "launcher");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
auto data = (*res)["data"][0];
info.version = (const char*) data["version"];
info.compile_date = (const char*) data["compile_date"];
info.compile_time = (const char*) data["compile_time"];
info.system_time = (const char*) data["system_time"];
for (auto arg : data["args"].as<JsonArray>()) {
info.args += (const char*) arg;
info.args += " ";
// TODO: remove last space
}
delete res;
return true;
}
bool info_memory(Connection &con, InfoMemory &info) {
auto req = request_gen("info", "memory");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
auto data = (*res)["data"][0];
info.mem_total = data["mem_total"];
info.mem_total_used = data["mem_total_used"];
info.mem_used = data["mem_used"];
info.vmem_total = data["vmem_total"];
info.vmem_total_used = data["vmem_total_used"];
info.vmem_used = data["vmem_used"];
delete res;
return true;
}
bool keypads_write(Connection &con, unsigned int keypad, const char *input) {
auto req = request_gen("keypads", "write");
auto params = (*req)["params"].as<JsonArray>();
params.add(keypad);
params.add(input);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str, 1000 + strlen(input) * 300));
if (!res)
return false;
delete res;
return true;
}
bool keypads_set(Connection &con, unsigned int keypad, const char *keys) {
auto keys_len = strlen(keys);
auto req = request_gen("keypads", "set");
auto params = (*req)["params"].as<JsonArray>();
params.add(keypad);
for (size_t i = 0; i < keys_len; i++) {
char buf[] = {keys[i], 0x00};
params.add(buf);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool keypads_get(Connection &con, unsigned int keypad, char *keys, size_t keys_len) {
auto req = request_gen("keypads", "get");
auto params = (*req)["params"].as<JsonArray>();
params.add(keypad);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
for (auto key : (*res)["data"].as<JsonArray>()) {
const char *key_str = key;
if (key_str != nullptr && keys_len > 0) {
*(keys++) = key_str[0];
keys_len--;
}
}
delete res;
return true;
}
size_t lights_read(Connection &con, LightState *buffer, size_t buffer_elements) {
auto req = request_gen("lights", "read");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return 0;
auto data = (*res)["data"].as<JsonArray>();
size_t buffer_count = 0;
for (auto val : data) {
if (buffer_count >= buffer_elements) {
delete res;
return buffer_count;
}
buffer[buffer_count].name = (const char*) val[0];
buffer[buffer_count].value = val[1];
buffer[buffer_count].enabled = val[2];
buffer_count++;
}
delete res;
return buffer_count;
}
bool lights_write(Connection &con, LightState *buffer, size_t buffer_elements) {
auto req = request_gen("lights", "write");
auto params = (*req)["params"].as<JsonArray>();
for (size_t i = 0; i < buffer_elements; i++) {
auto &state = buffer[i];
auto data = params.createNestedArray();
data.add(state.name);
data.add(state.value);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool lights_write_reset(Connection &con, LightState *buffer, size_t buffer_elements) {
auto req = request_gen("lights", "write_reset");
auto params = (*req)["params"].as<JsonArray>();
for (size_t i = 0; i < buffer_elements; i++) {
auto &state = buffer[i];
auto data = params.createNestedArray();
data.add(state.name);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool memory_write(Connection &con, const char *dll_name, const char *hex, uint32_t offset) {
auto req = request_gen("memory", "write");
auto params = (*req)["params"].as<JsonArray>();
params.add(dll_name);
params.add(hex);
params.add(offset);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool memory_read(Connection &con, const char *dll_name, uint32_t offset, uint32_t size, String &hex) {
auto req = request_gen("memory", "read");
auto params = (*req)["params"].as<JsonArray>();
params.add(dll_name);
params.add(offset);
params.add(size);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
hex = (const char*) (*res)["data"][0];
delete res;
return true;
}
bool memory_signature(Connection &con, const char *dll_name, const char *signature,
const char *replacement, uint32_t offset, uint32_t usage, uint32_t &file_offset) {
auto req = request_gen("memory", "signature");
auto params = (*req)["params"].as<JsonArray>();
params.add(dll_name);
params.add(signature);
params.add(replacement);
params.add(offset);
params.add(usage);
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
file_offset = (*res)["data"][0];
delete res;
return true;
}
size_t touch_read(Connection &con, TouchState *buffer, size_t buffer_elements) {
auto req = request_gen("touch", "read");
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return 0;
auto data = (*res)["data"].as<JsonArray>();
size_t buffer_count = 0;
for (auto val : data) {
if (buffer_count >= buffer_elements) {
delete res;
return buffer_count;
}
buffer[buffer_count].id = val[0];
buffer[buffer_count].x = val[1];
buffer[buffer_count].y = val[2];
buffer_count++;
}
delete res;
return buffer_count;
}
bool touch_write(Connection &con, TouchState *buffer, size_t buffer_elements) {
auto req = request_gen("touch", "write");
auto params = (*req)["params"].as<JsonArray>();
for (size_t i = 0; i < buffer_elements; i++) {
auto &state = buffer[i];
auto data = params.createNestedArray();
data.add(state.id);
data.add(state.x);
data.add(state.y);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
bool touch_write_reset(Connection &con, TouchState *buffer, size_t buffer_elements) {
auto req = request_gen("touch", "write_reset");
auto params = (*req)["params"].as<JsonArray>();
for (size_t i = 0; i < buffer_elements; i++) {
auto &state = buffer[i];
auto data = params.createNestedArray();
data.add(state.id);
}
auto req_str = doc2str(req);
delete req;
auto res = response_get(con, con.request(req_str));
if (!res)
return false;
delete res;
return true;
}
}
#endif //SPICEAPI_WRAPPERS_H

View File

@@ -0,0 +1,8 @@
# SpiceAPI C++ Library
This library is still a bit experimental and might contain bugs.
To include it into your project, it's recommended to just copy the
files into your source directory.
To use the wrappers, RapidJSON is required and you might need to
adjust the include paths for your project's build.

View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@@ -0,0 +1,181 @@
#include <iostream>
#include <ws2tcpip.h>
#include "connection.h"
namespace spiceapi {
// settings
static const size_t RECEIVE_BUFFER_SIZE = 64 * 1024;
static const int RECEIVE_TIMEOUT = 1000;
}
spiceapi::Connection::Connection(std::string host, uint16_t port, std::string password) {
this->host = host;
this->port = port;
this->password = password;
this->socket = INVALID_SOCKET;
this->cipher = nullptr;
// WSA startup
WSADATA wsa_data;
int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
if (error) {
std::cerr << "Failed to start WSA: " << error << std::endl;
exit(1);
}
}
spiceapi::Connection::~Connection() {
// clean up
if (this->cipher != nullptr)
delete this->cipher;
// cleanup WSA
WSACleanup();
}
void spiceapi::Connection::cipher_alloc() {
// delete old cipher
if (this->cipher != nullptr) {
delete this->cipher;
this->cipher = nullptr;
}
// create new cipher if password is set
if (this->password.length() > 0) {
this->cipher = new RC4(
(uint8_t *) this->password.c_str(),
strlen(this->password.c_str()));
}
}
bool spiceapi::Connection::check() {
int result = 0;
// check if socket is invalid
if (this->socket == INVALID_SOCKET) {
// get all addresses
addrinfo *addr_list;
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
if ((result = getaddrinfo(
this->host.c_str(),
std::to_string(this->port).c_str(),
&hints,
&addr_list))) {
std::cerr << "getaddrinfo failed: " << result << std::endl;
return false;
}
// check all addresses
for (addrinfo *addr = addr_list; addr != NULL; addr = addr->ai_next) {
// try open socket
this->socket = ::socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
if (this->socket == INVALID_SOCKET) {
std::cerr << "socket failed: " << WSAGetLastError() << std::endl;
freeaddrinfo(addr_list);
return false;
}
// try connect
result = connect(this->socket, addr->ai_addr, (int) addr->ai_addrlen);
if (result == SOCKET_ERROR) {
closesocket(this->socket);
this->socket = INVALID_SOCKET;
continue;
}
// configure socket
int opt_val;
opt_val = 1;
setsockopt(this->socket, IPPROTO_TCP, TCP_NODELAY, (const char*) &opt_val, sizeof(opt_val));
opt_val = RECEIVE_TIMEOUT;
setsockopt(this->socket, SOL_SOCKET, SO_RCVTIMEO, (const char*) &opt_val, sizeof(opt_val));
// connection successful
this->cipher_alloc();
break;
}
// check if successful
freeaddrinfo(addr_list);
if (this->socket == INVALID_SOCKET) {
return false;
}
}
// socket probably still valid
return true;
}
void spiceapi::Connection::change_pass(std::string password) {
this->password = password;
this->cipher_alloc();
}
std::string spiceapi::Connection::request(std::string json) {
// check connection
if (!this->check())
return "";
// crypt
auto json_len = strlen(json.c_str()) + 1;
uint8_t* json_data = new uint8_t[json_len];
memcpy(json_data, json.c_str(), json_len);
if (this->cipher != nullptr)
this->cipher->crypt(json_data, json_len);
// send
auto send_result = send(this->socket, (const char*) json_data, (int) json_len, 0);
delete[] json_data;
if (send_result == SOCKET_ERROR || send_result < (int) json_len) {
closesocket(this->socket);
this->socket = INVALID_SOCKET;
return "";
}
// receive
uint8_t receive_data[RECEIVE_BUFFER_SIZE];
size_t receive_data_len = 0;
int receive_result;
while ((receive_result = recv(
this->socket,
(char*) &receive_data[receive_data_len],
sizeof(receive_data) - receive_data_len, 0)) > 0) {
// check for buffer overflow
if (receive_data_len + receive_result >= sizeof(receive_data)) {
closesocket(this->socket);
this->socket = INVALID_SOCKET;
return "";
}
// crypt
if (this->cipher != nullptr)
this->cipher->crypt(&receive_data[receive_data_len], (size_t) receive_result);
// increase received data length
receive_data_len += receive_result;
// check for message end
if (receive_data[receive_data_len - 1] == 0)
break;
}
// return resulting json
if (receive_data_len > 0) {
return std::string((const char *) &receive_data[0], receive_data_len - 1);
} else {
// receive error
this->socket = INVALID_SOCKET;
return "";
}
}

View File

@@ -0,0 +1,31 @@
#ifndef SPICEAPI_CONNECTION_H
#define SPICEAPI_CONNECTION_H
#include <string>
#include <winsock2.h>
#include "rc4.h"
namespace spiceapi {
class Connection {
private:
std::string host;
uint16_t port;
std::string password;
SOCKET socket;
RC4* cipher;
void cipher_alloc();
public:
Connection(std::string host, uint16_t port, std::string password = "");
~Connection();
bool check();
void change_pass(std::string password);
std::string request(std::string json);
};
}
#endif //SPICEAPI_CONNECTION_H

View File

@@ -0,0 +1,45 @@
#include "rc4.h"
#include <iterator>
spiceapi::RC4::RC4(uint8_t *key, size_t key_size) {
// initialize S-BOX
for (size_t i = 0; i < std::size(s_box); i++)
s_box[i] = (uint8_t) i;
// check key size
if (!key_size)
return;
// KSA
size_t j = 0;
for (size_t i = 0; i < std::size(s_box); i++) {
// update
j = (j + s_box[i] + key[i % key_size]) % std::size(s_box);
// swap
auto tmp = s_box[i];
s_box[i] = s_box[j];
s_box[j] = tmp;
}
}
void spiceapi::RC4::crypt(uint8_t *data, size_t size) {
// iterate all bytes
for (size_t pos = 0; pos < size; pos++) {
// update
a = (a + 1) % std::size(s_box);
b = (b + s_box[a]) % std::size(s_box);
// swap
auto tmp = s_box[a];
s_box[a] = s_box[b];
s_box[b] = tmp;
// crypt
data[pos] ^= s_box[(s_box[a] + s_box[b]) % std::size(s_box)];
}
}

View File

@@ -0,0 +1,21 @@
#ifndef SPICEAPI_RC4_H
#define SPICEAPI_RC4_H
#include <cstdint>
namespace spiceapi {
class RC4 {
private:
uint8_t s_box[256];
size_t a = 0, b = 0;
public:
RC4(uint8_t *key, size_t key_size);
void crypt(uint8_t *data, size_t size);
};
}
#endif //SPICEAPI_RC4_H

View File

@@ -0,0 +1,638 @@
#include "wrappers.h"
#include <random>
#include <string>
/*
* RapidJSON dependency
* You might need to adjust the paths when importing into your own project.
*/
#include "external/rapidjson/document.h"
#include "external/rapidjson/writer.h"
using namespace rapidjson;
namespace spiceapi {
static inline std::string doc2str(Document &doc) {
StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> writer(sb);
doc.Accept(writer);
return sb.GetString();
}
static inline Document request_gen(const char *module, const char *function) {
// create document
Document doc;
doc.SetObject();
// add attributes
auto &alloc = doc.GetAllocator();
doc.AddMember("id", msg_gen_id(), alloc);
doc.AddMember("module", StringRef(module), alloc);
doc.AddMember("function", StringRef(function), alloc);
// add params
Value noparam(kArrayType);
doc.AddMember("params", noparam, alloc);
// return document
return doc;
}
static inline Document *response_get(std::string json) {
// parse document
Document *doc = new Document();
doc->Parse(json.c_str());
// check for parse error
if (doc->HasParseError()) {
delete doc;
return nullptr;
}
// check id
auto it_id = doc->FindMember("id");
if (it_id == doc->MemberEnd() || !(*it_id).value.IsUint64()) {
delete doc;
return nullptr;
}
// check errors
auto it_errors = doc->FindMember("errors");
if (it_errors == doc->MemberEnd() || !(*it_errors).value.IsArray()) {
delete doc;
return nullptr;
}
// check error count
if ((*it_errors).value.Size() > 0) {
delete doc;
return nullptr;
}
// check data
auto it_data = doc->FindMember("data");
if (it_data == doc->MemberEnd() || !(*it_data).value.IsArray()) {
delete doc;
return nullptr;
}
// return document
return doc;
}
}
uint64_t spiceapi::msg_gen_id() {
static uint64_t id_global = 0;
// check if global ID was initialized
if (id_global == 0) {
// generate a new ID
std::random_device rd;
std::mt19937_64 gen(rd());
std::uniform_int_distribution<uint64_t> dist(1, (uint64_t) std::llround(std::pow(2, 63)));
id_global = dist(gen);
} else {
// increase by one
id_global++;
}
// return global ID
return id_global;
}
bool spiceapi::analogs_read(spiceapi::Connection &con, std::vector<spiceapi::AnalogState> &states) {
auto req = request_gen("analogs", "read");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"];
for (auto &val : data.GetArray()) {
AnalogState state;
state.name = val[0].GetString();
state.value = val[1].GetFloat();
states.push_back(state);
}
delete res;
return true;
}
bool spiceapi::analogs_write(spiceapi::Connection &con, std::vector<spiceapi::AnalogState> &states) {
auto req = request_gen("analogs", "write");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
for (auto &state : states) {
Value state_val(kArrayType);
state_val.PushBack(StringRef(state.name.c_str()), alloc);
state_val.PushBack(state.value, alloc);
params.PushBack(state_val, alloc);
}
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::analogs_write_reset(spiceapi::Connection &con, std::vector<spiceapi::AnalogState> &states) {
auto req = request_gen("analogs", "write_reset");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
for (auto &state : states) {
Value state_val(kArrayType);
state_val.PushBack(StringRef(state.name.c_str()), alloc);
params.PushBack(state_val, alloc);
}
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::buttons_read(spiceapi::Connection &con, std::vector<spiceapi::ButtonState> &states) {
auto req = request_gen("buttons", "read");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"];
for (auto &val : data.GetArray()) {
ButtonState state;
state.name = val[0].GetString();
state.value = val[1].GetFloat();
states.push_back(state);
}
delete res;
return true;
}
bool spiceapi::buttons_write(spiceapi::Connection &con, std::vector<spiceapi::ButtonState> &states) {
auto req = request_gen("buttons", "write");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
for (auto &state : states) {
Value state_val(kArrayType);
state_val.PushBack(StringRef(state.name.c_str()), alloc);
state_val.PushBack(state.value, alloc);
params.PushBack(state_val, alloc);
}
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::buttons_write_reset(spiceapi::Connection &con, std::vector<spiceapi::ButtonState> &states) {
auto req = request_gen("buttons", "write_reset");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
for (auto &state : states) {
Value state_val(kArrayType);
state_val.PushBack(StringRef(state.name.c_str()), alloc);
params.PushBack(state_val, alloc);
}
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::card_insert(spiceapi::Connection &con, size_t index, const char *card_id) {
auto req = request_gen("card", "insert");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(index, alloc);
params.PushBack(StringRef(card_id), alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::coin_get(Connection &con, int &coins) {
auto req = request_gen("coin", "get");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
coins = (*res)["data"][0].GetInt();
delete res;
return true;
}
bool spiceapi::coin_set(Connection &con, int coins) {
auto req = request_gen("coin", "set");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(coins, alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::coin_insert(Connection &con, int coins) {
auto req = request_gen("coin", "insert");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(coins, alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::coin_blocker_get(Connection &con, bool &closed) {
auto req = request_gen("coin", "blocker_get");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
closed = (*res)["data"][0].GetBool();
delete res;
return true;
}
bool spiceapi::control_raise(spiceapi::Connection &con, const char *signal) {
auto req = request_gen("control", "raise");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(StringRef(signal), alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::control_exit(spiceapi::Connection &con) {
auto req = request_gen("control", "exit");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::control_exit(spiceapi::Connection &con, int exit_code) {
auto req = request_gen("control", "exit");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(exit_code, alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::control_restart(spiceapi::Connection &con) {
auto req = request_gen("control", "restart");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::control_session_refresh(spiceapi::Connection &con) {
auto req = request_gen("control", "session_refresh");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto key = (*res)["data"][0].GetString();
con.change_pass(key);
delete res;
return true;
}
bool spiceapi::control_shutdown(spiceapi::Connection &con) {
auto req = request_gen("control", "shutdown");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::control_reboot(spiceapi::Connection &con) {
auto req = request_gen("control", "reboot");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::iidx_ticker_get(spiceapi::Connection &con, char *ticker) {
auto req = request_gen("iidx", "ticker_get");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto data = (*res)["data"][0].GetString();
strncpy(ticker, data, 9);
ticker[9] = 0x00;
delete res;
return true;
}
bool spiceapi::iidx_ticker_set(spiceapi::Connection &con, const char *ticker) {
auto req = request_gen("iidx", "ticker_set");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(StringRef(ticker), alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::iidx_ticker_reset(spiceapi::Connection &con) {
auto req = request_gen("iidx", "ticker_reset");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::info_avs(spiceapi::Connection &con, spiceapi::InfoAvs &info) {
auto req = request_gen("info", "avs");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"][0];
info.model = data["model"].GetString();
info.dest = data["dest"].GetString();
info.spec = data["spec"].GetString();
info.rev = data["rev"].GetString();
info.ext = data["ext"].GetString();
delete res;
return true;
}
bool spiceapi::info_launcher(spiceapi::Connection &con, spiceapi::InfoLauncher &info) {
auto req = request_gen("info", "launcher");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"][0];
info.version = data["version"].GetString();
info.compile_date = data["compile_date"].GetString();
info.compile_time = data["compile_time"].GetString();
info.system_time = data["system_time"].GetString();
for (auto &arg : data["args"].GetArray())
info.args.push_back(arg.GetString());
delete res;
return true;
}
bool spiceapi::info_memory(spiceapi::Connection &con, spiceapi::InfoMemory &info) {
auto req = request_gen("info", "memory");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"][0];
info.mem_total = data["mem_total"].GetUint64();
info.mem_total_used = data["mem_total_used"].GetUint64();
info.mem_used = data["mem_used"].GetUint64();
info.vmem_total = data["vmem_total"].GetUint64();
info.vmem_total_used = data["vmem_total_used"].GetUint64();
info.vmem_used = data["vmem_used"].GetUint64();
delete res;
return true;
}
bool spiceapi::keypads_write(spiceapi::Connection &con, unsigned int keypad, const char *input) {
auto req = request_gen("keypads", "write");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(keypad, alloc);
params.PushBack(StringRef(input), alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::keypads_set(spiceapi::Connection &con, unsigned int keypad, std::vector<char> &keys) {
auto req = request_gen("keypads", "set");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(keypad, alloc);
for (auto &key : keys)
params.PushBack(StringRef(&key, 1), alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::keypads_get(spiceapi::Connection &con, unsigned int keypad, std::vector<char> &keys) {
auto req = request_gen("keypads", "get");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(keypad, alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"];
for (auto &val : data.GetArray())
keys.push_back(val.GetString()[0]);
delete res;
return true;
}
bool spiceapi::lights_read(spiceapi::Connection &con, std::vector<spiceapi::LightState> &states) {
auto req = request_gen("lights", "read");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"];
for (auto &val : data.GetArray()) {
LightState state;
state.name = val[0].GetString();
state.value = val[1].GetFloat();
states.push_back(state);
}
delete res;
return true;
}
bool spiceapi::lights_write(spiceapi::Connection &con, std::vector<spiceapi::LightState> &states) {
auto req = request_gen("lights", "write");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
for (auto &state : states) {
Value state_val(kArrayType);
state_val.PushBack(StringRef(state.name.c_str()), alloc);
state_val.PushBack(state.value, alloc);
params.PushBack(state_val, alloc);
}
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::lights_write_reset(spiceapi::Connection &con, std::vector<spiceapi::LightState> &states) {
auto req = request_gen("lights", "write_reset");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
for (auto &state : states) {
Value state_val(kArrayType);
state_val.PushBack(StringRef(state.name.c_str()), alloc);
params.PushBack(state_val, alloc);
}
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::memory_write(spiceapi::Connection &con, const char *dll_name, const char *hex, uint32_t offset) {
auto req = request_gen("memory", "write");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(StringRef(dll_name), alloc);
params.PushBack(StringRef(hex), alloc);
params.PushBack(offset, alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::memory_read(spiceapi::Connection &con, const char *dll_name, uint32_t offset, uint32_t size,
std::string &hex) {
auto req = request_gen("memory", "read");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(StringRef(dll_name), alloc);
params.PushBack(offset, alloc);
params.PushBack(size, alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
hex = (*res)["data"][0].GetString();
delete res;
return true;
}
bool spiceapi::memory_signature(spiceapi::Connection &con, const char *dll_name, const char *signature,
const char *replacement, uint32_t offset, uint32_t usage, uint32_t &file_offset) {
auto req = request_gen("memory", "signature");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
params.PushBack(StringRef(dll_name), alloc);
params.PushBack(StringRef(signature), alloc);
params.PushBack(StringRef(replacement), alloc);
params.PushBack(offset, alloc);
params.PushBack(usage, alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
file_offset = (*res)["data"][0].GetUint();
delete res;
return true;
}
bool spiceapi::touch_read(spiceapi::Connection &con, std::vector<spiceapi::TouchState> &states) {
auto req = request_gen("touch", "read");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"];
for (auto &val : data.GetArray()) {
TouchState state;
state.id = val[0].GetUint64();
state.x = val[1].GetInt64();
state.y = val[2].GetInt64();
states.push_back(state);
}
delete res;
return true;
}
bool spiceapi::touch_write(spiceapi::Connection &con, std::vector<spiceapi::TouchState> &states) {
auto req = request_gen("touch", "write");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
for (auto &state : states) {
Value state_val(kArrayType);
state_val.PushBack(state.id, alloc);
state_val.PushBack(state.x, alloc);
state_val.PushBack(state.y, alloc);
params.PushBack(state_val, alloc);
}
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::touch_write_reset(spiceapi::Connection &con, std::vector<spiceapi::TouchState> &states) {
auto req = request_gen("touch", "write_reset");
auto &alloc = req.GetAllocator();
Value params(kArrayType);
for (auto &state : states)
params.PushBack(state.id, alloc);
req["params"] = params;
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
delete res;
return true;
}
bool spiceapi::lcd_info(spiceapi::Connection &con, spiceapi::LCDInfo &info) {
auto req = request_gen("lcd", "info");
auto res = response_get(con.request(doc2str(req)));
if (!res)
return false;
auto &data = (*res)["data"][0];
info.enabled = data["enabled"].GetBool();
info.csm = data["csm"].GetString();
info.bri = data["bri"].GetInt();
info.con = data["con"].GetInt();
info.bl = data["bl"].GetInt();
info.red = data["red"].GetInt();
info.green = data["green"].GetInt();
info.blue = data["blue"].GetInt();
delete res;
return true;
}

View File

@@ -0,0 +1,104 @@
#ifndef SPICEAPI_WRAPPERS_H
#define SPICEAPI_WRAPPERS_H
#include <vector>
#include <string>
#include "connection.h"
namespace spiceapi {
struct AnalogState {
std::string name;
float value;
};
struct ButtonState {
std::string name;
float value;
};
struct LightState {
std::string name;
float value;
};
struct InfoAvs {
std::string model, dest, spec, rev, ext;
};
struct InfoLauncher {
std::string version;
std::string compile_date, compile_time, system_time;
std::vector<std::string> args;
};
struct InfoMemory {
uint64_t mem_total, mem_total_used, mem_used;
uint64_t vmem_total, vmem_total_used, vmem_used;
};
struct TouchState {
uint64_t id;
int64_t x, y;
};
struct LCDInfo {
bool enabled;
std::string csm;
uint8_t bri, con, bl, red, green, blue;
};
uint64_t msg_gen_id();
bool analogs_read(Connection &con, std::vector<AnalogState> &states);
bool analogs_write(Connection &con, std::vector<AnalogState> &states);
bool analogs_write_reset(Connection &con, std::vector<AnalogState> &states);
bool buttons_read(Connection &con, std::vector<ButtonState> &states);
bool buttons_write(Connection &con, std::vector<ButtonState> &states);
bool buttons_write_reset(Connection &con, std::vector<ButtonState> &states);
bool card_insert(Connection &con, size_t index, const char *card_id);
bool coin_get(Connection &con, int &coins);
bool coin_set(Connection &con, int coins);
bool coin_insert(Connection &con, int coins=1);
bool coin_blocker_get(Connection &con, bool &closed);
bool control_raise(Connection &con, const char *signal);
bool control_exit(Connection &con);
bool control_exit(Connection &con, int exit_code);
bool control_restart(Connection &con);
bool control_session_refresh(Connection &con);
bool control_shutdown(Connection &con);
bool control_reboot(Connection &con);
bool iidx_ticker_get(Connection &con, char *ticker);
bool iidx_ticker_set(Connection &con, const char *ticker);
bool iidx_ticker_reset(Connection &con);
bool info_avs(Connection &con, InfoAvs &info);
bool info_launcher(Connection &con, InfoLauncher &info);
bool info_memory(Connection &con, InfoMemory &info);
bool keypads_write(Connection &con, unsigned int keypad, const char *input);
bool keypads_set(Connection &con, unsigned int keypad, std::vector<char> &keys);
bool keypads_get(Connection &con, unsigned int keypad, std::vector<char> &keys);
bool lights_read(Connection &con, std::vector<LightState> &states);
bool lights_write(Connection &con, std::vector<LightState> &states);
bool lights_write_reset(Connection &con, std::vector<LightState> &states);
bool memory_write(Connection &con, const char *dll_name, const char *hex, uint32_t offset);
bool memory_read(Connection &con, const char *dll_name, uint32_t offset, uint32_t size, std::string &hex);
bool memory_signature(Connection &con, const char *dll_name, const char *signature, const char *replacement,
uint32_t offset, uint32_t usage, uint32_t &file_offset);
bool touch_read(Connection &con, std::vector<TouchState> &states);
bool touch_write(Connection &con, std::vector<TouchState> &states);
bool touch_write_reset(Connection &con, std::vector<TouchState> &states);
bool lcd_info(Connection &con, LCDInfo &info);
}
#endif //SPICEAPI_WRAPPERS_H

View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@@ -0,0 +1,22 @@
library spiceapi;
import 'dart:convert';
import 'dart:async';
import 'dart:math';
import 'dart:html';
import 'dart:typed_data';
part "src/connection.dart";
part "src/request.dart";
part "src/response.dart";
part "src/exceptions.dart";
part "src/rc4.dart";
part "src/wrappers/analogs.dart";
part "src/wrappers/buttons.dart";
part "src/wrappers/card.dart";
part "src/wrappers/coin.dart";
part "src/wrappers/control.dart";
part "src/wrappers/info.dart";
part "src/wrappers/keypads.dart";
part "src/wrappers/lights.dart";
part "src/wrappers/memory.dart";
part "src/wrappers/iidx.dart";
part "src/wrappers/touch.dart";

View File

@@ -0,0 +1,193 @@
part of spiceapi;
class Connection {
// settings
static const _TIMEOUT = Duration(seconds: 2);
static const _BUFFER_SIZE = 1024 * 8;
// state
final String host, pass;
final int port;
var resource;
List<int> _dataBuffer;
StreamController<Response> _responses;
StreamController<Connection> _connections;
WebSocket _socket;
RC4 _cipher;
bool _disposed = false;
Connection(this.host, this.port, this.pass,
{this.resource, bool refreshSession=true}) {
// initialize
_dataBuffer = List<int>();
_responses = StreamController<Response>.broadcast();
_connections = StreamController<Connection>.broadcast();
if (pass.length > 0)
_cipher = RC4(utf8.encode(pass));
// initialize socket
this._socket = WebSocket("ws://$host:${port + 1}");
this._socket.binaryType = "arraybuffer";
// listen to events
this._socket.onOpen.listen((e) async {
// refresh session
bool error = false;
if (refreshSession) {
try {
await controlRefreshSession(this);
} on Error {
error = true;
} on TimeoutException {
error = true;
}
}
// mark as connected
if (!this._connections.isClosed)
this._connections.add(this);
if (error)
this.dispose();
});
this._socket.onMessage.listen((e) {
// get data
var data = e.data;
if (data is ByteBuffer)
data = data.asUint8List();
// check type
if (data is List<int>) {
// cipher
if (_cipher != null)
_cipher.crypt(data);
// add data to buffer
_dataBuffer.addAll(data);
// check buffer size
if (_dataBuffer.length > _BUFFER_SIZE) {
this.dispose();
return;
}
// check for completed message
for (int i = 0; i < _dataBuffer.length; i++) {
if (_dataBuffer[i] == 0) {
// get message data and remove from buffer
var msgData = List<int>.from(_dataBuffer.getRange(0, i));
_dataBuffer.removeRange(0, i + 1);
// check data length
if (msgData.length > 0) {
// convert to JSON
var msgStr = utf8.decode(msgData, allowMalformed: false);
// build response
var res = Response.fromJson(msgStr);
this._responses.add(res);
}
}
}
}
});
this._socket.onClose.listen((e) {
this.dispose();
});
this._socket.onError.listen((e) {
this.dispose();
});
}
void changePass(String pass) {
if (pass.length > 0)
_cipher = RC4(utf8.encode(pass));
else
_cipher = null;
}
void dispose() {
if (_socket != null)
_socket.close();
_socket = null;
if (_responses != null)
_responses.close();
if (_connections != null)
_connections.close();
this._disposed = true;
this.free();
}
bool isDisposed() {
return this._disposed;
}
void free() {
// release optional resource
if (this.resource != null) {
this.resource.release();
this.resource = null;
}
}
bool isFree() {
return this.resource == null;
}
Future<Connection> onConnect() {
return _connections.stream.first;
}
bool isValid() {
return this._socket != null && !this._disposed;
}
Future<Response> request(Request req) {
// add response listener
var res = _awaitResponse(req._id);
// write request
_writeRequest(req);
// return future response
return res.then((res) {
// validate first
res.validate();
// return it
return res;
});
}
void _writeRequest(Request req) async {
// convert to JSON
var json = req.toJson() + "\x00";
var jsonEncoded = utf8.encode(json);
// cipher
if (_cipher != null)
_cipher.crypt(jsonEncoded);
// write to socket
this._socket.sendByteBuffer(Int8List.fromList(jsonEncoded).buffer);
}
Future<Response> _awaitResponse(int id) {
return _responses.stream.timeout(_TIMEOUT).firstWhere(
(res) => res._id == id, orElse: null);
}
}

View File

@@ -0,0 +1,11 @@
part of spiceapi;
class APIError implements Exception {
String cause;
APIError(this.cause);
@override
String toString() {
return this.cause;
}
}

View File

@@ -0,0 +1,48 @@
part of spiceapi;
class RC4 {
// state
int _a = 0;
int _b = 0;
List<int> _sBox = List<int>(256);
RC4(List<int> key) {
// init sBox
for (int i = 0; i < 256; i++) {
_sBox[i] = i;
}
// process key
int j = 0;
for (int i = 0; i < 256; i++) {
// update
j = (j + _sBox[i] + key[i % key.length]) % 256;
// swap
var tmp = _sBox[i];
_sBox[i] = _sBox[j];
_sBox[j] = tmp;
}
}
void crypt(List<int> inData) {
for (int i = 0; i < inData.length; i++) {
// update
_a = (_a + 1) % 256;
_b = (_b + _sBox[_a]) % 256;
// swap
var tmp = _sBox[_a];
_sBox[_a] = _sBox[_b];
_sBox[_b] = tmp;
// crypt
inData[i] ^= _sBox[(_sBox[_a] + _sBox[_b]) % 256];
}
}
}

View File

@@ -0,0 +1,45 @@
part of spiceapi;
class Request {
static int _lastID = 0;
// contents
int _id;
String _module;
String _function;
List _params;
Request(String module, String function, {id}) {
// automatic ID iteration
if (id == null) {
if (++_lastID >= pow(2, 32))
_lastID = 1;
id = _lastID;
} else
_lastID = id;
// build contents
this._id = id;
this._module = module;
this._function = function;
this._params = List();
}
String toJson() {
return jsonEncode(
{
"id": this._id,
"module": this._module,
"function": this._function,
"params": this._params,
}
);
}
void addParam(param) {
this._params.add(param);
}
}

View File

@@ -0,0 +1,35 @@
part of spiceapi;
class Response {
String _json;
int _id;
List _errors;
List _data;
Response.fromJson(String json) {
this._json = json;
var obj = jsonDecode(json);
this._id = obj["id"];
this._errors = obj["errors"];
this._data = obj["data"];
}
void validate() {
// check for errors
if (_errors.length > 0) {
// TODO: add all errors
throw APIError(_errors[0].toString());
}
}
List getData() {
return _data;
}
String toJson() {
return _json;
}
}

View File

@@ -0,0 +1,54 @@
part of spiceapi;
class AnalogState {
String name;
double state;
bool active;
AnalogState(this.name, this.state);
AnalogState._fromRead(this.name, this.state, this.active);
}
Future<List<AnalogState>> analogsRead(Connection con) {
var req = Request("analogs", "read");
return con.request(req).then((res) {
// build states list
List<AnalogState> states = [];
for (List state in res.getData()) {
states.add(AnalogState._fromRead(
state[0],
state[1],
state[2],
));
}
// return it
return states;
});
}
Future<void> analogsWrite(Connection con, List<AnalogState> states) {
var req = Request("analogs", "write");
// add params
for (var state in states) {
var obj = [
state.name,
state.state
];
req.addParam(obj);
}
return con.request(req);
}
Future<void> analogsWriteReset(Connection con, List<String> names) {
var req = Request("analogs", "write_reset");
// add params
for (var name in names)
req.addParam(name);
return con.request(req);
}

View File

@@ -0,0 +1,54 @@
part of spiceapi;
class ButtonState {
String name;
double state;
bool active;
ButtonState(this.name, this.state);
ButtonState._fromRead(this.name, this.state, this.active);
}
Future<List<ButtonState>> buttonsRead(Connection con) {
var req = Request("buttons", "read");
return con.request(req).then((res) {
// build states list
List<ButtonState> states = [];
for (List state in res.getData()) {
states.add(ButtonState._fromRead(
state[0],
state[1],
state[2],
));
}
// return it
return states;
});
}
Future<void> buttonsWrite(Connection con, List<ButtonState> states) {
var req = Request("buttons", "write");
// add params
for (var state in states) {
var obj = [
state.name,
state.state
];
req.addParam(obj);
}
return con.request(req);
}
Future<void> buttonsWriteReset(Connection con, List<String> names) {
var req = Request("buttons", "write_reset");
// add params
for (var name in names)
req.addParam(name);
return con.request(req);
}

View File

@@ -0,0 +1,8 @@
part of spiceapi;
Future<void> cardInsert(Connection con, int unit, String cardID) {
var req = Request("card", "insert");
req.addParam(unit);
req.addParam(cardID);
return con.request(req);
}

View File

@@ -0,0 +1,21 @@
part of spiceapi;
Future<int> coinGet(Connection con) {
var req = Request("coin", "get");
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<void> coinSet(Connection con, int amount) {
var req = Request("coin", "set");
req.addParam(amount);
return con.request(req);
}
Future<void> coinInsert(Connection con, [int amount=1]) {
var req = Request("coin", "insert");
if (amount != 1)
req.addParam(amount);
return con.request(req);
}

View File

@@ -0,0 +1,36 @@
part of spiceapi;
Future<void> controlRaise(Connection con, String signal) {
var req = Request("control", "raise");
req.addParam(signal);
return con.request(req);
}
Future<void> controlExit(Connection con, int code) {
var req = Request("control", "exit");
req.addParam(code);
return con.request(req);
}
Future<void> controlRestart(Connection con) {
var req = Request("control", "restart");
return con.request(req);
}
Future<void> controlRefreshSession(Connection con) {
var rnd = new Random();
var req = Request("control", "session_refresh", id: rnd.nextInt(pow(2, 32)));
return con.request(req).then((res) {
con.changePass(res.getData()[0]);
});
}
Future<void> controlShutdown(Connection con) {
var req = Request("control", "shutdown");
return con.request(req);
}
Future<void> controlReboot(Connection con) {
var req = Request("control", "reboot");
return con.request(req);
}

View File

@@ -0,0 +1,19 @@
part of spiceapi;
Future<String> iidxTickerGet(Connection con) {
var req = Request("iidx", "ticker_get");
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<void> iidxTickerSet(Connection con, String text) {
var req = Request("iidx", "ticker_set");
req.addParam(text);
return con.request(req);
}
Future<void> iidxTickerReset(Connection con) {
var req = Request("iidx", "ticker_reset");
return con.request(req);
}

View File

@@ -0,0 +1,22 @@
part of spiceapi;
Future<Map> infoAVS(Connection con) {
var req = Request("info", "avs");
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<Map> infoLauncher(Connection con) {
var req = Request("info", "launcher");
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<Map> infoMemory(Connection con) {
var req = Request("info", "memory");
return con.request(req).then((res) {
return res.getData()[0];
});
}

View File

@@ -0,0 +1,28 @@
part of spiceapi;
Future<void> keypadsWrite(Connection con, int unit, String input) {
var req = Request("keypads", "write");
req.addParam(unit);
req.addParam(input);
return con.request(req);
}
Future<void> keypadsSet(Connection con, int unit, String buttons) {
var req = Request("keypads", "set");
req.addParam(unit);
for (int i = 0; i < buttons.length; i++)
req.addParam(buttons[i]);
return con.request(req);
}
Future<String> keypadsGet(Connection con, int unit) {
var req = Request("keypads", "get");
req.addParam(unit);
return con.request(req).then((res) {
String buttons = "";
for (var obj in res.getData()) {
buttons += obj;
}
return buttons;
});
}

View File

@@ -0,0 +1,54 @@
part of spiceapi;
class LightState {
String name;
double state;
bool active;
LightState(this.name, this.state);
LightState._fromRead(this.name, this.state, this.active);
}
Future<List<LightState>> lightsRead(Connection con) {
var req = Request("lights", "read");
return con.request(req).then((res) {
// build states list
List<LightState> states = [];
for (List state in res.getData()) {
states.add(LightState._fromRead(
state[0],
state[1],
state[2],
));
}
// return it
return states;
});
}
Future<void> lightsWrite(Connection con, List<LightState> states) {
var req = Request("lights", "write");
// add params
for (var state in states) {
var obj = [
state.name,
state.state
];
req.addParam(obj);
}
return con.request(req);
}
Future<void> lightsWriteReset(Connection con, List<String> names) {
var req = Request("lights", "write_reset");
// add params
for (var name in names)
req.addParam(name);
return con.request(req);
}

View File

@@ -0,0 +1,35 @@
part of spiceapi;
Future<void> memoryWrite(Connection con,
String dllName, String data, int offset) {
var req = Request("memory", "write");
req.addParam(dllName);
req.addParam(data);
req.addParam(offset);
return con.request(req);
}
Future<String> memoryRead(Connection con,
String dllName, int offset, int size) {
var req = Request("memory", "read");
req.addParam(dllName);
req.addParam(offset);
req.addParam(size);
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<int> memorySignature(Connection con,
String dllName, String signature, String replacement,
int offset, int usage) {
var req = Request("memory", "signature");
req.addParam(dllName);
req.addParam(signature);
req.addParam(replacement);
req.addParam(offset);
req.addParam(usage);
return con.request(req).then((res) {
return res.getData()[0];
});
}

View File

@@ -0,0 +1,53 @@
part of spiceapi;
class TouchState {
int id;
int x, y;
TouchState(this.id, this.x, this.y);
}
Future<List<TouchState>> touchRead(Connection con) {
var req = Request("touch", "read");
return con.request(req).then((res) {
// build states list
List<TouchState> states = [];
for (List state in res.getData()) {
states.add(TouchState(
state[0],
state[1],
state[2],
));
}
// return it
return states;
});
}
Future<void> touchWrite(Connection con, List<TouchState> states) {
var req = Request("touch", "write");
// add params
for (var state in states) {
var obj = [
state.id,
state.x,
state.y
];
req.addParam(obj);
}
return con.request(req);
}
Future<void> touchWriteReset(Connection con, List<int> touchIDs) {
var req = Request("touch", "write_reset");
// add params
for (var id in touchIDs)
req.addParam(id);
return con.request(req);
}

View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@@ -0,0 +1,23 @@
library spiceapi;
import 'dart:io';
import 'dart:convert';
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
part "src/connection.dart";
part "src/request.dart";
part "src/response.dart";
part "src/exceptions.dart";
part "src/rc4.dart";
part "src/wrappers/analogs.dart";
part "src/wrappers/buttons.dart";
part "src/wrappers/capture.dart";
part "src/wrappers/card.dart";
part "src/wrappers/coin.dart";
part "src/wrappers/control.dart";
part "src/wrappers/info.dart";
part "src/wrappers/keypads.dart";
part "src/wrappers/lights.dart";
part "src/wrappers/memory.dart";
part "src/wrappers/iidx.dart";
part "src/wrappers/touch.dart";

View File

@@ -0,0 +1,192 @@
part of spiceapi;
class Connection {
// settings
static const _TIMEOUT = Duration(seconds: 3);
static const _BUFFER_SIZE = 1024 * 1024 * 8;
// state
final String host, pass;
final int port;
var resource;
List<int> _dataBuffer;
StreamController<Response> _responses;
StreamController<Connection> _connections;
Socket _socket;
RC4 _cipher;
bool _disposed = false;
Connection(this.host, this.port, this.pass,
{this.resource, bool refreshSession=true}) {
// initialize
_dataBuffer = List<int>();
_responses = StreamController<Response>.broadcast();
_connections = StreamController<Connection>.broadcast();
if (pass.length > 0)
_cipher = RC4(utf8.encode(pass));
// connect
Socket.connect(host, port, timeout: _TIMEOUT).then((socket) async {
// remember socket
this._socket = socket;
// listen to data
socket.listen((data) {
// cipher
if (_cipher != null)
_cipher.crypt(data);
// add data to buffer
_dataBuffer.addAll(data);
// check buffer size
if (_dataBuffer.length > _BUFFER_SIZE) {
socket.destroy();
return;
}
// check for completed message
for (int i = 0; i < _dataBuffer.length; i++) {
if (_dataBuffer[i] == 0) {
// get message data and remove from buffer
var msgData = List<int>.from(_dataBuffer.getRange(0, i));
_dataBuffer.removeRange(0, i + 1);
// check data length
if (msgData.length > 0) {
// convert to JSON
var msgStr = utf8.decode(msgData, allowMalformed: false);
// build response
var res = Response.fromJson(msgStr);
this._responses.add(res);
}
}
}
}, onError: (e) {
// dispose on listen error
this.dispose();
}, onDone: () {
// dispose on listen done
this.dispose();
});
// refresh session
bool error = false;
if (refreshSession) {
try {
await controlRefreshSession(this);
} on Error {
error = true;
} on TimeoutException {
error = true;
}
}
// mark as connected
if (!this._connections.isClosed)
this._connections.add(this);
if (error)
this.dispose();
}, onError: (e) {
// dispose on connection error
this.dispose();
});
}
void changePass(String pass) {
if (pass.length > 0)
_cipher = RC4(utf8.encode(pass));
else
_cipher = null;
}
void dispose() {
if (_socket != null)
_socket.destroy();
if (_responses != null)
_responses.close();
if (_connections != null)
_connections.close();
this._disposed = true;
this.free();
}
bool isDisposed() {
return this._disposed;
}
void free() {
// release optional resource
if (this.resource != null) {
this.resource.release();
this.resource = null;
}
}
bool isFree() {
return this.resource == null;
}
Future<Connection> onConnect() {
return _connections.stream.first;
}
bool isValid() {
return this._socket != null && !this._disposed;
}
Future<Response> request(Request req) {
// add response listener
var res = _awaitResponse(req._id);
// write request
_writeRequest(req);
// return future response
return res.then((res) {
// validate first
res.validate();
// return it
return res;
});
}
void _writeRequest(Request req) async {
// convert to JSON
var json = req.toJson() + "\x00";
var jsonEncoded = utf8.encode(json);
// cipher
if (_cipher != null)
_cipher.crypt(jsonEncoded);
// write to socket
this._socket.add(jsonEncoded);
}
Future<Response> _awaitResponse(int id) {
return _responses.stream.timeout(_TIMEOUT).firstWhere(
(res) => res._id == id, orElse: null);
}
}

View File

@@ -0,0 +1,11 @@
part of spiceapi;
class APIError implements Exception {
String cause;
APIError(this.cause);
@override
String toString() {
return this.cause;
}
}

View File

@@ -0,0 +1,48 @@
part of spiceapi;
class RC4 {
// state
int _a = 0;
int _b = 0;
List<int> _sBox = List<int>(256);
RC4(List<int> key) {
// init sBox
for (int i = 0; i < 256; i++) {
_sBox[i] = i;
}
// process key
int j = 0;
for (int i = 0; i < 256; i++) {
// update
j = (j + _sBox[i] + key[i % key.length]) % 256;
// swap
var tmp = _sBox[i];
_sBox[i] = _sBox[j];
_sBox[j] = tmp;
}
}
void crypt(List<int> inData) {
for (int i = 0; i < inData.length; i++) {
// update
_a = (_a + 1) % 256;
_b = (_b + _sBox[_a]) % 256;
// swap
var tmp = _sBox[_a];
_sBox[_a] = _sBox[_b];
_sBox[_b] = tmp;
// crypt
inData[i] ^= _sBox[(_sBox[_a] + _sBox[_b]) % 256];
}
}
}

View File

@@ -0,0 +1,45 @@
part of spiceapi;
class Request {
static int _lastID = 0;
// contents
int _id;
String _module;
String _function;
List _params;
Request(String module, String function, {id}) {
// automatic ID iteration
if (id == null) {
if (++_lastID >= pow(2, 32))
_lastID = 1;
id = _lastID;
} else
_lastID = id;
// build contents
this._id = id;
this._module = module;
this._function = function;
this._params = List();
}
String toJson() {
return jsonEncode(
{
"id": this._id,
"module": this._module,
"function": this._function,
"params": this._params,
}
);
}
void addParam(param) {
this._params.add(param);
}
}

View File

@@ -0,0 +1,35 @@
part of spiceapi;
class Response {
String _json;
int _id;
List _errors;
List _data;
Response.fromJson(String json) {
this._json = json;
var obj = jsonDecode(json);
this._id = obj["id"];
this._errors = obj["errors"];
this._data = obj["data"];
}
void validate() {
// check for errors
if (_errors.length > 0) {
// TODO: add all errors
throw APIError(_errors[0].toString());
}
}
List getData() {
return _data;
}
String toJson() {
return _json;
}
}

View File

@@ -0,0 +1,54 @@
part of spiceapi;
class AnalogState {
String name;
double state;
bool active;
AnalogState(this.name, this.state);
AnalogState._fromRead(this.name, this.state, this.active);
}
Future<List<AnalogState>> analogsRead(Connection con) {
var req = Request("analogs", "read");
return con.request(req).then((res) {
// build states list
List<AnalogState> states = [];
for (List state in res.getData()) {
states.add(AnalogState._fromRead(
state[0],
state[1],
state[2],
));
}
// return it
return states;
});
}
Future<void> analogsWrite(Connection con, List<AnalogState> states) {
var req = Request("analogs", "write");
// add params
for (var state in states) {
var obj = [
state.name,
state.state
];
req.addParam(obj);
}
return con.request(req);
}
Future<void> analogsWriteReset(Connection con, List<String> names) {
var req = Request("analogs", "write_reset");
// add params
for (var name in names)
req.addParam(name);
return con.request(req);
}

View File

@@ -0,0 +1,54 @@
part of spiceapi;
class ButtonState {
String name;
double state;
bool active;
ButtonState(this.name, this.state);
ButtonState._fromRead(this.name, this.state, this.active);
}
Future<List<ButtonState>> buttonsRead(Connection con) {
var req = Request("buttons", "read");
return con.request(req).then((res) {
// build states list
List<ButtonState> states = [];
for (List state in res.getData()) {
states.add(ButtonState._fromRead(
state[0],
state[1],
state[2],
));
}
// return it
return states;
});
}
Future<void> buttonsWrite(Connection con, List<ButtonState> states) {
var req = Request("buttons", "write");
// add params
for (var state in states) {
var obj = [
state.name,
state.state
];
req.addParam(obj);
}
return con.request(req);
}
Future<void> buttonsWriteReset(Connection con, List<String> names) {
var req = Request("buttons", "write_reset");
// add params
for (var name in names)
req.addParam(name);
return con.request(req);
}

View File

@@ -0,0 +1,38 @@
part of spiceapi;
class CaptureData {
int timestamp;
int width, height;
Uint8List data;
}
var _base64DecoderInstance = Base64Decoder();
Future<List> captureGetScreens(Connection con) {
var req = Request("capture", "get_screens");
return con.request(req).then((res) {
return res.getData();
});
}
Future<CaptureData> captureGetJPG(Connection con, {
int screen = 0,
int quality = 60,
int divide = 1,
}) {
var req = Request("capture", "get_jpg");
req.addParam(screen);
req.addParam(quality);
req.addParam(divide);
return con.request(req).then((res) {
var captureData = CaptureData();
var data = res.getData();
if (data.length > 0) captureData.timestamp = data[0];
if (data.length > 1) captureData.width = data[1];
if (data.length > 2) captureData.height = data[2];
if (data.length > 3) {
captureData.data = _base64DecoderInstance.convert(data[3]);
}
return captureData;
});
}

View File

@@ -0,0 +1,8 @@
part of spiceapi;
Future<void> cardInsert(Connection con, int unit, String cardID) {
var req = Request("card", "insert");
req.addParam(unit);
req.addParam(cardID);
return con.request(req);
}

View File

@@ -0,0 +1,21 @@
part of spiceapi;
Future<int> coinGet(Connection con) {
var req = Request("coin", "get");
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<void> coinSet(Connection con, int amount) {
var req = Request("coin", "set");
req.addParam(amount);
return con.request(req);
}
Future<void> coinInsert(Connection con, [int amount=1]) {
var req = Request("coin", "insert");
if (amount != 1)
req.addParam(amount);
return con.request(req);
}

View File

@@ -0,0 +1,36 @@
part of spiceapi;
Future<void> controlRaise(Connection con, String signal) {
var req = Request("control", "raise");
req.addParam(signal);
return con.request(req);
}
Future<void> controlExit(Connection con, int code) {
var req = Request("control", "exit");
req.addParam(code);
return con.request(req);
}
Future<void> controlRestart(Connection con) {
var req = Request("control", "restart");
return con.request(req);
}
Future<void> controlRefreshSession(Connection con) {
var rnd = new Random();
var req = Request("control", "session_refresh", id: rnd.nextInt(pow(2, 32)));
return con.request(req).then((res) {
con.changePass(res.getData()[0]);
});
}
Future<void> controlShutdown(Connection con) {
var req = Request("control", "shutdown");
return con.request(req);
}
Future<void> controlReboot(Connection con) {
var req = Request("control", "reboot");
return con.request(req);
}

View File

@@ -0,0 +1,19 @@
part of spiceapi;
Future<String> iidxTickerGet(Connection con) {
var req = Request("iidx", "ticker_get");
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<void> iidxTickerSet(Connection con, String text) {
var req = Request("iidx", "ticker_set");
req.addParam(text);
return con.request(req);
}
Future<void> iidxTickerReset(Connection con) {
var req = Request("iidx", "ticker_reset");
return con.request(req);
}

View File

@@ -0,0 +1,22 @@
part of spiceapi;
Future<Map> infoAVS(Connection con) {
var req = Request("info", "avs");
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<Map> infoLauncher(Connection con) {
var req = Request("info", "launcher");
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<Map> infoMemory(Connection con) {
var req = Request("info", "memory");
return con.request(req).then((res) {
return res.getData()[0];
});
}

View File

@@ -0,0 +1,28 @@
part of spiceapi;
Future<void> keypadsWrite(Connection con, int unit, String input) {
var req = Request("keypads", "write");
req.addParam(unit);
req.addParam(input);
return con.request(req);
}
Future<void> keypadsSet(Connection con, int unit, String buttons) {
var req = Request("keypads", "set");
req.addParam(unit);
for (int i = 0; i < buttons.length; i++)
req.addParam(buttons[i]);
return con.request(req);
}
Future<String> keypadsGet(Connection con, int unit) {
var req = Request("keypads", "get");
req.addParam(unit);
return con.request(req).then((res) {
String buttons = "";
for (var obj in res.getData()) {
buttons += obj;
}
return buttons;
});
}

View File

@@ -0,0 +1,54 @@
part of spiceapi;
class LightState {
String name;
double state;
bool active;
LightState(this.name, this.state);
LightState._fromRead(this.name, this.state, this.active);
}
Future<List<LightState>> lightsRead(Connection con) {
var req = Request("lights", "read");
return con.request(req).then((res) {
// build states list
List<LightState> states = [];
for (List state in res.getData()) {
states.add(LightState._fromRead(
state[0],
state[1],
state[2],
));
}
// return it
return states;
});
}
Future<void> lightsWrite(Connection con, List<LightState> states) {
var req = Request("lights", "write");
// add params
for (var state in states) {
var obj = [
state.name,
state.state
];
req.addParam(obj);
}
return con.request(req);
}
Future<void> lightsWriteReset(Connection con, List<String> names) {
var req = Request("lights", "write_reset");
// add params
for (var name in names)
req.addParam(name);
return con.request(req);
}

View File

@@ -0,0 +1,35 @@
part of spiceapi;
Future<void> memoryWrite(Connection con,
String dllName, String data, int offset) {
var req = Request("memory", "write");
req.addParam(dllName);
req.addParam(data);
req.addParam(offset);
return con.request(req);
}
Future<String> memoryRead(Connection con,
String dllName, int offset, int size) {
var req = Request("memory", "read");
req.addParam(dllName);
req.addParam(offset);
req.addParam(size);
return con.request(req).then((res) {
return res.getData()[0];
});
}
Future<int> memorySignature(Connection con,
String dllName, String signature, String replacement,
int offset, int usage) {
var req = Request("memory", "signature");
req.addParam(dllName);
req.addParam(signature);
req.addParam(replacement);
req.addParam(offset);
req.addParam(usage);
return con.request(req).then((res) {
return res.getData()[0];
});
}

View File

@@ -0,0 +1,68 @@
part of spiceapi;
class TouchState {
int id;
int x, y;
bool active = true;
bool updated = true;
TouchState(this.id, this.x, this.y);
}
Future<List<TouchState>> touchRead(Connection con) {
var req = Request("touch", "read");
return con.request(req).then((res) {
// build states list
List<TouchState> states = [];
for (List state in res.getData()) {
states.add(TouchState(
state[0],
state[1],
state[2],
));
}
// return it
return states;
});
}
Future<void> touchWrite(Connection con, List<TouchState> states) async {
if (states.isEmpty) return;
var req = Request("touch", "write");
// add params
for (var state in states) {
var obj = [
state.id,
state.x,
state.y
];
req.addParam(obj);
}
return con.request(req);
}
Future<void> touchWriteReset(Connection con, List<TouchState> states) async {
if (states.isEmpty) return;
var req = Request("touch", "write_reset");
// add params
for (var state in states)
req.addParam(state.id);
return con.request(req);
}
Future<void> touchWriteResetIDs(Connection con, List<int> touchIDs) async {
if (touchIDs.isEmpty) return;
var req = Request("touch", "write_reset");
// add params
for (var id in touchIDs)
req.addParam(id);
return con.request(req);
}

View File

@@ -0,0 +1,15 @@
# Lua Scripting
Supported version: Lua 5.4.3
No proper documentation yet. Check the example scripts if you need this!
For undocumented functions you can find the definitions in the source code (script/api/*.cpp).
They are very similar to what the network API provides.
# Automatic Execution
Create a "script" folder next to spice and put your scripts in there (subfolders allowed).
The prefix specifies when the script will be called:
- `boot_*`: executed on game boot
- `shutdown_*`: executed on game end
- `config_*`: executed when you start spicecfg (mostly for debugging/tests)
Example: "script/boot_patch.py" would be called on game boot.

View File

@@ -0,0 +1,113 @@
-- example script for light effects on IIDX TT stab movement
-- create a folder called "script" next to spice and put me in there
--------------------------------------------------------------------------------
-- settings
tt_duration = 0.25
zero_duration = 0.3141592
loop_delta = 1 / 240
curve_pow = 1 / 4
col_r = 1.0
col_g = 0.0
col_b = 0.0
light_p1_r = "Side Panel Left Avg R"
light_p1_g = "Side Panel Left Avg G"
light_p1_b = "Side Panel Left Avg B"
light_p2_r = "Side Panel Right Avg R"
light_p2_g = "Side Panel Right Avg G"
light_p2_b = "Side Panel Right Avg B"
-- wait for game
while not analogs.read()["Turntable P1"] do yield() end
-- initial state
tt1_last = tonumber(analogs.read()["Turntable P1"].state)
tt2_last = tonumber(analogs.read()["Turntable P2"].state)
tt1_diff_last = 0
tt2_diff_last = 0
tt1_trigger = 0
tt2_trigger = 0
tt1_zero_elapsed = 0
tt2_zero_elapsed = 0
-- main loop
while true do
-- read state
tt1 = tonumber(analogs.read()["Turntable P1"].state)
tt2 = tonumber(analogs.read()["Turntable P2"].state)
time_cur = time()
-- calculate difference
tt1_diff = tt1 - tt1_last
tt2_diff = tt2 - tt2_last
-- fix wrap around
if math.abs(tt1_diff) > 0.5 then tt1_diff = 0 end
if math.abs(tt2_diff) > 0.5 then tt2_diff = 0 end
-- trigger on movement start and direction changes
if (tt1_diff_last == 0 and tt1_diff ~= 0)
or (tt1_diff_last > 0 and tt1_diff < 0)
or (tt1_diff_last < 0 and tt1_diff > 0) then
tt1_trigger = time_cur
end
if (tt2_diff_last == 0 and tt2_diff ~= 0)
or (tt2_diff_last > 0 and tt2_diff < 0)
or (tt2_diff_last < 0 and tt2_diff > 0) then
tt2_trigger = time_cur
end
-- light effects when last trigger is still active
if time_cur - tt1_trigger < tt_duration then
brightness = 1 - ((time_cur - tt1_trigger) / tt_duration) ^ curve_pow
lights.write({[light_p1_r]={state=brightness*col_r}})
lights.write({[light_p1_g]={state=brightness*col_g}})
lights.write({[light_p1_b]={state=brightness*col_b}})
else
lights.write_reset(light_p1_r)
lights.write_reset(light_p1_g)
lights.write_reset(light_p1_b)
end
if time_cur - tt2_trigger < tt_duration then
brightness = 1 - ((time_cur - tt2_trigger) / tt_duration) ^ curve_pow
lights.write({[light_p2_r]={state=brightness*col_r}})
lights.write({[light_p2_g]={state=brightness*col_g}})
lights.write({[light_p2_b]={state=brightness*col_b}})
else
lights.write_reset(light_p2_r)
lights.write_reset(light_p2_g)
lights.write_reset(light_p2_b)
end
-- flush HID light output
lights.update()
-- turntable movement detection
-- doesn't set the diff back to zero unless enough time has passed
if tt1_diff == 0 then
tt1_zero_elapsed = tt1_zero_elapsed + loop_delta
if tt1_zero_elapsed >= zero_duration then
tt1_diff_last = tt1_diff
end
else
tt1_zero_elapsed = 0
tt1_diff_last = tt1_diff
end
if tt2_diff == 0 then
tt2_zero_elapsed = tt2_zero_elapsed + loop_delta
if tt2_zero_elapsed >= zero_duration then
tt2_diff_last = tt2_diff
end
else
tt2_zero_elapsed = 0
tt2_diff_last = tt2_diff
end
-- remember state
tt1_last = tt1
tt2_last = tt2
-- loop end
sleep(loop_delta)
end

View File

@@ -0,0 +1,57 @@
-- script examples
-- no proper documentation yet
-- create a folder called "script" next to spice and put me in there
-- then open the config and if needed select IIDX for the demo
--------------------------------------------------------------------------------
-- sleep for 0.2 seconds
sleep(0.2)
-- log functions
log_misc("example misc")
log_info("example info")
log_warning("example warning")
--log_fatal("this would terminate")
-- print time
log_info(time())
-- show message box
msgbox("You are running the example script! Select IIDX if not already done.")
-- wait until analog is available
while not analogs.read()["Turntable P1"] do yield() end
-- write button state
buttons.write({["P1 Start"]={state=1}})
-- write analog state
analogs.write({["Turntable P1"]={state=0.33}})
-- write light state
lights.write({["P2 Start"]={state=0.8}})
-- import other libraries in "script" folder
--local example = require('script.example')
-- demo
while true do
-- analog animation
analogs.write({["Turntable P2"]={state=math.abs(math.sin(time()))}})
-- button blink
if math.cos(time() * 10) > 0 then
buttons.write({["P1 1"]={state=1}})
else
buttons.write({["P1 1"]={state=0}})
end
-- flush HID light output
lights.update()
-- check for keyboard press
if GetAsyncKeyState(0x20) > 0 then
msgbox("You pressed space!")
end
end

116
api/resources/python/.gitignore vendored Normal file
View File

@@ -0,0 +1,116 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@@ -0,0 +1,14 @@
from .connection import Connection
from .request import Request
from .analogs import *
from .buttons import *
from .card import *
from .coin import *
from .control import *
from .exceptions import *
from .iidx import *
from .info import *
from .keypads import *
from .lights import *
from .memory import *
from .touch import *

View File

@@ -0,0 +1,28 @@
from .connection import Connection
from .request import Request
def analogs_read(con: Connection):
res = con.request(Request("analogs", "read"))
return res.get_data()
def analogs_write(con: Connection, analog_state_list):
req = Request("analogs", "write")
for state in analog_state_list:
req.add_param(state)
con.request(req)
def analogs_write_reset(con: Connection, analog_names=None):
req = Request("analogs", "write_reset")
# reset all analogs
if not analog_names:
con.request(req)
return
# reset specified analogs
for analog_name in analog_names:
req.add_param(analog_name)
con.request(req)

View File

@@ -0,0 +1,28 @@
from .connection import Connection
from .request import Request
def buttons_read(con: Connection):
res = con.request(Request("buttons", "read"))
return res.get_data()
def buttons_write(con: Connection, button_state_list):
req = Request("buttons", "write")
for state in button_state_list:
req.add_param(state)
con.request(req)
def buttons_write_reset(con: Connection, button_names=None):
req = Request("buttons", "write_reset")
# reset all buttons
if not button_names:
con.request(req)
return
# reset specified buttons
for button_name in button_names:
req.add_param(button_name)
con.request(req)

View File

@@ -0,0 +1,9 @@
from .connection import Connection
from .request import Request
def card_insert(con: Connection, unit: int, card_id: str):
req = Request("card", "insert")
req.add_param(unit)
req.add_param(card_id)
con.request(req)

View File

@@ -0,0 +1,20 @@
from .connection import Connection
from .request import Request
def coin_get(con: Connection):
res = con.request(Request("coin", "get"))
return res.get_data()[0]
def coin_set(con: Connection, amount: int):
req = Request("coin", "set")
req.add_param(amount)
con.request(req)
def coin_insert(con: Connection, amount=1):
req = Request("coin", "insert")
if amount != 1:
req.add_param(amount)
con.request(req)

View File

@@ -0,0 +1,139 @@
import os
import socket
from .request import Request
from .response import Response
from .rc4 import rc4
from .exceptions import MalformedRequestException, APIError
class Connection:
""" Container for managing a single connection to the API server.
"""
def __init__(self, host: str, port: int, password: str):
"""Default constructor.
:param host: the host string to connect to
:param port: the port of the host
:param password: the connection password string
"""
self.host = host
self.port = port
self.password = password
self.socket = None
self.cipher = None
self.reconnect()
def reconnect(self, refresh_session=True):
"""Reconnect to the server.
This opens a new connection and closes the previous one, if existing.
"""
# close old socket
self.close()
# create new socket
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(3)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect((self.host, self.port))
# cipher
self.change_password(self.password)
# refresh session
if refresh_session:
from .control import control_session_refresh
control_session_refresh(self)
def change_password(self, password):
"""Allows to change the password on the fly.
The cipher will be rebuilt.
"""
if len(password) > 0:
self.cipher = rc4(password.encode("UTF-8"))
else:
self.cipher = None
def close(self):
"""Close the active connection, if existing."""
# check if socket is existing
if self.socket:
# close and delete socket
self.socket.close()
self.socket = None
def request(self, request: Request):
"""Send a request to the server and receive the answer.
:param request: request object
:return: response object
"""
# check if disconnected
if not self.socket:
raise RuntimeError("No active connection.")
# build data
data = request.to_json().encode("UTF-8") + b"\x00"
if self.cipher:
data_list = list(data)
data_cipher = []
for b in data_list:
data_cipher.append(b ^ next(self.cipher))
data = bytes(data_cipher)
# send request
if os.name != 'nt':
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
self.socket.send(data)
# get answer
answer_data = []
while not len(answer_data) or answer_data[-1] != 0:
# receive data
if os.name != 'nt':
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
receive_data = self.socket.recv(4096)
# check length
if len(receive_data):
# check cipher
if self.cipher:
# add decrypted data
for b in receive_data:
answer_data.append(int(b ^ next(self.cipher)))
else:
# add plaintext
for b in receive_data:
answer_data.append(int(b))
else:
raise RuntimeError("Connection was closed.")
# check for empty response
if len(answer_data) <= 1:
# empty response means the JSON couldn't be parsed
raise MalformedRequestException()
# build response
response = Response(bytes(answer_data[:-1]).decode("UTF-8"))
if len(response.get_errors()):
raise APIError(response.get_errors())
# check ID
req_id = request.get_id()
res_id = response.get_id()
if req_id != res_id:
raise RuntimeError(f"Unexpected response ID: {res_id} (expected {req_id})")
# return response object
return response

View File

@@ -0,0 +1,51 @@
import random
from .connection import Connection
from .request import Request
def control_raise(con: Connection, signal: str):
req = Request("control", "raise")
req.add_param(signal)
con.request(req)
def control_exit(con: Connection, code=None):
req = Request("control", "exit")
if code:
req.add_param(code)
try:
con.request(req)
except RuntimeError:
pass # we expect the connection to get killed
def control_restart(con: Connection):
req = Request("control", "restart")
try:
con.request(req)
except RuntimeError:
pass # we expect the connection to get killed
def control_session_refresh(con: Connection):
res = con.request(Request("control", "session_refresh", req_id=random.randint(1, 2**64)))
# apply new password
password = res.get_data()[0]
con.change_password(password)
def control_shutdown(con: Connection):
req = Request("control", "shutdown")
try:
con.request(req)
except RuntimeError:
pass # we expect the connection to get killed
def control_reboot(con: Connection):
req = Request("control", "reboot")
try:
con.request(req)
except RuntimeError:
pass # we expect the connection to get killed

View File

@@ -0,0 +1,10 @@
class APIError(Exception):
def __init__(self, errors):
super().__init__("\r\n".join(errors))
class MalformedRequestException(Exception):
pass

View File

@@ -0,0 +1,18 @@
from .connection import Connection
from .request import Request
def iidx_ticker_get(con: Connection):
res = con.request(Request("iidx", "ticker_get"))
return res.get_data()
def iidx_ticker_set(con: Connection, text: str):
req = Request("iidx", "ticker_set")
req.add_param(text)
con.request(req)
def iidx_ticker_reset(con: Connection):
req = Request("iidx", "ticker_reset")
con.request(req)

View File

@@ -0,0 +1,17 @@
from .connection import Connection
from .request import Request
def info_avs(con: Connection):
res = con.request(Request("info", "avs"))
return res.get_data()[0]
def info_launcher(con: Connection):
res = con.request(Request("info", "launcher"))
return res.get_data()[0]
def info_memory(con: Connection):
res = con.request(Request("info", "memory"))
return res.get_data()[0]

View File

@@ -0,0 +1,24 @@
from .connection import Connection
from .request import Request
def keypads_write(con: Connection, keypad: int, input_values: str):
req = Request("keypads", "write")
req.add_param(keypad)
req.add_param(input_values)
con.request(req)
def keypads_set(con: Connection, keypad: int, input_values: str):
req = Request("keypads", "set")
req.add_param(keypad)
for value in input_values:
req.add_param(value)
con.request(req)
def keypads_get(con: Connection, keypad: int):
req = Request("keypads", "get")
req.add_param(keypad)
res = con.request(req)
return res.get_data()

Some files were not shown because too many files have changed in this diff Show More