feat: Generate identifiers for communication

In order to improve security around the command processing, I plan to
upgrade the communication protocol to use a fixed structure with an
optional payload. As a first pass on this, I added a script to generate
the IDs automatically from the configuration, as well as a script to map
the module names and log levels to integer IDs.

This also replaces the manual lmap_* and array_get_index lookup tables,
so I don't have to always deal with creating an entry in constants.h and
command.c and making sure they are in sync.
lipc-refactor
nirenjan 2026-04-22 09:02:17 -07:00
parent 772017661d
commit 5fecdd3929
11 changed files with 523 additions and 152 deletions

View File

@ -208,40 +208,9 @@ static void cmd_config(char *buffer, int *buflen, int argc, char **argv)
ERR_fmt("Unknown subcommand '%s' for 'config' command", argv[1]);
}
struct level_map {
int level;
const char *string;
};
static int lmap_get_level(const struct level_map *map, const char *string, int notfound)
{
int i;
for (i = 0; map[i].string != NULL; i++) {
if (strcasecmp(map[i].string, string) == 0) {
return map[i].level;
}
}
return notfound;
}
static const char *lmap_get_string(const struct level_map *map, int level)
{
int i;
for (i = 0; map[i].string != NULL; i++) {
if (map[i].level == level) {
return map[i].string;
}
}
return NULL;
}
#define DATA_LMAP(map, level, resp) do {\
#define DATA_LMAP(level, resp) do {\
int input_level_ ## __LINE__ = level; \
const char *lmap_level_ ## __LINE__ = lmap_get_string(map, input_level_ ## __LINE__); \
const char *lmap_level_ ## __LINE__ = lookup_level_by_id(input_level_ ## __LINE__); \
char lmap_unknown_level ## __LINE__[32] = {0}; \
if (lmap_level_ ## __LINE__ == NULL) { \
snprintf(lmap_unknown_level ## __LINE__, sizeof(lmap_unknown_level ## __LINE__), \
@ -252,47 +221,8 @@ static const char *lmap_get_string(const struct level_map *map, int level)
} while(0)
static int array_find_index(const char **array, int nmemb, const char *string)
{
int i;
for (i = 0; i < nmemb; i++) {
if (strcasecmp(array[i], string) == 0) {
return i;
}
}
return nmemb;
}
static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
{
static const char *modules[X52D_MOD_MAX] = {
[X52D_MOD_CONFIG] = "config",
[X52D_MOD_CLOCK] = "clock",
[X52D_MOD_DEVICE] = "device",
[X52D_MOD_IO] = "io",
[X52D_MOD_LED] = "led",
[X52D_MOD_MOUSE] = "mouse",
[X52D_MOD_COMMAND] = "command",
[X52D_MOD_CLIENT] = "client",
[X52D_MOD_NOTIFY] = "notify",
[X52D_MOD_KEYBOARD_LAYOUT] = "keyboard_layout",
};
// This corresponds to the levels in pinelog
static const struct level_map loglevels[] = {
{PINELOG_LVL_NOTSET, "default"},
{PINELOG_LVL_NONE, "none"},
{PINELOG_LVL_FATAL, "fatal"},
{PINELOG_LVL_ERROR, "error"},
{PINELOG_LVL_WARNING, "warning"},
{PINELOG_LVL_INFO, "info"},
{PINELOG_LVL_DEBUG, "debug"},
{PINELOG_LVL_TRACE, "trace"},
{0, NULL},
};
if (argc < 2) {
ERR("Insufficient arguments for 'logging' command");
return;
@ -301,13 +231,13 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
// logging show [module]
MATCH(1, "show") {
if (argc == 2) {
DATA_LMAP(loglevels, pinelog_get_level(), "global");
DATA_LMAP(pinelog_get_level(), "global");
} else if (argc == 3) {
int module = array_find_index(modules, X52D_MOD_MAX, argv[2]);
if (module == X52D_MOD_MAX) {
int module = lookup_module_by_name(argv[2]);
if (module == INT_MAX) {
ERR_fmt("Invalid module '%s'", argv[2]);
} else {
DATA_LMAP(loglevels, pinelog_get_module_level(module), argv[2]);
DATA_LMAP(pinelog_get_module_level(module), argv[2]);
}
} else {
ERR_fmt("Unexpected arguments for 'logging show' command; got %d, expected 2 or 3", argc);
@ -319,7 +249,7 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
// logging set [module] <level>
MATCH(1, "set") {
if (argc == 3) {
int level = lmap_get_level(loglevels, argv[2], INT_MAX);
int level = lookup_level_by_name(argv[2]);
if (level == INT_MAX) {
ERR_fmt("Unknown level '%s' for 'logging set' command", argv[2]);
} else if (level == PINELOG_LVL_NOTSET) {
@ -329,10 +259,10 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
OK("logging", "set", argv[2]);
}
} else if (argc == 4) {
int level = lmap_get_level(loglevels, argv[3], INT_MAX);
int module = array_find_index(modules, X52D_MOD_MAX, argv[2]);
int level = lookup_level_by_name(argv[3]);
int module = lookup_module_by_name(argv[2]);
if (module == X52D_MOD_MAX) {
if (module == INT_MAX) {
ERR_fmt("Invalid module '%s'", argv[2]);
return;
}

View File

@ -0,0 +1,54 @@
{
"_comment": "The configuration registry is a historic record of\nall configuration identifiers. Do NOT edit this file manually, or else, the\ncommunication protocol may break.",
"sections": {
"CLOCK": 1,
"LED": 2,
"BRIGHTNESS": 3,
"MOUSE": 4,
"PROFILES": 5
},
"options": {
"CLOCK": {
"ENABLED": 1,
"PRIMARYISLOCAL": 2,
"SECONDARY": 3,
"TERTIARY": 4,
"FORMATPRIMARY": 5,
"FORMATSECONDARY": 6,
"FORMATTERTIARY": 7,
"DATEFORMAT": 8
},
"LED": {
"FIRE": 1,
"THROTTLE": 2,
"A": 3,
"B": 4,
"D": 5,
"E": 6,
"T1": 7,
"T2": 8,
"T3": 9,
"POV": 10,
"CLUTCH": 11
},
"BRIGHTNESS": {
"MFD": 1,
"LED": 2
},
"MOUSE": {
"ENABLED": 1,
"SENSITIVITY": 2,
"SPEED": 3,
"REVERSESCROLL": 4,
"ISOMETRICMODE": 5,
"CURVEFACTOR": 6,
"DEADZONE": 7
},
"PROFILES": {
"DIRECTORY": 1,
"CLUTCHENABLED": 2,
"CLUTCHLATCHED": 3,
"KEYBOARDLAYOUT": 4
}
}
}

View File

@ -26,19 +26,6 @@
#define X52D_MAX_CLIENTS 63
enum {
X52D_MOD_CONFIG,
X52D_MOD_CLOCK,
X52D_MOD_DEVICE,
X52D_MOD_IO,
X52D_MOD_LED,
X52D_MOD_MOUSE,
X52D_MOD_COMMAND,
X52D_MOD_CLIENT,
X52D_MOD_NOTIFY,
X52D_MOD_KEYBOARD_LAYOUT,
X52D_MOD_MAX
};
#include "module-map.h" // For module IDs
#endif // !defined X52D_CONST_H

View File

@ -1,4 +1,33 @@
# x52d (dep_config_h: Meson build-config.h; private API is daemon/config.h)
config_defs = custom_target('config-defs',
depend_files: ['x52d_map_config.py', 'x52d.conf'],
input: [
'x52d.conf',
'config_registry.json'
],
output: [
'config-defs.h',
'config-defs.c',
'config_defs.py'
],
command: [
python, meson.current_source_dir() / 'x52d_map_config.py',
'@INPUT0@', '@INPUT1@',
'@OUTPUT0@', '@OUTPUT1@', '@OUTPUT2@'
])
module_defs = custom_target('module-defs',
depend_files: ['x52d_gen_module.py', 'module_defs.py'],
output: ['module-map.h', 'module-map.c'],
command: [python, meson.current_source_dir() / 'x52d_gen_module.py',
'@OUTPUT0@', '@OUTPUT1@'])
slib_comm_defs = static_library('x52dcommdefs',
[config_defs[1],
module_defs[1],
'name-id-map.c',
])
libx52dcomm_version = '1.0.0'
libx52dcomm_sources = [
@ -44,7 +73,7 @@ x52d_sources = [
dep_threads = dependency('threads')
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is only for x52ctl.
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io]
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io, slib_comm_defs]
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_math, dep_intl, dep_config_h]
x52d_cflags = []

View File

@ -0,0 +1,28 @@
"""Module name to identifier mapping"""
from enum import Enum
class Module(Enum):
"""Module name to identifier"""
CONFIG = 0
CLOCK = 1
DEVICE = 2
IO = 3
LED = 4
MOUSE = 5
COMMAND = 6
CLIENT = 7
NOTIFY = 8
KEYBOARD_LAYOUT = 9
class LogLevel(Enum):
"""Map log level names to pinelog levels"""
# This is hard coded to the pinelog levels
NOTSET = -2
NONE = -1
FATAL = 0
ERROR = 1
WARNING = 2
INFO = 3
DEBUG = 4
TRACE = 5

View File

@ -0,0 +1,59 @@
/*
* Name ID map - needed to map module/loglevel names to numeric v alues
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stddef.h>
#include <string.h>
#include "name-id-map.h"
#include "module-map.h"
static int map_get_id(const struct name_id_map *map, const char *string)
{
int i;
for (i = 0; map[i].name != NULL; i++) {
if (strcasecmp(map[i].name, string) == 0) {
return map[i].id;
}
}
// We've broken out of the loop, return the current ID
return map[i].id;
}
static const char *map_get_name(const struct name_id_map *map, int id)
{
int i;
for (i = 0; map[i].name != NULL; i++) {
if (map[i].id == id) {
return map[i].name;
}
}
return NULL;
}
int lookup_module_by_name(const char *name)
{
return map_get_id(module_map, name);
}
const char * lookup_module_by_id(int id)
{
return map_get_name(module_map, id);
}
int lookup_level_by_name(const char *name)
{
return map_get_id(loglevel_map, name);
}
const char * lookup_level_by_id(int id)
{
return map_get_name(loglevel_map, id);
}

View File

@ -0,0 +1,19 @@
/*
* Name ID map - needed to map module/loglevel names to numeric v alues
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#ifndef NAME_ID_MAP_H
#define NAME_ID_MAP_H
struct name_id_map {
char *name;
int id;
};
extern const struct name_id_map module_map[];
extern const struct name_id_map loglevel_map[];
#endif // !defined NAME_ID_MAP_H

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Generate the module name to map for use by the daemon"""
import os.path
import sys
import module_defs
def main():
if len(sys.argv) != 3:
print("Usage: {sys.argv[0]} <output-header> <output-source>", file=sys.stderr)
sys.exit(1)
with open(sys.argv[1], 'w', encoding='utf-8') as out_fd:
# Generate the header
print("// Autogenerated module/loglevel header - DO NOT EDIT\n",
file=out_fd)
include_guard = os.path.basename(sys.argv[1]).replace('-', '_').replace('.', '_').upper()
print(f"#ifndef {include_guard}", file=out_fd)
print(f"#define {include_guard}\n", file=out_fd)
for mod in module_defs.Module:
print(f"#define X52D_MOD_{mod.name} {mod.value}", file=out_fd)
print(f"#define X52D_MOD_GLOBAL 0xFF", file=out_fd)
print(f"#define X52D_MOD_MAX {len(module_defs.Module)}\n", file=out_fd)
print(f"int lookup_module_by_name(const char *name);", file=out_fd)
print(f"const char * lookup_module_by_id(int id);", file=out_fd)
print(f"int lookup_level_by_name(const char *name);", file=out_fd)
print(f"const char * lookup_level_by_id(int id);", file=out_fd)
print(f"\n#endif // !defined {include_guard}", file=out_fd)
with open(sys.argv[2], 'w', encoding='utf-8') as out_fd:
print("// Autogenerated module/loglevel tables - DO NOT EDIT\n",
file=out_fd)
print('#include <stddef.h>', file=out_fd)
print('#include <limits.h>\n', file=out_fd)
print(f'#include "{os.path.basename(sys.argv[1])}"', file=out_fd)
print('#include "name-id-map.h"\n', file=out_fd)
print('const struct name_id_map module_map[] = {', file=out_fd)
for mod in module_defs.Module:
print(f' {{ "{mod.name.lower()}", {mod.value} }},', file=out_fd)
print(' { NULL, INT_MAX }', file=out_fd)
print('};\n', file=out_fd)
print('const struct name_id_map loglevel_map[] = {', file=out_fd)
for level in module_defs.LogLevel:
if level == module_defs.LogLevel.NOTSET:
level_name = 'default'
else:
level_name = level.name.lower()
print(f' {{ "{level_name}", {level.value} }},', file=out_fd)
print(' { NULL, INT_MAX }', file=out_fd)
print('};\n', file=out_fd)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Read the default configuration file, and create ID enums for sections
and options.
"""
import argparse
import configparser
import json
import os.path
from collections import defaultdict
from itertools import count
from pprint import pprint
class ConfigToEnum:
"""ConfigToEnum scans a configuration file, and dumps the secions and
options within a section as Python Enums"""
REGISTRY_COMMENT = """The configuration registry is a historic record of
all configuration identifiers. Do NOT edit this file manually, or else, the
communication protocol may break."""
def __init__(self, cfg_file, config_ids):
"""Initialize the object"""
self.config = configparser.ConfigParser(default_section=None, interpolation=None)
self.config.optionxform = str
self.config.read(cfg_file)
self.registry_file = config_ids
try:
with open(self.registry_file, encoding='utf-8') as regfd:
self.config_ids = json.load(regfd)
except Exception:
# On any error, ignore it and start with a clean slate
self.config_ids = {}
self.sections = {}
self.options = {}
def parse(self):
"""Parse the config object and assign IDs"""
self._parse_sections()
for section in self.config.sections():
self._parse_options(section)
def _parse_sections(self):
"""Assign IDs to each section"""
sections = {}
unassigned = []
for section in self.config.sections():
section = section.upper()
section_id = self.config_ids.get('sections', {}).get(section)
if section_id is None:
unassigned.append(section)
else:
sections[section] = section_id
if not sections:
counter = count(1)
else:
counter = count(max(sections.values()) + 1)
sections.update({k:v for k, v in zip(unassigned, counter)})
orig_sections = self.config_ids.get('sections', {})
sections.update({k:v for k, v in orig_sections.items() if k not in sections})
self.sections = sections
def _parse_options(self, section):
options = {}
unassigned = []
for option in self.config.options(section):
option = option.upper()
section = section.upper()
option_id = self.config_ids.get('options', {}).get(section, {}).get(option)
if option_id is None:
unassigned.append(option)
else:
options[option] = option_id
if not options:
counter = count(1)
else:
counter = count(max(options.values()) + 1)
options.update({k:v for k, v in zip(unassigned, counter)})
orig_options = self.config_ids.get('options', {}).get(section, {})
# Make sure that we have all the entries already
options.update({k:v for k, v in orig_options.items() if k not in options})
self.options[section] = options
def save_registry(self):
"""Save the generated registry"""
registry = {
"_comment": self.REGISTRY_COMMENT,
"sections": self.sections,
"options": self.options,
}
with open(self.registry_file, 'w', encoding='utf-8') as regfd:
json.dump(registry, regfd, indent=4)
def generate_c_definitions(self, output_header, output_source):
"""Generate the C definitions"""
with open(output_header, 'w', encoding='utf-8') as out_fd:
include_guard = os.path.basename(output_header).replace('-', '_').replace('.', '_').upper()
print("// Autogenerated config identifiers - DO NOT EDIT\n", file=out_fd)
print(f"#ifndef {include_guard}", file=out_fd)
print(f"#define {include_guard}", file=out_fd)
print(file=out_fd)
max_sec_val = max(self.sections.values()) + 1
max_opt_val_global = 0
for section, value in self.sections.items():
print(f"#define CFG_SECTION_{section} {value}", file=out_fd)
max_opt_val = max(self.options[section].values()) + 1
max_opt_val_global = max(max_opt_val, max_opt_val_global)
for option, value in self.options[section].items():
print(f"#define CFG_OPTION_{section}_{option} {value}", file=out_fd)
print(f"#define CFG_OPTION_{section}_MAX_OPTIONS {max_opt_val}\n", file=out_fd)
print(f"#define CFG_SECTION_MAX {max_sec_val}\n", file=out_fd)
print(f"#define CFG_SECTION_MAX_OPT_VAL {max_opt_val_global}\n", file=out_fd)
print("extern const char * section_names[CFG_SECTION_MAX];", file=out_fd)
print("extern const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL];", file=out_fd)
print(f"#endif // !defined {include_guard}", file=out_fd)
with open(output_source, 'w', encoding='utf-8') as out_fd:
print("// Autogenerated config string table - DO NOT EDIT\n", file=out_fd)
print(f'#include "{os.path.basename(output_header)}"', file=out_fd)
print("const char * section_names[CFG_SECTION_MAX] = {", file=out_fd)
for section, value in self.sections.items():
print(f' [{value}] = "{section.lower()}",', file=out_fd)
print("};\n", file=out_fd)
print("const char * options_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL] = {", file=out_fd)
for section, value in self.sections.items():
print(f' [{value}] =', '{', file=out_fd)
for option, value in self.options[section].items():
print(f' [{value}] = "{option.lower()}",', file=out_fd)
print(' },', file=out_fd)
print("};\n", file=out_fd)
def generate_py_definitions(self, output_file):
"""Generate the Python definitions"""
try:
out_fd = open(output_file, 'w', encoding='utf-8')
print("'''Autogenerated config identifiers from x52d.conf'''", file=out_fd)
print("# DO NOT EDIT\n", file=out_fd)
print("from enum import Enum", file=out_fd)
print("\nclass Section(Enum):", file=out_fd)
print(" '''Section identifiers'''", file=out_fd)
for section, value in self.sections.items():
print(f" {section} = {value}", file=out_fd)
for section in self.sections.keys():
print(f"\nclass {section}(Enum):", file=out_fd)
print(f" '''Section {section} identifiers'''", file=out_fd)
for option, value in self.options[section].items():
print(f" {option} = {value}", file=out_fd)
finally:
out_fd.close()
def main():
parser = argparse.ArgumentParser(description="Generate C enum and python enum for config")
parser.add_argument('input_file')
parser.add_argument('registry')
parser.add_argument('output_c_header')
parser.add_argument('output_c_strings')
parser.add_argument('output_py_defs')
args = parser.parse_args()
c2e = ConfigToEnum(args.input_file, args.registry)
c2e.parse()
c2e.save_registry()
c2e.generate_c_definitions(args.output_c_header, args.output_c_strings)
c2e.generate_py_definitions(args.output_py_defs)
if __name__ == '__main__':
main()

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: libx52 0.3.3\n"
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
"POT-Creation-Date: 2026-04-20 21:55-0700\n"
"POT-Creation-Date: 2026-04-22 09:02-0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -285,89 +285,89 @@ msgstr ""
msgid "Event @ %ld.%06ld: %s, value %d\n"
msgstr ""
#: joytest/x52_test.c:97
#: joytest/x52_test.c:99
msgid "Test brightness scale (~ 1m)"
msgstr ""
#: joytest/x52_test.c:98
#: joytest/x52_test.c:100
msgid "Test LED states (~ 45s)"
msgstr ""
#: joytest/x52_test.c:99
#: joytest/x52_test.c:101
msgid "Test MFD string display (~ 30s)"
msgstr ""
#: joytest/x52_test.c:100
#: joytest/x52_test.c:102
msgid "Test MFD displays all characters (~ 2m 15s)"
msgstr ""
#: joytest/x52_test.c:101
#: joytest/x52_test.c:103
msgid "Test the blink and shift commands (< 10s)"
msgstr ""
#: joytest/x52_test.c:102
#: joytest/x52_test.c:104
msgid "Test the clock commands (~1m)"
msgstr ""
#: joytest/x52_test.c:126
#: joytest/x52_test.c:128
msgid ""
"x52test is a suite of tests to write to the X52 Pro device\n"
"and test the extra functionality available in the LEDs and MFD\n"
msgstr ""
#: joytest/x52_test.c:130
#: joytest/x52_test.c:132
msgid "These tests take roughly 6 minutes to run"
msgstr ""
#: joytest/x52_test.c:132
#: joytest/x52_test.c:134
msgid "Press Enter to begin the tests, press Ctrl-C to abort anytime"
msgstr ""
#: joytest/x52_test.c:138
#: joytest/x52_test.c:140
#, c-format
msgid "Unable to initialize X52 library: %s\n"
msgstr ""
#: joytest/x52_test.c:153
msgid "All tests completed successfully"
msgstr ""
#: joytest/x52_test.c:155
#, c-format
msgid "Got error %s\n"
msgid "All tests completed successfully"
msgstr ""
#: joytest/x52_test.c:157
#, c-format
msgid "Got error %s\n"
msgstr ""
#: joytest/x52_test.c:159
#, c-format
msgid "Received %s signal, quitting...\n"
msgstr ""
#: joytest/x52_test.c:176
#: joytest/x52_test.c:178
msgid ""
"These are the available tests with a description and\n"
"approximate runtime. Not specifying any tests will run\n"
"all the tests\n"
msgstr ""
#: joytest/x52_test.c:180
#: joytest/x52_test.c:182
msgid "List of tests:"
msgstr ""
#: joytest/x52_test.c:230
#: joytest/x52_test.c:214
#, c-format
msgid ""
"Usage: %s [list of tests]\n"
"\n"
msgstr ""
#: joytest/x52_test.c:244
#: joytest/x52_test.c:220
#, c-format
msgid ""
"Unrecognized test identifier: %s\n"
"\n"
msgstr ""
#: joytest/x52_test.c:257
#: joytest/x52_test.c:231
msgid "Not running any tests"
msgstr ""
@ -599,41 +599,41 @@ msgstr ""
msgid "Shutting down X52 clock manager thread"
msgstr ""
#: daemon/command.c:380
#: daemon/command.c:310
#, c-format
msgid "Error reading from client %d: %s"
msgstr ""
#: daemon/command.c:391
#: daemon/command.c:321
#, c-format
msgid "Short write to client %d; expected %d bytes, wrote %d bytes"
msgstr ""
#: daemon/command.c:416
#: daemon/command.c:346
#, c-format
msgid "Error %d during command loop: %s"
msgstr ""
#: daemon/command.c:443
#: daemon/command.c:373
#, c-format
msgid "Error creating command socket: %s"
msgstr ""
#: daemon/command.c:451
#: daemon/command.c:381
#, c-format
msgid "Error marking command socket as nonblocking: %s"
msgstr ""
#: daemon/command.c:457
#: daemon/command.c:387
#, c-format
msgid "Error listening on command socket: %s"
msgstr ""
#: daemon/command.c:461
#: daemon/command.c:391
msgid "Starting command processing thread"
msgstr ""
#: daemon/command.c:479
#: daemon/command.c:409
msgid "Shutting down command processing thread"
msgstr ""

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: libx52 0.3.3\n"
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
"POT-Creation-Date: 2026-04-20 21:55-0700\n"
"POT-Creation-Date: 2026-04-22 09:02-0700\n"
"PO-Revision-Date: 2026-04-04 12:00-0700\n"
"Last-Translator: Nirenjan Krishnan <nirenjan@gmail.com>\n"
"Language-Team: Dummy Language for testing i18n\n"
@ -285,31 +285,31 @@ msgstr "Estingtay (interruptay otay exitay)\n"
msgid "Event @ %ld.%06ld: %s, value %d\n"
msgstr "Eventay @ %ld.%06ld: %s, aluevay %d\n"
#: joytest/x52_test.c:97
#: joytest/x52_test.c:99
msgid "Test brightness scale (~ 1m)"
msgstr "Esttay ightnessbray alescay (~ 1m)"
#: joytest/x52_test.c:98
#: joytest/x52_test.c:100
msgid "Test LED states (~ 45s)"
msgstr "Esstay EDLay atesstay (~ 45s)"
#: joytest/x52_test.c:99
#: joytest/x52_test.c:101
msgid "Test MFD string display (~ 30s)"
msgstr "Esttay MFDay ingstray isplayday (~ 30s)"
#: joytest/x52_test.c:100
#: joytest/x52_test.c:102
msgid "Test MFD displays all characters (~ 2m 15s)"
msgstr "Esttay MFDay isplaysday allay aracterschay (~ 2m 15s)"
#: joytest/x52_test.c:101
#: joytest/x52_test.c:103
msgid "Test the blink and shift commands (< 10s)"
msgstr "Esttay ethay inkblay anday iftshay ommandscay (< 10s)"
#: joytest/x52_test.c:102
#: joytest/x52_test.c:104
msgid "Test the clock commands (~1m)"
msgstr "Esttay ethay ockclay ommandscay (~1m)"
#: joytest/x52_test.c:126
#: joytest/x52_test.c:128
msgid ""
"x52test is a suite of tests to write to the X52 Pro device\n"
"and test the extra functionality available in the LEDs and MFD\n"
@ -318,36 +318,36 @@ msgstr ""
"X52 Pro eviceday anday esttay ethay extray unctionalityfay\n"
"availableay inay ethay EDsLay anday FDMay\n"
#: joytest/x52_test.c:130
#: joytest/x52_test.c:132
msgid "These tests take roughly 6 minutes to run"
msgstr "Esethay eststay aketay oughlyray 6 inutesmay otay unray"
#: joytest/x52_test.c:132
#: joytest/x52_test.c:134
msgid "Press Enter to begin the tests, press Ctrl-C to abort anytime"
msgstr ""
"Esspray Enteray otay eginbay ethay eststay, esspray Ctrl-C otay abortay "
"anytimeay"
#: joytest/x52_test.c:138
#: joytest/x52_test.c:140
#, c-format
msgid "Unable to initialize X52 library: %s\n"
msgstr "Unableay otay initializeay X52 ibrarylay: %s\n"
#: joytest/x52_test.c:153
#: joytest/x52_test.c:155
msgid "All tests completed successfully"
msgstr "Allay eststay ompletedcay uccessfullysay"
#: joytest/x52_test.c:155
#: joytest/x52_test.c:157
#, c-format
msgid "Got error %s\n"
msgstr "Otgay erroray %s\n"
#: joytest/x52_test.c:157
#: joytest/x52_test.c:159
#, c-format
msgid "Received %s signal, quitting...\n"
msgstr "Eceivedray %s ignalsay, uittingqay...\n"
#: joytest/x52_test.c:176
#: joytest/x52_test.c:178
msgid ""
"These are the available tests with a description and\n"
"approximate runtime. Not specifying any tests will run\n"
@ -358,18 +358,18 @@ msgstr ""
"ecifyingspay anyay eststay illway unray allay ethay\n"
"eststay\n"
#: joytest/x52_test.c:180
#: joytest/x52_test.c:182
msgid "List of tests:"
msgstr "Istlay ofay eststay:"
#: joytest/x52_test.c:230
#: joytest/x52_test.c:214
#, c-format
msgid ""
"Usage: %s [list of tests]\n"
"\n"
msgstr "Usageay: %s [istlay ofay eststay]\n"
#: joytest/x52_test.c:244
#: joytest/x52_test.c:220
#, c-format
msgid ""
"Unrecognized test identifier: %s\n"
@ -378,7 +378,7 @@ msgstr ""
"Unrecognizeday esttay identifieray: %s\n"
"\n"
#: joytest/x52_test.c:257
#: joytest/x52_test.c:231
msgid "Not running any tests"
msgstr "Otnay unningray anyay eststay"
@ -645,42 +645,42 @@ msgstr "Erroray %d initializingay ockclay eadthray: %s"
msgid "Shutting down X52 clock manager thread"
msgstr "Uttingshay ownday X52 ockclay anagermay eadthray"
#: daemon/command.c:380
#: daemon/command.c:310
#, c-format
msgid "Error reading from client %d: %s"
msgstr "Erroray eadingray omfray ientclay %d: %s"
#: daemon/command.c:391
#: daemon/command.c:321
#, c-format
msgid "Short write to client %d; expected %d bytes, wrote %d bytes"
msgstr ""
"Ortshay itewray otay ientclay %d; expecteday %d ytesbay, otewray %d ytesbay"
#: daemon/command.c:416
#: daemon/command.c:346
#, c-format
msgid "Error %d during command loop: %s"
msgstr "Erroray %d uringday ommandcay ooplay: %s"
#: daemon/command.c:443
#: daemon/command.c:373
#, c-format
msgid "Error creating command socket: %s"
msgstr "Erroray eatingcray ommandcay ocketsay: %s"
#: daemon/command.c:451
#: daemon/command.c:381
#, c-format
msgid "Error marking command socket as nonblocking: %s"
msgstr "Erroray arkingmay ommandcay ocketsay asay onblockingnay: %s"
#: daemon/command.c:457
#: daemon/command.c:387
#, c-format
msgid "Error listening on command socket: %s"
msgstr "Erroray isteninglay onay ommandcay ocketsay: %s"
#: daemon/command.c:461
#: daemon/command.c:391
msgid "Starting command processing thread"
msgstr "Artingstay ommandcay ocessingpray eadthray"
#: daemon/command.c:479
#: daemon/command.c:409
msgid "Shutting down command processing thread"
msgstr "Uttingshay ownday ommandcay ocessingpray eadthray"