mirror of
https://github.com/libratbag/libratbag.git
synced 2026-11-07 15:14:38 +01:00
561 lines
16 KiB
C
561 lines
16 KiB
C
/*
|
|
* Copyright © 2019 Red Hat, Inc.
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice (including the next
|
|
* paragraph) shall be included in all copies or substantial portions of the
|
|
* Software.
|
|
*
|
|
* 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 OR COPYRIGHT HOLDERS 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.
|
|
*/
|
|
|
|
/* JSON parser for ratbag test devices. The JSON format is not fixed and may
|
|
change at any time, but basically looks like this:
|
|
|
|
{
|
|
"profiles": [
|
|
{
|
|
"is_active": bool,
|
|
"is_default": bool,
|
|
"is_disabled": bool,
|
|
"rate": int,
|
|
"report_rates": [ int, ...],
|
|
"capabilities": [int, ...] # profile capabilities
|
|
"resolutions": [
|
|
{
|
|
"xres": int,
|
|
"yres": int,
|
|
"dpi_min", int,
|
|
"dpi_max", int,
|
|
"is_active": bool,
|
|
"is_default": bool,
|
|
"is_disabled": bool,
|
|
"capabilities": [int, ...] # resolution capabilities
|
|
}
|
|
]
|
|
"buttons": [
|
|
"action_type": "<enum>" # none, button, key, special, macro, unknown
|
|
"button": int, # if button
|
|
"key": int, # if key
|
|
"special": "<enum>", # if special, for values see see special_lookup()
|
|
"macro": ["+B", "-B", "t400"] # if macro. uppercase only
|
|
|
|
]
|
|
"leds": [ ]
|
|
},
|
|
{
|
|
... next profile ...
|
|
}
|
|
]
|
|
}
|
|
|
|
ratbagd uses a minimum sane device (1 profile, 1 resolution, etc.) and any
|
|
JSON instructions get merged into that device. Which means you usually
|
|
only need to set those bits you care about being checked.
|
|
*/
|
|
|
|
#include "libratbag-util.h"
|
|
#include "ratbagd-json.h"
|
|
#include "ratbagd-test.h"
|
|
#include <json-glib/json-glib.h>
|
|
#include <libevdev/libevdev.h>
|
|
|
|
/* Hack because we don't do proper data passing */
|
|
static int num_resolutions;
|
|
static int num_buttons;
|
|
static int num_leds;
|
|
|
|
static int parse_error = 0;
|
|
|
|
#define parser_error(element_) { \
|
|
log_error("json: parser error: %s:%d: element '%s'\n", __func__, __LINE__, (element_)); \
|
|
parse_error = -EINVAL; \
|
|
goto out; \
|
|
}
|
|
|
|
static void parse_resolution_member(JsonObject *obj, const gchar *name,
|
|
JsonNode *node, void *data)
|
|
{
|
|
struct ratbag_test_resolution *resolution = data;
|
|
|
|
if (streq(name, "xres")) {
|
|
gboolean v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > 20000)
|
|
parser_error("xres");
|
|
|
|
resolution->xres = v;
|
|
log_verbose("json: xres: %d\n", v);
|
|
} else if (streq(name, "yres")) {
|
|
gboolean v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > 20000)
|
|
parser_error("yres");
|
|
|
|
resolution->yres = v;
|
|
log_verbose("json: yres: %d\n", v);
|
|
} else if (streq(name, "dpi_min")) {
|
|
gboolean v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > 20000)
|
|
parser_error("dpi_min");
|
|
|
|
resolution->dpi_min = v;
|
|
log_verbose("json: dpi_min: %d\n", v);
|
|
} else if (streq(name, "dpi_max")) {
|
|
gboolean v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > 20000)
|
|
parser_error("dpi_min");
|
|
|
|
resolution->dpi_max = v;
|
|
log_verbose("json: dpi_max: %d\n", v);
|
|
} else if (streq(name, "is_active")) {
|
|
gboolean v = json_object_get_boolean_member(obj, name);
|
|
resolution->active = v;
|
|
log_verbose("json: is_active: %d\n", v);
|
|
} else if (streq(name, "is_default")) {
|
|
gboolean v = json_object_get_boolean_member(obj, name);
|
|
resolution->dflt = v;
|
|
log_verbose("json: is_default: %d\n", v);
|
|
} else if (streq(name, "is_disabled")) {
|
|
gboolean v = json_object_get_boolean_member(obj, name);
|
|
resolution->disabled = v;
|
|
log_verbose("json: is_disabled: %d\n", v);
|
|
} else if (streq(name, "capabilities")) {
|
|
JsonArray *a = json_object_get_array_member(obj, name);
|
|
|
|
assert(json_array_get_length(a) < ARRAY_LENGTH(resolution->caps));
|
|
for (size_t s = 0; s < json_array_get_length(a); s++) {
|
|
int v = json_array_get_int_element(a, s);
|
|
if (v > RATBAG_RESOLUTION_CAP_DISABLE)
|
|
parser_error("capabilities");
|
|
resolution->caps[s] = v;
|
|
}
|
|
|
|
log_verbose("json: caps: %d %d %d %d %d...\n",
|
|
resolution->caps[0],
|
|
resolution->caps[1],
|
|
resolution->caps[2],
|
|
resolution->caps[3],
|
|
resolution->caps[4]);
|
|
} else {
|
|
log_error("json: unknown resolution key '%s'\n", name);
|
|
parse_error = -EINVAL;
|
|
}
|
|
out:
|
|
return;
|
|
}
|
|
|
|
static void parse_resolution(JsonNode *node,
|
|
struct ratbag_test_resolution *resolution)
|
|
{
|
|
JsonObject *obj = json_node_get_object(node);
|
|
|
|
json_object_foreach_member(obj, parse_resolution_member, resolution);
|
|
}
|
|
|
|
static void parse_led_member(JsonObject *obj, const gchar *name,
|
|
JsonNode *node, void *data)
|
|
{
|
|
struct ratbag_test_led *led = data;
|
|
|
|
if (streq(name, "mode")) {
|
|
gboolean v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > RATBAG_LED_BREATHING)
|
|
parser_error("mode");
|
|
|
|
led->mode = v;
|
|
log_verbose("json: mode: %d\n", v);
|
|
} else if (streq(name, "duration")) {
|
|
gboolean v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > 10000)
|
|
parser_error("duration");
|
|
|
|
led->ms = v;
|
|
log_verbose("json: duration: %d\n", v);
|
|
} else if (streq(name, "brightness")) {
|
|
gboolean v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > 100)
|
|
parser_error("brightness");
|
|
|
|
led->brightness = v;
|
|
log_verbose("json: brightness: %d\n", v);
|
|
} else if (streq(name, "color")) {
|
|
JsonArray *a = json_object_get_array_member(obj, name);
|
|
|
|
assert(json_array_get_length(a) == 3);
|
|
led->color.red = json_array_get_int_element(a, 0);
|
|
led->color.green = json_array_get_int_element(a, 1);
|
|
led->color.blue = json_array_get_int_element(a, 2);
|
|
log_verbose("json: color: %02x%02x%02x\n",
|
|
led->color.red,
|
|
led->color.green,
|
|
led->color.blue);
|
|
} else {
|
|
log_error("json: unknown led key '%s'\n", name);
|
|
parse_error = -EINVAL;
|
|
}
|
|
out:
|
|
return;
|
|
}
|
|
|
|
static void parse_led(JsonNode *node, struct ratbag_test_led *led)
|
|
{
|
|
JsonObject *obj = json_node_get_object(node);
|
|
|
|
json_object_foreach_member(obj, parse_led_member, led);
|
|
}
|
|
|
|
static enum ratbag_button_action_special
|
|
special_lookup(const char *string)
|
|
{
|
|
const struct {
|
|
const char *key;
|
|
enum ratbag_button_action_special special;
|
|
} lut[] = {
|
|
{ "invalid", RATBAG_BUTTON_ACTION_SPECIAL_INVALID},
|
|
{ "unknown", RATBAG_BUTTON_ACTION_SPECIAL_UNKNOWN},
|
|
{ "doubleclick", RATBAG_BUTTON_ACTION_SPECIAL_DOUBLECLICK},
|
|
{ "wheel-left", RATBAG_BUTTON_ACTION_SPECIAL_WHEEL_LEFT},
|
|
{ "wheel-right", RATBAG_BUTTON_ACTION_SPECIAL_WHEEL_RIGHT},
|
|
{ "wheel-up", RATBAG_BUTTON_ACTION_SPECIAL_WHEEL_UP},
|
|
{ "wheel-down", RATBAG_BUTTON_ACTION_SPECIAL_WHEEL_DOWN},
|
|
{ "ratchet-mode-switch", RATBAG_BUTTON_ACTION_SPECIAL_RATCHET_MODE_SWITCH},
|
|
{ "resolution-cycle-up", RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_CYCLE_UP},
|
|
{ "resolution-cycle-down", RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_CYCLE_DOWN},
|
|
{ "resolution-up", RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_UP},
|
|
{ "resolution-down", RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_DOWN},
|
|
{ "resolution-alternate", RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_ALTERNATE},
|
|
{ "resolution-default", RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_DEFAULT},
|
|
{ "profile-cycle-up", RATBAG_BUTTON_ACTION_SPECIAL_PROFILE_CYCLE_UP},
|
|
{ "profile-cycle-down", RATBAG_BUTTON_ACTION_SPECIAL_PROFILE_CYCLE_DOWN},
|
|
{ "profile-up", RATBAG_BUTTON_ACTION_SPECIAL_PROFILE_UP},
|
|
{ "profile-down", RATBAG_BUTTON_ACTION_SPECIAL_PROFILE_DOWN},
|
|
{ "second-mode", RATBAG_BUTTON_ACTION_SPECIAL_SECOND_MODE},
|
|
{ "battery-level", RATBAG_BUTTON_ACTION_SPECIAL_BATTERY_LEVEL},
|
|
};
|
|
|
|
for (size_t i = 0; i < ARRAY_LENGTH(lut); i++) {
|
|
if (streq(string, lut[i].key))
|
|
return lut[i].special;
|
|
}
|
|
|
|
parser_error("special");
|
|
|
|
out:
|
|
return RATBAG_BUTTON_ACTION_SPECIAL_INVALID;
|
|
}
|
|
|
|
static enum ratbag_button_action_type
|
|
action_type_lookup(const char *string)
|
|
{
|
|
const struct {
|
|
const char *key;
|
|
enum ratbag_button_action_type type;
|
|
} lut[] = {
|
|
{ "none", RATBAG_BUTTON_ACTION_TYPE_NONE},
|
|
{ "button", RATBAG_BUTTON_ACTION_TYPE_BUTTON},
|
|
{ "special", RATBAG_BUTTON_ACTION_TYPE_SPECIAL},
|
|
{ "key", RATBAG_BUTTON_ACTION_TYPE_KEY},
|
|
{ "macro", RATBAG_BUTTON_ACTION_TYPE_MACRO},
|
|
{ "unknown", RATBAG_BUTTON_ACTION_TYPE_UNKNOWN},
|
|
};
|
|
|
|
for (size_t i = 0; i < ARRAY_LENGTH(lut); i++) {
|
|
if (streq(string, lut[i].key))
|
|
return lut[i].type;
|
|
}
|
|
|
|
parser_error("action_type");
|
|
|
|
out:
|
|
return RATBAG_BUTTON_ACTION_TYPE_UNKNOWN;
|
|
}
|
|
|
|
static inline struct ratbag_test_macro_event
|
|
parse_macro(const char *m)
|
|
{
|
|
struct ratbag_test_macro_event event = {
|
|
.type = RATBAG_MACRO_EVENT_INVALID,
|
|
.value = 0,
|
|
};
|
|
char keyname[32];
|
|
|
|
if (strlen(m) < 2)
|
|
goto out;
|
|
|
|
switch(m[0]) {
|
|
case '+':
|
|
event.type = RATBAG_MACRO_EVENT_KEY_PRESSED;
|
|
snprintf(keyname, sizeof(keyname), "KEY_%s", m + 1);
|
|
event.value = libevdev_event_code_from_name(EV_KEY,
|
|
keyname);
|
|
log_verbose("json: macro: +%s\n", keyname);
|
|
break;
|
|
case '-':
|
|
event.type = RATBAG_MACRO_EVENT_KEY_RELEASED;
|
|
snprintf(keyname, sizeof(keyname), "KEY_%s", m + 1);
|
|
event.value = libevdev_event_code_from_name(EV_KEY,
|
|
keyname);
|
|
log_verbose("json: macro: -%s\n", keyname);
|
|
break;
|
|
case 't':
|
|
event.type = RATBAG_MACRO_EVENT_WAIT;
|
|
event.value = atoi(m+1);
|
|
log_verbose("json: macro: t%d\n", event.value);
|
|
break;
|
|
default:
|
|
parser_error("macro");
|
|
break;
|
|
}
|
|
|
|
out:
|
|
return event;
|
|
}
|
|
|
|
|
|
static void parse_button_member(JsonObject *obj, const gchar *name,
|
|
JsonNode *node, void *data)
|
|
{
|
|
struct ratbag_test_button *button = data;
|
|
|
|
if (streq(name, "action_type")) {
|
|
const gchar* v = json_object_get_string_member(obj, name);
|
|
button->action_type = action_type_lookup(v);
|
|
log_verbose("json: action_type: %s\n", v);
|
|
} else if (streq(name, "button")) {
|
|
gint v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > 32)
|
|
parser_error("button");
|
|
|
|
button->button = v;
|
|
log_verbose("json: button: %d\n", v);
|
|
} else if (streq(name, "key")) {
|
|
gint v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > KEY_MAX)
|
|
parser_error("key");
|
|
|
|
button->key = v;
|
|
log_verbose("json: key: %d\n", v);
|
|
} else if (streq(name, "special")) {
|
|
const gchar *v = json_object_get_string_member(obj, name);
|
|
button->special = special_lookup(v);
|
|
log_verbose("json: special: %s\n", v);
|
|
} else if (streq(name, "macro")) {
|
|
JsonArray *a = json_object_get_array_member(obj, name);
|
|
|
|
assert(json_array_get_length(a) < ARRAY_LENGTH(button->macro));
|
|
for (size_t s = 0; s < json_array_get_length(a); s++) {
|
|
const gchar *v = json_array_get_string_element(a, s);
|
|
button->macro[s] = parse_macro(v);
|
|
}
|
|
} else {
|
|
log_error("json: unknown button key '%s'\n", name);
|
|
parse_error = -EINVAL;
|
|
}
|
|
out:
|
|
return;
|
|
}
|
|
|
|
static void parse_button(JsonNode *node, struct ratbag_test_button *button)
|
|
{
|
|
JsonObject *obj = json_node_get_object(node);
|
|
|
|
json_object_foreach_member(obj, parse_button_member, button);
|
|
}
|
|
|
|
static void parse_profile_member(JsonObject *obj, const gchar *name,
|
|
JsonNode *node, void *data)
|
|
{
|
|
struct ratbag_test_profile *profile = data;
|
|
|
|
if (streq(name, "name")) {
|
|
const char *v = json_object_get_string_member(obj, name);
|
|
|
|
if (v == NULL)
|
|
parser_error("name");
|
|
|
|
free(profile->name);
|
|
profile->name = strdup_safe(v);
|
|
log_verbose("name: %s\n", v);
|
|
} else if (streq(name, "is_default")) {
|
|
gboolean v = json_object_get_boolean_member(obj, name);
|
|
profile->dflt = v;
|
|
log_verbose("json: is_default: %d\n", v);
|
|
} else if (streq(name, "is_active")) {
|
|
gboolean v = json_object_get_boolean_member(obj, name);
|
|
profile->active = v;
|
|
log_verbose("json: is_active: %d\n", v);
|
|
} else if (streq(name, "is_disabled")) {
|
|
gboolean v = json_object_get_boolean_member(obj, name);
|
|
profile->disabled = v;
|
|
log_verbose("json: is_disabled: %d\n", v);
|
|
} else if (streq(name, "rate")) {
|
|
gboolean v = json_object_get_int_member(obj, name);
|
|
|
|
if (v < 0 || v > 20000)
|
|
parser_error("rate");
|
|
|
|
profile->hz = v;
|
|
log_verbose("json: rate: %d\n", v);
|
|
} else if (streq(name, "report_rates")) {
|
|
JsonArray *a = json_object_get_array_member(obj, name);
|
|
assert(json_array_get_length(a) < ARRAY_LENGTH(profile->report_rates));
|
|
for (size_t s = 0; s < json_array_get_length(a); s++) {
|
|
int v = json_array_get_int_element(a, s);
|
|
if (v < 0 || v > 20000)
|
|
parser_error("report_rate");
|
|
profile->report_rates[s] = v;
|
|
}
|
|
|
|
log_verbose("json: report rates: %d %d %d %d %d\n",
|
|
profile->report_rates[0],
|
|
profile->report_rates[1],
|
|
profile->report_rates[2],
|
|
profile->report_rates[3],
|
|
profile->report_rates[4]);
|
|
} else if (streq(name, "capabilities")) {
|
|
JsonArray *a = json_object_get_array_member(obj, name);
|
|
|
|
assert(json_array_get_length(a) < ARRAY_LENGTH(profile->caps));
|
|
for (size_t s = 0; s < json_array_get_length(a); s++) {
|
|
int v = json_array_get_int_element(a, s);
|
|
profile->caps[s] = v;
|
|
}
|
|
|
|
log_verbose("json: caps: %d %d %d %d %d...\n",
|
|
profile->caps[0],
|
|
profile->caps[1],
|
|
profile->caps[2],
|
|
profile->caps[3],
|
|
profile->caps[4]);
|
|
} else if (streq(name, "resolutions")) {
|
|
JsonArray *a = json_object_get_array_member(obj, name);
|
|
GList *list = json_array_get_elements(a);
|
|
GList *l = list;
|
|
int idx = 0;
|
|
while (l != NULL) {
|
|
log_verbose("json: processing resolution %d\n", idx);
|
|
parse_resolution(l->data, &profile->resolutions[idx]);
|
|
l = g_list_next(l);
|
|
idx++;
|
|
}
|
|
g_list_free(list);
|
|
num_resolutions = max(idx, num_resolutions);
|
|
} else if (streq(name, "leds")) {
|
|
JsonArray *a = json_object_get_array_member(obj, name);
|
|
GList *list = json_array_get_elements(a);
|
|
GList *l = list;
|
|
int idx = 0;
|
|
while (l != NULL) {
|
|
log_verbose("json: processing LED %d\n", idx);
|
|
parse_led(l->data, &profile->leds[idx]);
|
|
l = g_list_next(l);
|
|
idx++;
|
|
}
|
|
g_list_free(list);
|
|
num_leds = max(idx, num_leds);
|
|
} else if (streq(name, "buttons")) {
|
|
JsonArray *a = json_object_get_array_member(obj, name);
|
|
GList *list = json_array_get_elements(a);
|
|
GList *l = list;
|
|
int idx = 0;
|
|
while (l != NULL) {
|
|
log_verbose("json: processing button %d\n", idx);
|
|
parse_button(l->data, &profile->buttons[idx]);
|
|
l = g_list_next(l);
|
|
idx++;
|
|
}
|
|
g_list_free(list);
|
|
num_buttons = max(idx, num_buttons);
|
|
} else {
|
|
log_error("json: unknown profile key '%s'\n", name);
|
|
parse_error = -EINVAL;
|
|
}
|
|
out:
|
|
return;
|
|
}
|
|
|
|
static void parse_profile(JsonNode *node,
|
|
struct ratbag_test_profile *profile)
|
|
{
|
|
JsonObject *obj = json_node_get_object(node);
|
|
|
|
json_object_foreach_member(obj, parse_profile_member, profile);
|
|
}
|
|
|
|
/* declared here because this isn't really public API, we just need to
|
|
* access it from the tests */
|
|
int ratbagd_parse_json(const char *data, struct ratbag_test_device *device)
|
|
{
|
|
g_autoptr(JsonParser) parser = NULL;
|
|
JsonNode *root;
|
|
JsonObject *obj;
|
|
JsonArray *arr;
|
|
int r = -EINVAL;
|
|
g_autoptr(GError) error = NULL;
|
|
g_autoptr(GList) list = NULL;
|
|
|
|
error = 0;
|
|
parser = json_parser_new();
|
|
if (!json_parser_load_from_data(parser, data, strlen(data), &error)) {
|
|
log_error("Failed to load JSON: %s\n", error->message);
|
|
r = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
log_verbose("json: data: %s\n", data);
|
|
|
|
root = json_parser_get_root(parser);
|
|
if (JSON_NODE_TYPE(root) != JSON_NODE_OBJECT)
|
|
parser_error("root");
|
|
|
|
obj = json_node_get_object(root);
|
|
arr = json_object_get_array_member(obj, "profiles");
|
|
if (!arr)
|
|
parser_error("profiles");
|
|
|
|
/* Our test device is preloaded with sane defaults, let's keep those */
|
|
num_resolutions = device->num_resolutions;
|
|
num_buttons = device->num_buttons;
|
|
num_leds = device->num_leds;
|
|
|
|
list = json_array_get_elements(arr);
|
|
GList *l = list;
|
|
int idx = 0;
|
|
while (l != NULL) {
|
|
log_verbose("json: processing profile %d\n", idx);
|
|
parse_profile(l->data, &device->profiles[idx]);
|
|
if (parse_error != 0)
|
|
goto out;;
|
|
l = g_list_next(l);
|
|
idx++;
|
|
}
|
|
device->num_profiles = idx;
|
|
device->num_resolutions = num_resolutions;
|
|
device->num_buttons = num_buttons;
|
|
device->num_leds = num_leds;
|
|
|
|
r = 0;
|
|
out:
|
|
return r;
|
|
}
|