Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ target_link_libraries(

install(TARGETS controlMgr RUNTIME DESTINATION bin)

install(FILES ${CMAKE_BINARY_DIR}/ctrlm_config.json.template DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} COMPONENT config )
install(FILES ${CMAKE_BINARY_DIR}/ctrlm_config.json DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} COMPONENT config )
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctrlm_load_config() now loads OEM config from /etc/vendor/input/ctrlm_config.json, but the build installs ctrlm_config.json to ${CMAKE_INSTALL_SYSCONFDIR} (typically /etc). This mismatch means the installed config may never be read at runtime. Align the install destination with the runtime search path (or update the runtime path to match the install).

Copilot uses AI. Check for mistakes.

# DEFINES FROM OPTIONS
if(ANSI_CODES_DISABLED)
Expand Down Expand Up @@ -260,7 +260,7 @@ endif()

install(TARGETS controlMgr RUNTIME DESTINATION bin)

install(FILES ${CMAKE_BINARY_DIR}/ctrlm_config.json.template DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} COMPONENT config )
install(FILES ${CMAKE_BINARY_DIR}/ctrlm_config.json DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} COMPONENT config )

# GENERATED FILES
add_custom_command( OUTPUT ctrlm_version_build.h
Expand All @@ -276,16 +276,16 @@ add_custom_command( OUTPUT ctrlm_version_build.h
)

add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/ctrlm_config.json.template
COMMAND python3 ${CTRLM_UTILS_JSON_COMBINE} -i ${CMAKE_CURRENT_SOURCE_DIR}/src/ctrlm_config_default.json -a ${CTRLM_CONFIG_JSON_VSDK}:vsdk -a ${CTRLM_CONFIG_JSON_CPC} -s ${CTRLM_CONFIG_JSON_CPC_SUB} -a ${CTRLM_CONFIG_JSON_CPC_ADD} -s ${CTRLM_CONFIG_JSON_OEM_SUB} -a ${CTRLM_CONFIG_JSON_OEM_ADD} -s ${CTRLM_CONFIG_JSON_MAIN_SUB} -a ${CTRLM_CONFIG_JSON_MAIN_ADD} -o ${CMAKE_BINARY_DIR}/ctrlm_config.json.template
OUTPUT ${CMAKE_BINARY_DIR}/ctrlm_config.json
COMMAND python3 ${CTRLM_UTILS_JSON_COMBINE} -i ${CMAKE_CURRENT_SOURCE_DIR}/src/ctrlm_config_default.json -a ${CTRLM_CONFIG_JSON_CPC} -s ${CTRLM_CONFIG_JSON_CPC_SUB} -a ${CTRLM_CONFIG_JSON_CPC_ADD} -s ${CTRLM_CONFIG_JSON_MAIN_SUB} -a ${CTRLM_CONFIG_JSON_MAIN_ADD} -o ${CMAKE_BINARY_DIR}/ctrlm_config.json
DEPENDS src/ctrlm_config_default.json
VERBATIM
)

add_custom_command(
OUTPUT ctrlm_config_default.h ${CMAKE_CURRENT_SOURCE_DIR}/src/ctrlm_config_default.c
COMMAND python3 ${CTRLM_UTILS_JSON_TO_HEADER} -i ${CMAKE_BINARY_DIR}/ctrlm_config.json.template -o ctrlm_config_default.h -c ${CMAKE_CURRENT_SOURCE_DIR}/src/ctrlm_config_default.c -v "ctrlm_global,network_rf4ce,network_ip,network_ble,ir,voice,device_update" -d "network_ble"
DEPENDS ${CMAKE_BINARY_DIR}/ctrlm_config.json.template
COMMAND python3 ${CTRLM_UTILS_JSON_TO_HEADER} -i ${CMAKE_BINARY_DIR}/ctrlm_config.json -o ctrlm_config_default.h -c ${CMAKE_CURRENT_SOURCE_DIR}/src/ctrlm_config_default.c -v "ctrlm_global,network_rf4ce,network_ip,network_ble,ir,voice,device_update" -d "network_ble"
DEPENDS ${CMAKE_BINARY_DIR}/ctrlm_config.json
VERBATIM
)

Expand All @@ -296,5 +296,5 @@ add_custom_command(
)

add_custom_target( ctrlm_config
DEPENDS ${CMAKE_BINARY_DIR}/ctrlm_config.json.template
DEPENDS ${CMAKE_BINARY_DIR}/ctrlm_config.json
)
48 changes: 44 additions & 4 deletions src/config/ctrlm_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ ctrlm_config_t::~ctrlm_config_t() {
}
}

bool ctrlm_config_t::load_config(const std::string &file_path) {
bool ctrlm_config_t::load_config(const std::string &file_path, bool verbose) {
bool ret = false;
std::string contents = file_to_string(file_path);
if(this->root) {
Expand All @@ -63,20 +63,60 @@ bool ctrlm_config_t::load_config(const std::string &file_path) {
}
if(!contents.empty()) {
json_error_t json_error;
XLOGD_INFO_OPTS(XLOG_OPTS_DEFAULT, 20 * 1024, "Loading Configuration for <%s> <%s>", file_path.c_str(), contents.c_str());
if(verbose) {
XLOGD_INFO_OPTS(XLOG_OPTS_DEFAULT, 20 * 1024, "Loading Configuration for <%s> <%s>", file_path.c_str(), contents.c_str());
} else {
XLOGD_INFO("Loading Configuration for <%s>", file_path.c_str());
}
this->root = json_loads(contents.c_str(), JSON_REJECT_DUPLICATES, &json_error);
if(this->root != NULL) {
XLOGD_INFO("config loaded successfully as JSON");
if(verbose) {
XLOGD_INFO("config loaded successfully as JSON for <%s>", file_path.c_str());
}
ret = true;
} else {
XLOGD_ERROR("JSON ERROR: Line <%u> Column <%u> Text <%s>", json_error.line, json_error.column, json_error.text);
XLOGD_ERROR("JSON ERROR: Line <%u> Column <%u> Text <%s> Contents <%s>", json_error.line, json_error.column, json_error.text, contents.c_str());
}
Comment on lines +78 to 79
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON parse error log now includes the full config file contents. This can be very large and may leak sensitive data into logs. Consider truncating the logged contents and/or only including it when verbose is enabled.

Copilot uses AI. Check for mistakes.
} else {
XLOGD_ERROR("no config file contents");
}
return(ret);
}

bool ctrlm_config_t::append_config(const std::string &file_path, bool verbose) {
if(this->root == NULL) {
XLOGD_ERROR("no config file loaded");
return(false);
}
bool ret = false;
std::string contents = file_to_string(file_path);
if(contents.empty()) {
XLOGD_ERROR("no config file contents");
} else {
json_error_t json_error;
if(verbose) {
XLOGD_INFO_OPTS(XLOG_OPTS_DEFAULT, 20 * 1024, "Appending Configuration for <%s> <%s>", file_path.c_str(), contents.c_str());
} else {
XLOGD_INFO("Appending Configuration for <%s>", file_path.c_str());
}
json_t *append_root = json_loads(contents.c_str(), JSON_REJECT_DUPLICATES, &json_error);
if(append_root == NULL) {
XLOGD_ERROR("JSON ERROR: Line <%u> Column <%u> Text <%s> Contents <%s>", json_error.line, json_error.column, json_error.text, contents.c_str());
} else {
if(json_object_update(this->root, append_root) != 0) {
XLOGD_ERROR("Failed to merge JSON objects");
} else {
if(verbose) {
XLOGD_INFO("config appended successfully as JSON for <%s>", file_path.c_str());
}
ret = true;
}
json_decref(append_root);
Comment on lines +102 to +114
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append_config() uses json_object_update(), which overwrites top-level keys and replaces nested objects wholesale. If the OPT file is intended to override only a subset of an OEM section (common for config overrides), this will drop the rest of the OEM subtree. Use a recursive merge (e.g., json_object_update_recursive() or equivalent custom deep-merge) to preserve existing nested keys while overriding only those provided.

Copilot uses AI. Check for mistakes.
}
}
return(ret);
}

bool ctrlm_config_t::path_exists(const std::string &path) {
return(ctrlm_utils_json_from_path(this->root, path, false) != NULL ? true : false);
}
Expand Down
8 changes: 7 additions & 1 deletion src/config/ctrlm_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ class ctrlm_config_t {
* @param file_path The path to the configuration file
* @return True on success, otherwise False
*/
bool load_config(const std::string &file_path);
bool load_config(const std::string &file_path, bool verbose = false);
/**
* Function which appends a configuration file into the configuration object.
* @param file_path The path to the configuration file
* @return True on success, otherwise False
*/
bool append_config(const std::string &file_path, bool verbose = false);
/**
* Function to check if object exists from a path
* @param path A period seperated string used to navigate a JSON object i.e. "network_rf4ce.polling.enabled"
Expand Down
4 changes: 0 additions & 4 deletions src/ctrlm_config_default.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@
"authservice_poll_period" : 5,
"authservice_fast_poll_period" : 1000,
"authservice_fast_max_retries" : 25,
"keycode_logging_poll_period" : 30,
"keycode_logging_max_retries" : 5,
"url_auth_service" : "",
"timeout_recently_booted" : 1800000,
"timeout_line_of_sight" : 10000,
"timeout_autobind" : 1200,
"timeout_button_binding" : 600000,
"timeout_screen_bind" : 600000,
"timeout_one_touch_autobind" : 600000,
"mask_key_codes" : false,
"mask_pii" : [true, false],
"crash_recovery_threshold" : 2,
"device_id" : "",
Expand Down Expand Up @@ -263,7 +260,6 @@
"utterance_path" : "/opt/logs/rf4ce_adpcm_header_vrex.raw",
"force_voice_settings" : false,
"vrex_response_timeout" : 10000,
"keyword_detect_sensitivity" : 0.3,
"opus_encoder_params" : "4080084410",
"packet_loss_threshold" : 5,
"vrex_test_flag" : false,
Expand Down
69 changes: 53 additions & 16 deletions src/ctrlm_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1601,8 +1601,7 @@ gboolean ctrlm_load_authservice_data(void) {

gboolean ctrlm_load_config(json_t **json_obj_root, json_t **json_obj_net_rf4ce, json_t **json_obj_voice, json_t **json_obj_device_update, json_t **json_obj_validation, json_t **json_obj_vsdk) {
std::string config_fn_opt = "/opt/ctrlm_config.json";
std::string config_fn_etc = "/etc/ctrlm_config.json";
std::string config_fn_etc_override = "/etc/ctrlm_config.json.OVERRIDE";
std::string config_fn_oem = "/etc/vendor/input/ctrlm_config.json";
json_t *json_obj_ctrlm;
ctrlm_config_t *ctrlm_config = ctrlm_config_t::get_instance();
gboolean local_conf = false;
Expand All @@ -1612,18 +1611,44 @@ gboolean ctrlm_load_config(json_t **json_obj_root, json_t **json_obj_net_rf4ce,
if(ctrlm_config == NULL) {
XLOGD_ERROR("Failed to get config manager instance");
return(false);
} else if(!ctrlm_is_production_build() && g_file_test(config_fn_opt.c_str(), G_FILE_TEST_EXISTS) && ctrlm_config->load_config(config_fn_opt)) {
XLOGD_INFO("Read configuration from <%s>", config_fn_opt.c_str());
local_conf = true;
} else if(g_file_test(config_fn_etc.c_str(), G_FILE_TEST_EXISTS) && ctrlm_config->load_config(config_fn_etc)) {
XLOGD_INFO("Read configuration from <%s>", config_fn_etc.c_str());
} else if(g_file_test(config_fn_etc_override.c_str(), G_FILE_TEST_EXISTS) && ctrlm_config->load_config(config_fn_etc_override)) {
XLOGD_INFO("Read configuration from <%s>", config_fn_etc_override.c_str());
} else {
XLOGD_WARN("Configuration error. Configuration file(s) missing, using defaults");
}

bool oem_append = g_file_test(config_fn_oem.c_str(), G_FILE_TEST_EXISTS);
bool opt_append = ctrlm_is_production_build() ? false : g_file_test(config_fn_opt.c_str(), G_FILE_TEST_EXISTS);

if(!oem_append && !opt_append) {
XLOGD_INFO("Using default configuration");
return(false);
}

// Check for OEM config file override
if(oem_append) {
XLOGD_INFO("Loading OEM configuration from <%s>", config_fn_oem.c_str());
if(!ctrlm_config->load_config(config_fn_oem)) {
XLOGD_ERROR("Failed to load OEM configuration from <%s>", config_fn_oem.c_str());
return(false);
}
}

// Check for OPT config file override
if(opt_append) {
if(!oem_append) {
XLOGD_INFO("Loading OPT configuration from <%s>", config_fn_opt.c_str());
if(!ctrlm_config->load_config(config_fn_opt)) {
XLOGD_ERROR("Failed to load OPT configuration from <%s>", config_fn_opt.c_str());
return(false);
}
} else {
XLOGD_INFO("Appending OPT configuration from <%s>", config_fn_opt.c_str());

if(!ctrlm_config->append_config(config_fn_opt)) {
XLOGD_ERROR("Failed to append OPT configuration from <%s>", config_fn_opt.c_str());
return(false);
}
}
local_conf = true;
}

// Parse the JSON data
*json_obj_root = ctrlm_config->json_from_path("", true); // Get root AND add ref to it, since this code derefs it
if(*json_obj_root == NULL) {
Expand All @@ -1637,24 +1662,36 @@ gboolean ctrlm_load_config(json_t **json_obj_root, json_t **json_obj_net_rf4ce,
return(false);
}

// Print the configuration since it was loaded from files
char *json_dump = json_dumps(*json_obj_root, JSON_INDENT(3) | JSON_SORT_KEYS);
if(json_dump == NULL) {
XLOGD_ERROR("unable to dump JSON object");
json_decref(*json_obj_root);
*json_obj_root = NULL;
return(false);
} else {
XLOGD_INFO_OPTS(XLOG_OPTS_DEFAULT, 20 * 1024, "Final configuration:\n%s", json_dump);
free(json_dump);
}
Comment on lines +1665 to +1675
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dumping the entire merged configuration to logs can expose secrets/PII (e.g., device_id, endpoints, auth URLs) in production. Consider gating this behind a debug/verbose flag, masking sensitive fields before logging, or removing the full dump entirely.

Suggested change
// Print the configuration since it was loaded from files
char *json_dump = json_dumps(*json_obj_root, JSON_INDENT(3) | JSON_SORT_KEYS);
if(json_dump == NULL) {
XLOGD_ERROR("unable to dump JSON object");
json_decref(*json_obj_root);
*json_obj_root = NULL;
return(false);
} else {
XLOGD_INFO_OPTS(XLOG_OPTS_DEFAULT, 20 * 1024, "Final configuration:\n%s", json_dump);
free(json_dump);
}
// Configuration successfully loaded and validated at this point

Copilot uses AI. Check for mistakes.

// Extract the RF4CE network configuration object
if(g_ctrlm.rf4ce_enabled) {
*json_obj_net_rf4ce = json_object_get(*json_obj_root, JSON_OBJ_NAME_NETWORK_RF4CE);
if(*json_obj_net_rf4ce == NULL || !json_is_object(*json_obj_net_rf4ce)) {
XLOGD_WARN("RF4CE network object not found");
XLOGD_INFO("RF4CE network object not found");
}
}

// Extract the voice configuration object
*json_obj_voice = json_object_get(*json_obj_root, JSON_OBJ_NAME_VOICE);
if(*json_obj_voice == NULL || !json_is_object(*json_obj_voice)) {
XLOGD_WARN("voice object not found");
XLOGD_INFO("voice object not found");
}

// Extract the device update configuration object
*json_obj_device_update = json_object_get(*json_obj_root, JSON_OBJ_NAME_DEVICE_UPDATE);
if(*json_obj_device_update == NULL || !json_is_object(*json_obj_device_update)) {
XLOGD_WARN("device update object not found");
XLOGD_INFO("device update object not found");
}

//Extract the vsdk configuration object
Comment on lines 1695 to 1697
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the vsdk extraction block immediately below this section, the code sets json_obj_vsdk = NULL when the JSON value is missing or not an object. That only clears the local pointer variable, not the out-param; if the value exists but is the wrong type, callers can still receive a non-object *json_obj_vsdk. Update that branch to clear *json_obj_vsdk instead.

Copilot uses AI. Check for mistakes.
Expand All @@ -1667,7 +1704,7 @@ gboolean ctrlm_load_config(json_t **json_obj_root, json_t **json_obj_net_rf4ce,
// Extract the ctrlm global configuration object
json_obj_ctrlm = json_object_get(*json_obj_root, JSON_OBJ_NAME_CTRLM_GLOBAL);
if(json_obj_ctrlm == NULL || !json_is_object(json_obj_ctrlm)) {
XLOGD_WARN("control manger object not found");
XLOGD_INFO("control manger object not found");
} else {
json_config conf_global;
if(!conf_global.config_object_set(json_obj_ctrlm)) {
Expand All @@ -1677,7 +1714,7 @@ gboolean ctrlm_load_config(json_t **json_obj_root, json_t **json_obj_net_rf4ce,
// Extract the validation configuration object
*json_obj_validation = json_object_get(json_obj_ctrlm, JSON_OBJ_NAME_CTRLM_GLOBAL_VALIDATION_CONFIG);
if(*json_obj_validation == NULL || !json_is_object(*json_obj_validation)) {
XLOGD_WARN("validation object not found");
XLOGD_INFO("validation object not found");
}

// Now parse the control manager object
Expand Down
Loading