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

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

View File

@@ -0,0 +1,28 @@
from .connection import Connection
from .request import Request
def lights_read(con: Connection):
res = con.request(Request("lights", "read"))
return res.get_data()
def lights_write(con: Connection, light_state_list):
req = Request("lights", "write")
for state in light_state_list:
req.add_param(state)
con.request(req)
def lights_write_reset(con: Connection, light_names=None):
req = Request("lights", "write_reset")
# reset all lights
if not light_names:
con.request(req)
return
# reset specified lights
for light_name in light_names:
req.add_param(light_name)
con.request(req)

View File

@@ -0,0 +1,31 @@
from .connection import Connection
from .request import Request
def memory_write(con: Connection, dll_name: str, data: str, offset: int):
req = Request("memory", "write")
req.add_param(dll_name)
req.add_param(data)
req.add_param(offset)
con.request(req)
def memory_read(con: Connection, dll_name: str, offset: int, size: int):
req = Request("memory", "read")
req.add_param(dll_name)
req.add_param(offset)
req.add_param(size)
res = con.request(req)
return res.get_data()[0]
def memory_signature(con: Connection, dll_name: str, signature: str,
replacement: str, offset: int, usage: int):
req = Request("memory", "signature")
req.add_param(dll_name)
req.add_param(signature)
req.add_param(replacement)
req.add_param(offset)
req.add_param(usage)
res = con.request(req)
return res.get_data()[0]

View File

@@ -0,0 +1,24 @@
def rc4_ksa(key):
n = len(key)
j = 0
s_box = list(range(256))
for i in range(256):
j = (j + s_box[i] + key[i % n]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
return s_box
def rc4_prga(s_box):
i = 0
j = 0
while True:
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
yield s_box[(s_box[i] + s_box[j]) % 256]
def rc4(key):
return rc4_prga(rc4_ksa(key))

View File

@@ -0,0 +1,63 @@
import json
from threading import Lock
class Request:
# global ID pool
GLOBAL_ID = 1
GLOBAL_ID_LOCK = Lock()
def __init__(self, module: str, function: str, req_id=None):
# use global ID
with Request.GLOBAL_ID_LOCK:
if req_id is None:
# reset at max value
Request.GLOBAL_ID += 1
if Request.GLOBAL_ID >= 2 ** 64:
Request.GLOBAL_ID = 1
# get ID and increase by one
req_id = Request.GLOBAL_ID
else:
# carry over ID
Request.GLOBAL_ID = req_id
# remember ID
self._id = req_id
# build data dict
self.data = {
"id": req_id,
"module": module,
"function": function,
"params": []
}
@staticmethod
def from_json(request_json: str):
req = Request("", "", 0)
req.data = json.loads(request_json)
req._id = req.data["id"]
return req
def get_id(self):
return self._id
def to_json(self):
return json.dumps(
self.data,
ensure_ascii=False,
check_circular=False,
allow_nan=False,
indent=None,
separators=(",", ":"),
sort_keys=False
)
def add_param(self, param):
self.data["params"].append(param)

View File

@@ -0,0 +1,30 @@
import json
class Response:
def __init__(self, response_json: str):
self._res = json.loads(response_json)
self._id = self._res["id"]
self._errors = self._res["errors"]
self._data = self._res["data"]
def to_json(self):
return json.dumps(
self._res,
ensure_ascii=True,
check_circular=False,
allow_nan=False,
indent=2,
separators=(",", ": "),
sort_keys=False
)
def get_id(self):
return self._id
def get_errors(self):
return self._errors
def get_data(self):
return self._data

View File

@@ -0,0 +1,21 @@
from .connection import Connection
from .request import Request
def touch_read(con: Connection):
res = con.request(Request("touch", "read"))
return res.get_data()
def touch_write(con: Connection, touch_points):
req = Request("touch", "write")
for state in touch_points:
req.add_param(state)
con.request(req)
def touch_write_reset(con: Connection, touch_ids):
req = Request("touch", "write_reset")
for touch_id in touch_ids:
req.add_param(touch_id)
con.request(req)

View File

@@ -0,0 +1,574 @@
#!/usr/bin/env python3
from datetime import datetime
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.messagebox
try:
import spiceapi
except ModuleNotFoundError:
raise RuntimeError("spiceapi module not installed")
NSEW = tk.N+tk.S+tk.E+tk.W
def api_action(func):
def wrapper(*args, **kwargs):
try:
func(*args, **kwargs)
except spiceapi.APIError as e:
tk.messagebox.showerror(title="API Error", message=str(e))
except ValueError as e:
tk.messagebox.showerror(title="Input Error", message=str(e))
except (ConnectionResetError, BrokenPipeError, RuntimeError) as e:
tk.messagebox.showerror(title="Connection Error", message=str(e))
except AttributeError:
tk.messagebox.showerror(title="Error", message="No active connection.")
except BaseException as e:
tk.messagebox.showerror(title="Exception", message=str(e))
return wrapper
class TextField(ttk.Frame):
"""Simple text field with a scroll bar to the right."""
def __init__(self, parent, read_only=False, **kwargs):
# init frame
ttk.Frame.__init__(self, parent, **kwargs)
self.parent = parent
self.read_only = read_only
# create scroll bar
self.scroll = ttk.Scrollbar(self)
self.scroll.pack(side=tk.RIGHT, fill=tk.Y)
# create text field
self.contents = tk.Text(self, height=1, width=50,
foreground="black", background="#E8E6E0",
insertbackground="black")
self.contents.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
# link scroll bar with text field
self.scroll.config(command=self.contents.yview)
self.contents.config(yscrollcommand=self.scroll.set)
# read only setting
if self.read_only:
self.contents.config(state=tk.DISABLED)
def get_text(self):
"""Get the current text content as string.
:return: text content
"""
return self.contents.get("1.0", tk.END+"-1c")
def set_text(self, text):
"""Set the current text content.
:param text: text content
:return: None
"""
# enable if read only
if self.read_only:
self.contents.config(state=tk.NORMAL)
# delete old content and insert replacement text
self.contents.delete("1.0", tk.END)
self.contents.insert(tk.END, text)
# disable if read only
if self.read_only:
self.contents.config(state=tk.DISABLED)
class ManualTab(ttk.Frame):
"""Manual JSON request/response functinality."""
def __init__(self, app, parent, **kwargs):
# init frame
ttk.Frame.__init__(self, parent, **kwargs)
self.app = app
self.parent = parent
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.columnconfigure(0, weight=1)
# request text field
self.txt_request = TextField(self, read_only=False)
self.txt_request.grid(row=0, column=0, sticky=NSEW, padx=2, pady=2)
self.txt_request.set_text(
'{\n'
' "id": 1,\n'
' "module": "coin",\n'
' "function": "insert",\n'
' "params": []\n'
'}\n'
)
# response text field
self.txt_response = TextField(self, read_only=False)
self.txt_response.grid(row=1, column=0, sticky=NSEW, padx=2, pady=2)
#self.txt_response.contents.config(state=tk.DISABLED)
# send button
self.btn_send = ttk.Button(self, text="Send", command=self.action_send)
self.btn_send.grid(row=2, column=0, sticky=tk.W+tk.E, padx=2, pady=2)
def action_send(self):
"""Gets called when the send button is pressed."""
# check connection
if self.app.connection:
# send request and get response
self.txt_response.set_text("Sending...")
try:
# build request
request = spiceapi.Request.from_json(self.txt_request.get_text())
# send request and get response, measure time
t1 = datetime.now()
response = self.app.connection.request(request)
t2 = datetime.now()
# set response text
self.txt_response.set_text("{}\n\nElapsed time: {} seconds".format(
response.to_json(),
(t2 - t1).total_seconds())
)
except spiceapi.APIError as e:
self.txt_response.set_text(f"Server returned error:\n{e}")
except spiceapi.MalformedRequestException:
self.txt_response.set_text("Malformed request detected.")
except (ConnectionResetError, BrokenPipeError, RuntimeError) as e:
self.txt_response.set_text("Error sending request: " + str(e))
except BaseException as e:
self.txt_response.set_text("General Exception: " + str(e))
else:
# print error
self.txt_response.set_text("No active connection.")
class ControlTab(ttk.Frame):
"""Main control tab."""
def __init__(self, app, parent, **kwargs):
# init frame
ttk.Frame.__init__(self, parent, **kwargs)
self.app = app
self.parent = parent
# scale grid
self.columnconfigure(0, weight=1)
# card
self.card = ttk.Frame(self, padding=(8, 8, 8, 8))
self.card.grid(row=0, column=0, sticky=tk.E+tk.W)
self.card.columnconfigure(0, weight=1)
self.card.columnconfigure(1, weight=1)
self.card_lbl = ttk.Label(self.card, text="Card")
self.card_lbl.grid(row=0, columnspan=2)
self.card_entry = ttk.Entry(self.card)
self.card_entry.insert(tk.END, "E004010000000000")
self.card_entry.grid(row=1, columnspan=2, sticky=NSEW, padx=2, pady=2)
self.card_insert_p1 = ttk.Button(self.card, text="Insert P1", command=self.action_insert_p1)
self.card_insert_p1.grid(row=2, column=0, sticky=NSEW, padx=2, pady=2)
self.card_insert_p2 = ttk.Button(self.card, text="Insert P2", command=self.action_insert_p2)
self.card_insert_p2.grid(row=2, column=1, sticky=NSEW, padx=2, pady=2)
# coin
self.coin = ttk.Frame(self, padding=(8, 8, 8, 8))
self.coin.grid(row=1, column=0, sticky=tk.E+tk.W)
self.coin.columnconfigure(0, weight=1)
self.coin.columnconfigure(1, weight=1)
self.coin.columnconfigure(2, weight=1)
self.coin_lbl = ttk.Label(self.coin, text="Coins")
self.coin_lbl.grid(row=0, columnspan=3)
self.coin_entry = ttk.Entry(self.coin)
self.coin_entry.insert(tk.END, "1")
self.coin_entry.grid(row=1, columnspan=3, sticky=NSEW, padx=2, pady=2)
self.coin_set = ttk.Button(self.coin, text="Set to Amount", command=self.action_coin_set)
self.coin_set.grid(row=2, column=0, sticky=NSEW, padx=2, pady=2)
self.coin_insert = ttk.Button(self.coin, text="Insert Amount", command=self.action_coin_insert)
self.coin_insert.grid(row=2, column=1, sticky=NSEW, padx=2, pady=2)
self.coin_insert = ttk.Button(self.coin, text="Insert Single", command=self.action_coin_insert_single)
self.coin_insert.grid(row=2, column=2, sticky=NSEW, padx=2, pady=2)
@api_action
def action_insert(self, unit: int):
spiceapi.card_insert(self.app.connection, unit, self.card_entry.get())
@api_action
def action_insert_p1(self):
return self.action_insert(0)
@api_action
def action_insert_p2(self):
return self.action_insert(1)
@api_action
def action_coin_set(self):
spiceapi.coin_set(self.app.connection, int(self.coin_entry.get()))
@api_action
def action_coin_insert(self):
spiceapi.coin_insert(self.app.connection, int(self.coin_entry.get()))
@api_action
def action_coin_insert_single(self):
spiceapi.coin_insert(self.app.connection)
class InfoTab(ttk.Frame):
"""The info tab."""
def __init__(self, app, parent, **kwargs):
# init frame
ttk.Frame.__init__(self, parent, **kwargs)
self.app = app
self.parent = parent
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
# info text field
self.txt_info = TextField(self, read_only=True)
self.txt_info.grid(row=0, column=0, sticky=NSEW, padx=2, pady=2)
# refresh button
self.btn_refresh = ttk.Button(self, text="Refresh", command=self.action_refresh)
self.btn_refresh.grid(row=1, column=0, sticky=tk.W+tk.E, padx=2, pady=2)
@api_action
def action_refresh(self):
# get information
avs = spiceapi.info_avs(self.app.connection)
launcher = spiceapi.info_launcher(self.app.connection)
memory = spiceapi.info_memory(self.app.connection)
# build text
avs_text = ""
for k, v in avs.items():
avs_text += f"{k}: {v}\n"
launcher_text = ""
for k, v in launcher.items():
if isinstance(v, list):
launcher_text += f"{k}:\n"
for i in v:
launcher_text += f" {i}\n"
else:
launcher_text += f"{k}: {v}\n"
memory_text = ""
for k, v in memory.items():
memory_text += f"{k}: {v}\n"
# set text
self.txt_info.set_text(
f"AVS:\n{avs_text}\n"
f"Launcher:\n{launcher_text}\n"
f"Memory:\n{memory_text}"
)
class ButtonsTab(ttk.Frame):
"""The buttons tab."""
def __init__(self, app, parent, **kwargs):
# init frame
ttk.Frame.__init__(self, parent, **kwargs)
self.app = app
self.parent = parent
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
# button text field
self.txt_buttons = TextField(self, read_only=True)
self.txt_buttons.grid(row=0, column=0, sticky=NSEW, padx=2, pady=2)
# refresh button
self.btn_refresh = ttk.Button(self, text="Refresh", command=self.action_refresh)
self.btn_refresh.grid(row=1, column=0, sticky=tk.W+tk.E, padx=2, pady=2)
@api_action
def action_refresh(self):
# get states
states = spiceapi.buttons_read(self.app.connection)
# build text
txt = ""
for name, velocity, active in states:
state = "on" if velocity > 0 else "off"
active_txt = "" if active else " (inactive)"
txt += f"{name}: {state}{active_txt}\n"
if len(states) == 0:
txt = "No buttons available."
# set text
self.txt_buttons.set_text(txt)
class AnalogsTab(ttk.Frame):
"""The analogs tab."""
def __init__(self, app, parent, **kwargs):
# init frame
ttk.Frame.__init__(self, parent, **kwargs)
self.app = app
self.parent = parent
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
# button text field
self.txt_analogs = TextField(self, read_only=True)
self.txt_analogs.grid(row=0, column=0, sticky=NSEW, padx=2, pady=2)
# refresh button
self.btn_refresh = ttk.Button(self, text="Refresh", command=self.action_refresh)
self.btn_refresh.grid(row=1, column=0, sticky=tk.W+tk.E, padx=2, pady=2)
@api_action
def action_refresh(self):
# get states
states = spiceapi.analogs_read(self.app.connection)
# build text
txt = ""
for name, value, active in states:
value_txt = round(value, 2)
active_txt = "" if active else " (inactive)"
txt += f"{name}: {value_txt}{active_txt}\n"
if len(states) == 0:
txt = "No analogs available."
# set text
self.txt_analogs.set_text(txt)
class LightsTab(ttk.Frame):
"""The lights tab."""
def __init__(self, app, parent, **kwargs):
# init frame
ttk.Frame.__init__(self, parent, **kwargs)
self.app = app
self.parent = parent
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
# light text field
self.txt_lights = TextField(self, read_only=True)
self.txt_lights.grid(row=0, column=0, sticky=NSEW, padx=2, pady=2)
# refresh button
self.btn_refresh = ttk.Button(self, text="Refresh", command=self.action_refresh)
self.btn_refresh.grid(row=1, column=0, sticky=tk.W+tk.E, padx=2, pady=2)
@api_action
def action_refresh(self):
# get states
states = spiceapi.lights_read(self.app.connection)
# build text
txt = ""
for name, value, active in states:
value_txt = round(value, 2)
active_txt = "" if active else " (inactive)"
txt += f"{name}: {value_txt}{active_txt}\n"
if len(states) == 0:
txt = "No lights available."
# set text
self.txt_lights.set_text(txt)
class MainApp(ttk.Frame):
"""The main application frame."""
def __init__(self, parent, **kwargs):
# init frame
ttk.Frame.__init__(self, parent, **kwargs)
self.parent = parent
self.connection = None
self.tabs = ttk.Notebook(self)
self.tab_control = ControlTab(self, self.tabs)
self.tabs.add(self.tab_control, text="Control")
self.tab_info = InfoTab(self, self.tabs)
self.tabs.add(self.tab_info, text="Info")
self.tab_buttons = ButtonsTab(self, self.tabs)
self.tabs.add(self.tab_buttons, text="Buttons")
self.tab_analogs = AnalogsTab(self, self.tabs)
self.tabs.add(self.tab_analogs, text="Analogs")
self.tab_lights = LightsTab(self, self.tabs)
self.tabs.add(self.tab_lights, text="Lights")
self.tab_manual = ManualTab(self, self.tabs)
self.tabs.add(self.tab_manual, text="Manual")
self.tabs.pack(expand=True, fill=tk.BOTH)
# connection panel
self.frm_connection = ttk.Frame(self)
# host: [field]
self.lbl_host = ttk.Label(self.frm_connection, text="Host:")
self.txt_host = ttk.Entry(self.frm_connection, width=15)
self.lbl_host.columnconfigure(0, weight=1)
self.txt_host.columnconfigure(1, weight=1)
self.txt_host.insert(tk.END, "localhost")
# port: [field]
self.lbl_port = ttk.Label(self.frm_connection, text="Port:")
self.txt_port = ttk.Entry(self.frm_connection, width=5)
self.lbl_port.columnconfigure(2, weight=1)
self.txt_port.columnconfigure(3, weight=1)
self.txt_port.insert(tk.END, "1337")
# pass: [field]
self.lbl_pw = ttk.Label(self.frm_connection, text="Pass:")
self.txt_pw = ttk.Entry(self.frm_connection, width=10)
self.lbl_pw.columnconfigure(4, weight=1)
self.txt_pw.columnconfigure(5, weight=1)
self.txt_pw.insert(tk.END, "debug")
# grid setup
self.lbl_host.grid(row=0, column=0, sticky=tk.W+tk.E, padx=2)
self.txt_host.grid(row=0, column=1, sticky=tk.W+tk.E, padx=2)
self.lbl_port.grid(row=0, column=2, sticky=tk.W+tk.E, padx=2)
self.txt_port.grid(row=0, column=3, sticky=tk.W+tk.E, padx=2)
self.lbl_pw.grid(row=0, column=4, sticky=tk.W+tk.E, padx=2)
self.txt_pw.grid(row=0, column=5, sticky=tk.W+tk.E, padx=2)
self.frm_connection.pack(fill=tk.NONE, pady=2)
# send/connect/disconnect buttons panel
self.frm_connect = ttk.Frame(self)
# connect button
self.btn_connect = ttk.Button(
self.frm_connect,
text="Connect",
command=self.action_connect,
width=10)
# disconnect button
self.btn_disconnect = ttk.Button(
self.frm_connect,
text="Disconnect",
command=self.action_disconnect,
width=10)
# kill button
self.btn_kill = ttk.Button(
self.frm_connect,
text="Kill",
command=self.action_kill,
width=10)
# restart button
self.btn_restart = ttk.Button(
self.frm_connect,
text="Restart",
command=self.action_restart,
width=10)
# grid setup
self.btn_connect.grid(row=0, column=1, sticky=tk.W+tk.E, padx=2)
self.btn_disconnect.grid(row=0, column=2, sticky=tk.W+tk.E, padx=2)
self.btn_kill.grid(row=0, column=3, sticky=tk.W+tk.E, padx=2)
self.btn_restart.grid(row=0, column=4, sticky=tk.W+tk.E, padx=2)
self.frm_connect.pack(fill=tk.NONE, pady=2)
def action_connect(self):
"""Gets called when the connect button is pressed."""
# retrieve connection info
host = self.txt_host.get()
port = self.txt_port.get()
password = self.txt_pw.get()
# check input
if not port.isdigit():
tk.messagebox.showerror("Connection Error", f"Port '{port}' is not a valid number.")
return
# create a new connection
try:
self.connection = spiceapi.Connection(host=host, port=int(port), password=password)
except OSError as e:
tk.messagebox.showerror("Connection Error", "Failed to connect: " + str(e))
return
# print success
tk.messagebox.showinfo("Success", "Connected.")
def action_disconnect(self):
"""Gets called when the disconnect button is pressed."""
# check connection
if self.connection:
# close connection
self.connection.close()
self.connection = None
tk.messagebox.showinfo("Success", "Closed connection.")
else:
# print error
tk.messagebox.showinfo("Error", "No active connection.")
def action_kill(self):
"""Gets called when the kill button is pressed."""
# check connection
if self.connection:
spiceapi.control_exit(self.connection)
self.connection = None
else:
tk.messagebox.showinfo("Error", "No active connection.")
def action_restart(self):
"""Gets called when the restart button is pressed."""
# check connection
if self.connection:
spiceapi.control_restart(self.connection)
self.connection = None
else:
tk.messagebox.showinfo("Error", "No active connection.")
if __name__ == "__main__":
# create root
root = tk.Tk()
root.title("SpiceRemote")
root.geometry("500x300")
# set theme
preferred_theme = "clam"
s = ttk.Style(root)
if preferred_theme in s.theme_names():
s.theme_use(preferred_theme)
# add application
MainApp(root).pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# run root
root.mainloop()

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
import binascii
import spiceapi
import argparse
def patch_string(con, dll_name: str, find: str, replace: str):
while True:
try:
# replace first result
address = spiceapi.memory_signature(
con, dll_name,
binascii.hexlify(bytes(find, "utf-8")).decode("utf-8"),
binascii.hexlify(bytes(replace, "utf-8")).decode("utf-8"),
0, 0)
# print findings
print("{}: {} = {} => {}".format(
dll_name,
hex(address),
find,
replace))
except spiceapi.APIError:
# this happens when the signature wasn't found anymore
break
def main():
# parse args
parser = argparse.ArgumentParser(description="SpiceAPI string replacer")
parser.add_argument("host", type=str, help="The host to connect to")
parser.add_argument("port", type=int, help="The port the host is using")
parser.add_argument("password", type=str, help="The pass the host is using")
parser.add_argument("dll", type=str, help="The DLL to patch")
parser.add_argument("find", type=str, help="The string to find")
parser.add_argument("replace", type=str, help="The string to replace with")
args = parser.parse_args()
# connect
con = spiceapi.Connection(host=args.host, port=args.port, password=args.password)
# replace the string
patch_string(con, args.dll, args.find, args.replace)
if __name__ == "__main__":
main()