From e3fe9fac601c3ddc9f626b79ba624a7a0017f60d Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 22 Jan 2025 00:50:55 +1100 Subject: [PATCH 01/25] First stab at codegen. --- builddefs/build_keyboard.mk | 14 ++++ data/schemas/keymap.jsonschema | 6 ++ lib/python/qmk/cli/__init__.py | 1 + .../qmk/cli/generate/community_modules.py | 75 +++++++++++++++++++ lib/python/qmk/cli/generate/rules_mk.py | 9 +++ lib/python/qmk/keymap.py | 27 ------- modules/hello_world/hello_world.c | 17 +++++ modules/hello_world/rules.mk | 1 + 8 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 lib/python/qmk/cli/generate/community_modules.py create mode 100644 modules/hello_world/hello_world.c create mode 100644 modules/hello_world/rules.mk diff --git a/builddefs/build_keyboard.mk b/builddefs/build_keyboard.mk index 7d58c29462..e4661a7f3f 100644 --- a/builddefs/build_keyboard.mk +++ b/builddefs/build_keyboard.mk @@ -224,6 +224,20 @@ $(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) generated-files: $(INTERMEDIATE_OUTPUT)/src/config.h $(INTERMEDIATE_OUTPUT)/src/keymap.c $(INTERMEDIATE_OUTPUT)/src/keymap.h +$(INTERMEDIATE_OUTPUT)/src/community_modules.h: $(KEYMAP_JSON) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) generate-community-modules-h --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(KEYMAP_JSON)) + @$(BUILD_CMD) + +$(INTERMEDIATE_OUTPUT)/src/community_modules.c: $(KEYMAP_JSON) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) generate-community-modules-c --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(KEYMAP_JSON)) + @$(BUILD_CMD) + +SRC += $(INTERMEDIATE_OUTPUT)/src/community_modules.c + +generated-files: $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c + endif include $(BUILDDEFS_PATH)/converters.mk diff --git a/data/schemas/keymap.jsonschema b/data/schemas/keymap.jsonschema index e967e45c53..b92a536c2c 100644 --- a/data/schemas/keymap.jsonschema +++ b/data/schemas/keymap.jsonschema @@ -71,6 +71,12 @@ "config": {"$ref": "qmk.keyboard.v1"}, "notes": { "type": "string" + }, + "modules": { + "type": "array", + "items": { + "type": "string" + } } } } diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 2d63dfb447..bfe15f8b8c 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -49,6 +49,7 @@ subcommands = [ 'qmk.cli.generate.api', 'qmk.cli.generate.autocorrect_data', 'qmk.cli.generate.compilation_database', + 'qmk.cli.generate.community_modules', 'qmk.cli.generate.config_h', 'qmk.cli.generate.develop_pr_list', 'qmk.cli.generate.dfu_header', diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py new file mode 100644 index 0000000000..b9a75c50d4 --- /dev/null +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -0,0 +1,75 @@ +from argcomplete.completers import FilesCompleter + +from milc import cli +from milc.attrdict import AttrDict + +import qmk.path +from qmk.commands import dump_lines, parse_configurator_json +from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE + +class ModuleAPI(AttrDict): + def __init__(self, **kwargs): + super().__init__() + for key, value in kwargs.items(): + self[key] = value + +MODULE_API_LIST = [ + ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', ret_val=None), + ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', ret_val=None), + ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', ret_val=None), + ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true'), +] + +@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.subcommand('Creates a community_modules.h from a keymap.json file.') +def generate_community_modules_h(cli): + """Creates a community_modules.h from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', ''] + keymap_json = parse_configurator_json(cli.args.filename) + if keymap_json and 'modules' in keymap_json: + for module in keymap_json['modules']: + for api in MODULE_API_LIST: + lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') + dump_lines(cli.args.output, lines, cli.args.quiet) + +@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.subcommand('Creates a community_modules.c from a keymap.json file.') +def generate_community_modules_c(cli): + """Creates a community_modules.c from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', '#include "community_modules.h"'] + keymap_json = parse_configurator_json(cli.args.filename) + if keymap_json and 'modules' in keymap_json: + for module in keymap_json['modules']: + for api in MODULE_API_LIST: + lines.append('') + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}({api.args}) {{ ') + if api.ret_val: + lines.append(f' return {api.ret_val};') + lines.append('}') + + for api in MODULE_API_LIST: + lines.append('') + lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') + if api.ret_type == 'bool': + lines.append(' return true') + for module in keymap_json['modules']: + if api.ret_type == 'bool': + lines.append(f' && {api.name}_{module}({api.call_params})') + else: + lines.append(f' {api.name}_{module}({api.call_params});') + if api.ret_type == 'bool': + lines.append(' ;') + lines.append('}') + dump_lines(cli.args.output, lines, cli.args.quiet) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 5291556109..5b06c283d9 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -55,6 +55,7 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict): def generate_rules_mk(cli): """Generates a rules.mk file from info.json. """ + user_keymap = None converter = None # Determine our keyboard/keymap if cli.args.filename: @@ -99,6 +100,14 @@ def generate_rules_mk(cli): if converter: rules_mk_lines.append(generate_rule('CONVERT_TO', converter)) + if user_keymap and 'modules' in user_keymap: + for module in user_keymap['modules']: + rules_mk_lines.append('') + rules_mk_lines.append(f'COMMUNITY_MODULES += {module}') + rules_mk_lines.append(f'OPT_DEFS += -DMODULE_{module.upper()}=TRUE') + rules_mk_lines.append(f'VPATH += modules/{module}') + rules_mk_lines.append(f'include modules/{module}/rules.mk') + # Show the results dump_lines(cli.args.output, rules_mk_lines) diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index 9dd043c4a8..8e36461722 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py @@ -334,33 +334,6 @@ def write_json(keyboard, keymap, layout, layers, macros=None): return write_file(keymap_file, keymap_content) -def write(keymap_json): - """Generate the `keymap.c` and write it to disk. - - Returns the filename written to. - - `keymap_json` should be a dict with the following keys: - keyboard - The name of the keyboard - - keymap - The name of the keymap - - layout - The LAYOUT macro this keymap uses. - - layers - An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. - - macros - A list of macros for this keymap. - """ - keymap_content = generate_c(keymap_json) - keymap_file = qmk.path.keymaps(keymap_json['keyboard'])[0] / keymap_json['keymap'] / 'keymap.c' - - return write_file(keymap_file, keymap_content) - - def locate_keymap(keyboard, keymap, force_layout=None): """Returns the path to a keymap for a specific keyboard. """ diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c new file mode 100644 index 0000000000..078d641ba5 --- /dev/null +++ b/modules/hello_world/hello_world.c @@ -0,0 +1,17 @@ +// Copyright 2025 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#include QMK_KEYBOARD_H + +static uint32_t init_timestamp = 0; + +void keyboard_post_init_hello_world(void) { + init_timestamp = timer_read32(); +} + +void housekeeping_task_hello_world(void) { + static bool welcomed = false; + if(!welcomed && timer_elapsed32(init_timestamp) > 1000) { + welcomed = true; + print("Hello, world! I'm a QMK based keyboard!\n"); + } +} diff --git a/modules/hello_world/rules.mk b/modules/hello_world/rules.mk new file mode 100644 index 0000000000..0326df8935 --- /dev/null +++ b/modules/hello_world/rules.mk @@ -0,0 +1 @@ +SRC += hello_world.c From 11b9ff0c4f462fe08cd0ff4126064bb8b87204b7 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 22 Jan 2025 00:59:50 +1100 Subject: [PATCH 02/25] Appease the gods of formatting. --- lib/python/qmk/cli/generate/community_modules.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index b9a75c50d4..28b3a75c9f 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -7,12 +7,14 @@ import qmk.path from qmk.commands import dump_lines, parse_configurator_json from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE + class ModuleAPI(AttrDict): def __init__(self, **kwargs): super().__init__() for key, value in kwargs.items(): self[key] = value + MODULE_API_LIST = [ ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', ret_val=None), ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', ret_val=None), @@ -20,6 +22,7 @@ MODULE_API_LIST = [ ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true'), ] + @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') @@ -38,6 +41,7 @@ def generate_community_modules_h(cli): lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') dump_lines(cli.args.output, lines, cli.args.quiet) + @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') From 8f79ed1f23eb936e32eabd7aa839e547e03ae8da Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 22 Jan 2025 14:10:38 +1100 Subject: [PATCH 03/25] More APIs added, versioning. --- .../qmk/cli/generate/community_modules.py | 36 ++++++++++++--- lib/python/qmk/cli/generate/rules_mk.py | 4 +- lib/python/qmk/commands.py | 5 ++- modules/hello_world/hello_world.c | 4 +- quantum/action.h | 2 +- quantum/keyboard.c | 44 ++++++++++++++++++- quantum/quantum.c | 21 ++++++++- quantum/quantum.h | 4 ++ 8 files changed, 106 insertions(+), 14 deletions(-) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index 28b3a75c9f..88d63396e7 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -18,10 +18,15 @@ class ModuleAPI(AttrDict): MODULE_API_LIST = [ ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', ret_val=None), ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', ret_val=None), - ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', ret_val=None), + ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true'), ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true'), + ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val=None), + ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', ret_val=None), + ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', ret_val='true'), ] +MODULE_API_VERSION = '20250122' + @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @@ -33,13 +38,31 @@ def generate_community_modules_h(cli): if cli.args.output and cli.args.output.name == '-': cli.args.output = None - lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', ''] + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '#pragma once', + '#include ', + '#include ', + '', + f'#define COMMUNITY_MODULES_API_VERSION {MODULE_API_VERSION}', + f'#define ASSERT_COMMUNITY_MODULES_API_VERSION(x) _Static_assert((x) <= COMMUNITY_MODULES_API_VERSION, "Modules API mismatch, module requires higher version of QMK with API >= {MODULE_API_VERSION}.")', + '', + 'typedef struct keyrecord_t keyrecord_t;', + ] keymap_json = parse_configurator_json(cli.args.filename) if keymap_json and 'modules' in keymap_json: for module in keymap_json['modules']: + lines.append('') + lines.append(f'// From module: {module}') for api in MODULE_API_LIST: lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') - dump_lines(cli.args.output, lines, cli.args.quiet) + lines.append('') + + lines.append(f'// Parent implementation') + for api in MODULE_API_LIST: + lines.append(f'{api.ret_type} {api.name}_modules({api.args});') + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @@ -52,13 +75,14 @@ def generate_community_modules_c(cli): if cli.args.output and cli.args.output.name == '-': cli.args.output = None - lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', '#include "community_modules.h"'] + lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '', '#include "community_modules.h"'] keymap_json = parse_configurator_json(cli.args.filename) if keymap_json and 'modules' in keymap_json: for module in keymap_json['modules']: + lines.append('') for api in MODULE_API_LIST: lines.append('') - lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}({api.args}) {{ ') + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}({api.args}) {{') if api.ret_val: lines.append(f' return {api.ret_val};') lines.append('}') @@ -76,4 +100,4 @@ def generate_community_modules_c(cli): if api.ret_type == 'bool': lines.append(' ;') lines.append('}') - dump_lines(cli.args.output, lines, cli.args.quiet) + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 5b06c283d9..ea44f1e58a 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -101,10 +101,12 @@ def generate_rules_mk(cli): rules_mk_lines.append(generate_rule('CONVERT_TO', converter)) if user_keymap and 'modules' in user_keymap: + rules_mk_lines.append('') + rules_mk_lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLED=TRUE') for module in user_keymap['modules']: rules_mk_lines.append('') rules_mk_lines.append(f'COMMUNITY_MODULES += {module}') - rules_mk_lines.append(f'OPT_DEFS += -DMODULE_{module.upper()}=TRUE') + rules_mk_lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module.upper()}=TRUE') rules_mk_lines.append(f'VPATH += modules/{module}') rules_mk_lines.append(f'include modules/{module}/rules.mk') diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index a05b3641b5..38e032765f 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -98,11 +98,14 @@ def in_virtualenv(): return active_prefix != sys.prefix -def dump_lines(output_file, lines, quiet=True): +def dump_lines(output_file, lines, quiet=True, remove_repeated_newlines=False): """Handle dumping to stdout or file Creates parent folders if required """ generated = '\n'.join(lines) + '\n' + if remove_repeated_newlines: + if '\n\n\n' in generated: + generated = generated.replace('\n\n\n', '\n\n') if output_file and output_file.name != '-': output_file.parent.mkdir(parents=True, exist_ok=True) if output_file.exists(): diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index 078d641ba5..4fe9bd052e 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -2,6 +2,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include QMK_KEYBOARD_H +ASSERT_COMMUNITY_MODULES_API_VERSION(20250122); + static uint32_t init_timestamp = 0; void keyboard_post_init_hello_world(void) { @@ -10,7 +12,7 @@ void keyboard_post_init_hello_world(void) { void housekeeping_task_hello_world(void) { static bool welcomed = false; - if(!welcomed && timer_elapsed32(init_timestamp) > 1000) { + if(!welcomed && timer_elapsed32(init_timestamp) > 10000) { welcomed = true; print("Hello, world! I'm a QMK based keyboard!\n"); } diff --git a/quantum/action.h b/quantum/action.h index d5b15c6f17..7596688f31 100644 --- a/quantum/action.h +++ b/quantum/action.h @@ -45,7 +45,7 @@ typedef struct { } tap_t; /* Key event container for recording */ -typedef struct { +typedef struct keyrecord_t { keyevent_t event; #ifndef NO_ACTION_TAPPING tap_t tap; diff --git a/quantum/keyboard.c b/quantum/keyboard.c index d7836cf36e..ad740de4b3 100644 --- a/quantum/keyboard.c +++ b/quantum/keyboard.c @@ -289,6 +289,21 @@ __attribute__((weak)) void keyboard_pre_init_kb(void) { keyboard_pre_init_user(); } +/** \brief keyboard_pre_init_modules + * + * FIXME: needs doc + */ +__attribute__((weak)) void keyboard_pre_init_modules(void) {} + +/** \brief keyboard_pre_init_quantum + * + * FIXME: needs doc + */ +void keyboard_pre_init_quantum(void) { + keyboard_pre_init_modules(); + keyboard_pre_init_kb(); +} + /** \brief keyboard_post_init_user * * FIXME: needs doc @@ -305,6 +320,23 @@ __attribute__((weak)) void keyboard_post_init_kb(void) { keyboard_post_init_user(); } +/** \brief keyboard_post_init_modules + * + * FIXME: needs doc + */ + +__attribute__((weak)) void keyboard_post_init_modules(void) {} + +/** \brief keyboard_post_init_quantum + * + * FIXME: needs doc + */ + +void keyboard_post_init_quantum(void) { + keyboard_post_init_modules(); + keyboard_post_init_kb(); +} + /** \brief matrix_can_read * * Allows overriding when matrix scanning operations should be executed. @@ -323,7 +355,7 @@ void keyboard_setup(void) { eeprom_driver_init(); #endif matrix_setup(); - keyboard_pre_init_kb(); + keyboard_pre_init_quantum(); } #ifndef SPLIT_KEYBOARD @@ -355,6 +387,13 @@ __attribute__((weak)) bool should_process_keypress(void) { return is_keyboard_master(); } +/** \brief housekeeping_task_modules + * + * Codegen will override this if community modules are enabled. + * This is specific to keyboard-level functionality. + */ +__attribute__((weak)) void housekeeping_task_modules(void) {} + /** \brief housekeeping_task_kb * * Override this function if you have a need to execute code for every keyboard main loop iteration. @@ -374,6 +413,7 @@ __attribute__((weak)) void housekeeping_task_user(void) {} * Invokes hooks for executing code after QMK is done after each loop iteration. */ void housekeeping_task(void) { + housekeeping_task_modules(); housekeeping_task_kb(); housekeeping_task_user(); } @@ -493,7 +533,7 @@ void keyboard_init(void) { debug_enable = true; #endif - keyboard_post_init_kb(); /* Always keep this last */ + keyboard_post_init_quantum(); /* Always keep this last */ } /** \brief key_event_task diff --git a/quantum/quantum.c b/quantum/quantum.c index d4ebd58e7f..66e1cef24d 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -162,6 +162,10 @@ __attribute__((weak)) void tap_code16(uint16_t code) { tap_code16_delay(code, code == KC_CAPS_LOCK ? TAP_HOLD_CAPS_DELAY : TAP_CODE_DELAY); } +__attribute__((weak)) bool pre_process_record_modules(uint16_t keycode, keyrecord_t *record) { + return true; +} + __attribute__((weak)) bool pre_process_record_kb(uint16_t keycode, keyrecord_t *record) { return pre_process_record_user(keycode, record); } @@ -174,6 +178,10 @@ __attribute__((weak)) bool process_action_kb(keyrecord_t *record) { return true; } +__attribute__((weak)) bool process_record_modules(uint16_t keycode, keyrecord_t *record) { + return true; +} + __attribute__((weak)) bool process_record_kb(uint16_t keycode, keyrecord_t *record) { return process_record_user(keycode, record); } @@ -182,12 +190,18 @@ __attribute__((weak)) bool process_record_user(uint16_t keycode, keyrecord_t *re return true; } +__attribute__((weak)) void post_process_record_modules(uint16_t keycode, keyrecord_t *record) {} + __attribute__((weak)) void post_process_record_kb(uint16_t keycode, keyrecord_t *record) { post_process_record_user(keycode, record); } __attribute__((weak)) void post_process_record_user(uint16_t keycode, keyrecord_t *record) {} +__attribute__((weak)) bool shutdown_modules(bool jump_to_bootloader) { + return true; +} + void shutdown_quantum(bool jump_to_bootloader) { clear_keyboard(); #if defined(MIDI_ENABLE) && defined(MIDI_BASIC) @@ -199,11 +213,13 @@ void shutdown_quantum(bool jump_to_bootloader) { # endif uint16_t timer_start = timer_read(); PLAY_SONG(goodbye_song); + shutdown_modules(jump_to_bootloader); shutdown_kb(jump_to_bootloader); while (timer_elapsed(timer_start) < 250) wait_ms(1); stop_all_notes(); #else + shutdown_modules(jump_to_bootloader); shutdown_kb(jump_to_bootloader); wait_ms(250); #endif @@ -258,7 +274,7 @@ uint16_t get_event_keycode(keyevent_t event, bool update_layer_cache) { /* Get keycode, and then process pre tapping functionality */ bool pre_process_record_quantum(keyrecord_t *record) { - return pre_process_record_kb(get_record_keycode(record, true), record) && + return pre_process_record_modules(get_record_keycode(record, true), record) && pre_process_record_kb(get_record_keycode(record, true), record) && #ifdef COMBO_ENABLE process_combo(get_record_keycode(record, true), record) && #endif @@ -268,6 +284,7 @@ bool pre_process_record_quantum(keyrecord_t *record) { /* Get keycode, and then call keyboard function */ void post_process_record_quantum(keyrecord_t *record) { uint16_t keycode = get_record_keycode(record, false); + post_process_record_modules(keycode, record); post_process_record_kb(keycode, record); } @@ -335,7 +352,7 @@ bool process_record_quantum(keyrecord_t *record) { #if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE) process_auto_mouse(keycode, record) && #endif - process_record_kb(keycode, record) && + process_record_modules(keycode, record) && process_record_kb(keycode, record) && #if defined(SECURE_ENABLE) process_secure(keycode, record) && #endif diff --git a/quantum/quantum.h b/quantum/quantum.h index 9db88a54d4..856120334e 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -244,6 +244,10 @@ extern layer_state_t layer_state; # include "layer_lock.h" #endif +#ifdef COMMUNITY_MODULES_ENABLED +# include "community_modules.h" +#endif + void set_single_default_layer(uint8_t default_layer); void set_single_persistent_default_layer(uint8_t default_layer); From 5a70aecb6bda45aad53120923b0c9c2d33d84075 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 22 Jan 2025 14:19:56 +1100 Subject: [PATCH 04/25] Suspend callbacks. --- lib/python/qmk/cli/generate/community_modules.py | 2 ++ quantum/quantum.c | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index 88d63396e7..7ef041a728 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -22,6 +22,8 @@ MODULE_API_LIST = [ ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true'), ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val=None), ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', ret_val=None), + ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', ret_val=None), + ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', ret_val=None), ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', ret_val='true'), ] diff --git a/quantum/quantum.c b/quantum/quantum.c index 66e1cef24d..8239e22f72 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -202,6 +202,10 @@ __attribute__((weak)) bool shutdown_modules(bool jump_to_bootloader) { return true; } +__attribute__((weak)) void suspend_power_down_modules(void) {} + +__attribute__((weak)) void suspend_wakeup_init_modules(void) {} + void shutdown_quantum(bool jump_to_bootloader) { clear_keyboard(); #if defined(MIDI_ENABLE) && defined(MIDI_BASIC) @@ -543,6 +547,7 @@ __attribute__((weak)) bool shutdown_kb(bool jump_to_bootloader) { } void suspend_power_down_quantum(void) { + suspend_power_down_modules(); suspend_power_down_kb(); #ifndef NO_SUSPEND_POWER_DOWN // Turn off backlight @@ -610,6 +615,7 @@ __attribute__((weak)) void suspend_wakeup_init_quantum(void) { #if defined(RGB_MATRIX_ENABLE) rgb_matrix_set_suspend_state(false); #endif + suspend_wakeup_init_modules(); suspend_wakeup_init_kb(); } From fe3eac28ba3170908833ef2b9ad0aac6261fb58c Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 22 Jan 2025 17:00:06 +1100 Subject: [PATCH 05/25] Preprocessor guards, OS detection hooks. --- .../qmk/cli/generate/community_modules.py | 87 +++++++++++++------ modules/hello_world/hello_world.c | 2 +- quantum/os_detection.c | 7 ++ 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index 7ef041a728..5c72cd2455 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -1,3 +1,4 @@ +import contextlib from argcomplete.completers import FilesCompleter from milc import cli @@ -16,20 +17,30 @@ class ModuleAPI(AttrDict): MODULE_API_LIST = [ - ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', ret_val=None), - ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', ret_val=None), - ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true'), - ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true'), - ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val=None), - ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', ret_val=None), - ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', ret_val=None), - ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', ret_val=None), - ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', ret_val='true'), + ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', ret_val=None, guard=None), + ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', ret_val=None, guard=None), + ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true', guard=None), + ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true', guard=None), + ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val=None, guard=None), + ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', ret_val=None, guard=None), + ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', ret_val=None, guard=None), + ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', ret_val=None, guard=None), + ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', ret_val='true', guard=None), + ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', ret_val='true', guard="defined(OS_DETECTION_ENABLE)"), ] MODULE_API_VERSION = '20250122' +@contextlib.contextmanager +def api_guard(lines, api): + if api.guard: + lines.append(f'#if {api.guard}') + yield + if api.guard: + lines.append(f'#endif // {api.guard}') + + @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') @@ -48,22 +59,31 @@ def generate_community_modules_h(cli): '#include ', '', f'#define COMMUNITY_MODULES_API_VERSION {MODULE_API_VERSION}', - f'#define ASSERT_COMMUNITY_MODULES_API_VERSION(x) _Static_assert((x) <= COMMUNITY_MODULES_API_VERSION, "Modules API mismatch, module requires higher version of QMK with API >= {MODULE_API_VERSION}.")', + f'#define ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(x) _Static_assert((x) <= COMMUNITY_MODULES_API_VERSION, "Community module requires higher version of QMK modules API -- needs: " #x ", current: {MODULE_API_VERSION}.")', + '', + '#ifdef OS_DETECTION_ENABLE', + '#include "os_detection.h"', + '#endif // OS_DETECTION_ENABLE', '', 'typedef struct keyrecord_t keyrecord_t;', ] + keymap_json = parse_configurator_json(cli.args.filename) + if keymap_json and 'modules' in keymap_json: for module in keymap_json['modules']: lines.append('') lines.append(f'// From module: {module}') for api in MODULE_API_LIST: - lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') + with api_guard(lines, api): + lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') lines.append('') - lines.append(f'// Parent implementation') + lines.append('// Core wrapper') for api in MODULE_API_LIST: - lines.append(f'{api.ret_type} {api.name}_modules({api.args});') + with api_guard(lines, api): + lines.append(f'{api.ret_type} {api.name}_modules({api.args});') + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) @@ -77,29 +97,40 @@ def generate_community_modules_c(cli): if cli.args.output and cli.args.output.name == '-': cli.args.output = None - lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '', '#include "community_modules.h"'] + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + '#include "community_modules.h"', + ] + keymap_json = parse_configurator_json(cli.args.filename) + if keymap_json and 'modules' in keymap_json: + for module in keymap_json['modules']: lines.append('') for api in MODULE_API_LIST: lines.append('') - lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}({api.args}) {{') - if api.ret_val: - lines.append(f' return {api.ret_val};') - lines.append('}') + with api_guard(lines, api): + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}({api.args}) {{') + if api.ret_val: + lines.append(f' return {api.ret_val};') + lines.append('}') for api in MODULE_API_LIST: lines.append('') - lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') - if api.ret_type == 'bool': - lines.append(' return true') - for module in keymap_json['modules']: + with api_guard(lines, api): + lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') if api.ret_type == 'bool': - lines.append(f' && {api.name}_{module}({api.call_params})') - else: - lines.append(f' {api.name}_{module}({api.call_params});') - if api.ret_type == 'bool': - lines.append(' ;') - lines.append('}') + lines.append(' return true') + for module in keymap_json['modules']: + if api.ret_type == 'bool': + lines.append(f' && {api.name}_{module}({api.call_params})') + else: + lines.append(f' {api.name}_{module}({api.call_params});') + if api.ret_type == 'bool': + lines.append(' ;') + lines.append('}') + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index 4fe9bd052e..e46023cebf 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include QMK_KEYBOARD_H -ASSERT_COMMUNITY_MODULES_API_VERSION(20250122); +ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(20250122); static uint32_t init_timestamp = 0; diff --git a/quantum/os_detection.c b/quantum/os_detection.c index 0dd048a8c4..f3ce2d4cdd 100644 --- a/quantum/os_detection.c +++ b/quantum/os_detection.c @@ -72,6 +72,8 @@ static volatile struct usb_device_state maxprev_usb_device_state = {.configure_s static volatile bool debouncing = false; static volatile fast_timer_t last_time = 0; +bool process_detected_host_os_modules(os_variant_t os); + void os_detection_task(void) { #ifdef OS_DETECTION_KEYBOARD_RESET // resetting the keyboard on the USB device state change callback results in instability, so delegate that to this task @@ -96,12 +98,17 @@ void os_detection_task(void) { if (detected_os != reported_os || first_report) { first_report = false; reported_os = detected_os; + process_detected_host_os_modules(detected_os); process_detected_host_os_kb(detected_os); } } } } +__attribute__((weak)) bool process_detected_host_os_modules(os_variant_t os) { + return true; +} + __attribute__((weak)) bool process_detected_host_os_kb(os_variant_t detected_os) { return process_detected_host_os_user(detected_os); } From d4733cc5742d4a7259b9bee3f593db23392560f3 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Thu, 23 Jan 2025 23:21:07 +1100 Subject: [PATCH 06/25] user/kb module hooks. --- .../qmk/cli/generate/community_modules.py | 130 ++++++++++++------ modules/hello_world/hello_world.c | 4 + 2 files changed, 95 insertions(+), 39 deletions(-) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index 5c72cd2455..96a1c1fc94 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -17,23 +17,23 @@ class ModuleAPI(AttrDict): MODULE_API_LIST = [ - ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', ret_val=None, guard=None), - ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', ret_val=None, guard=None), - ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true', guard=None), - ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val='true', guard=None), - ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', ret_val=None, guard=None), - ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', ret_val=None, guard=None), - ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', ret_val=None, guard=None), - ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', ret_val=None, guard=None), - ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', ret_val='true', guard=None), - ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', ret_val='true', guard="defined(OS_DETECTION_ENABLE)"), + ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', guard=None, header=None), + ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', guard="defined(OS_DETECTION_ENABLE)", header="os_detection.h"), ] MODULE_API_VERSION = '20250122' @contextlib.contextmanager -def api_guard(lines, api): +def _api_guard(lines, api): if api.guard: lines.append(f'#if {api.guard}') yield @@ -41,6 +41,78 @@ def api_guard(lines, api): lines.append(f'#endif // {api.guard}') +def _render_api_header(api): + lines = [] + if api.header: + lines.append('') + with _api_guard(lines, api): + lines.append(f'#include <{api.header}>') + return lines + + +def _render_api_declarations(api, module, user_kb=True): + lines = [] + lines.append('') + with _api_guard(lines, api): + if user_kb: + lines.append(f'{api.ret_type} {api.name}_{module}_user({api.args});') + lines.append(f'{api.ret_type} {api.name}_{module}_kb({api.args});') + lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') + return lines + + +def _render_api_implementations(api, module): + lines = [] + lines.append('') + with _api_guard(lines, api): + # _user + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}_user({api.args}) {{') + if api.ret_type == 'bool': + lines.append(' return true;') + else: + pass + lines.append('}') + lines.append('') + + # _kb + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}_kb({api.args}) {{') + if api.ret_type == 'bool': + lines.append(f' if(!{api.name}_{module}_user({api.call_params})) {{ return false; }}') + lines.append(' return true;') + else: + lines.append(f' {api.name}_{module}_user({api.call_params});') + lines.append('}') + lines.append('') + + # module (non-suffixed) + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}({api.args}) {{') + if api.ret_type == 'bool': + lines.append(f' if(!{api.name}_{module}_kb({api.call_params})) {{ return false; }}') + lines.append(' return true;') + else: + lines.append(f' {api.name}_{module}_kb({api.call_params});') + lines.append('}') + return lines + + +def _render_core_implementation(api, keymap_json): + lines = [] + lines.append('') + with _api_guard(lines, api): + lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') + if api.ret_type == 'bool': + lines.append(' return true') + for module in keymap_json['modules']: + if api.ret_type == 'bool': + lines.append(f' && {api.name}_{module}({api.call_params})') + else: + lines.append(f' {api.name}_{module}({api.call_params});') + if api.ret_type == 'bool': + lines.append(' ;') + lines.append('}') + return lines + + @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') @@ -61,28 +133,26 @@ def generate_community_modules_h(cli): f'#define COMMUNITY_MODULES_API_VERSION {MODULE_API_VERSION}', f'#define ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(x) _Static_assert((x) <= COMMUNITY_MODULES_API_VERSION, "Community module requires higher version of QMK modules API -- needs: " #x ", current: {MODULE_API_VERSION}.")', '', - '#ifdef OS_DETECTION_ENABLE', - '#include "os_detection.h"', - '#endif // OS_DETECTION_ENABLE', + 'typedef struct keyrecord_t keyrecord_t; // forward declaration so we don\'t need to include quantum.h', '', - 'typedef struct keyrecord_t keyrecord_t;', ] keymap_json = parse_configurator_json(cli.args.filename) + for api in MODULE_API_LIST: + lines.extend(_render_api_header(api)) + if keymap_json and 'modules' in keymap_json: for module in keymap_json['modules']: lines.append('') lines.append(f'// From module: {module}') for api in MODULE_API_LIST: - with api_guard(lines, api): - lines.append(f'{api.ret_type} {api.name}_{module}({api.args});') + lines.extend(_render_api_declarations(api, module)) lines.append('') lines.append('// Core wrapper') for api in MODULE_API_LIST: - with api_guard(lines, api): - lines.append(f'{api.ret_type} {api.name}_modules({api.args});') + lines.extend(_render_api_declarations(api, 'modules', user_kb=False)) dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) @@ -109,28 +179,10 @@ def generate_community_modules_c(cli): if keymap_json and 'modules' in keymap_json: for module in keymap_json['modules']: - lines.append('') for api in MODULE_API_LIST: - lines.append('') - with api_guard(lines, api): - lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}({api.args}) {{') - if api.ret_val: - lines.append(f' return {api.ret_val};') - lines.append('}') + lines.extend(_render_api_implementations(api, module)) for api in MODULE_API_LIST: - lines.append('') - with api_guard(lines, api): - lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') - if api.ret_type == 'bool': - lines.append(' return true') - for module in keymap_json['modules']: - if api.ret_type == 'bool': - lines.append(f' && {api.name}_{module}({api.call_params})') - else: - lines.append(f' {api.name}_{module}({api.call_params});') - if api.ret_type == 'bool': - lines.append(' ;') - lines.append('}') + lines.extend(_render_core_implementation(api, keymap_json)) dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index e46023cebf..0f02640cc4 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -7,10 +7,14 @@ ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(20250122); static uint32_t init_timestamp = 0; void keyboard_post_init_hello_world(void) { + keyboard_post_init_hello_world_kb(); + init_timestamp = timer_read32(); } void housekeeping_task_hello_world(void) { + housekeeping_task_hello_world_kb(); + static bool welcomed = false; if(!welcomed && timer_elapsed32(init_timestamp) > 10000) { welcomed = true; From c9ee24ba86507a582d89a26f198414c0e6e42790 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 01:11:45 +1100 Subject: [PATCH 07/25] keyboard and keymap modules --- builddefs/build_keyboard.mk | 85 ++++++++++--------- .../qmk/cli/generate/community_modules.py | 34 ++++---- lib/python/qmk/cli/generate/rules_mk.py | 7 +- lib/python/qmk/cli/info.py | 5 ++ lib/python/qmk/info.py | 27 ++++++ 5 files changed, 97 insertions(+), 61 deletions(-) diff --git a/builddefs/build_keyboard.mk b/builddefs/build_keyboard.mk index e4661a7f3f..9ae7b213a8 100644 --- a/builddefs/build_keyboard.mk +++ b/builddefs/build_keyboard.mk @@ -112,6 +112,39 @@ endif ifneq ("$(wildcard $(KEYBOARD_PATH_1)/rules.mk)","") include $(KEYBOARD_PATH_1)/rules.mk endif +# Create dependencies on DD keyboard config - structure validated elsewhere +DD_CONFIG_FILES := +ifneq ("$(wildcard $(KEYBOARD_PATH_1)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/info.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_2)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/info.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_3)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/info.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_4)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/info.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/info.json +endif + +ifneq ("$(wildcard $(KEYBOARD_PATH_1)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/keyboard.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_2)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/keyboard.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_3)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/keyboard.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_4)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/keyboard.json +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_5)/keyboard.json)","") + DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/keyboard.json +endif MAIN_KEYMAP_PATH_1 := $(KEYBOARD_PATH_1)/keymaps/$(KEYMAP) MAIN_KEYMAP_PATH_2 := $(KEYBOARD_PATH_2)/keymaps/$(KEYMAP) @@ -207,38 +240,40 @@ ifneq ("$(wildcard $(KEYMAP_JSON))", "") include $(INFO_RULES_MK) # Add rules to generate the keymap files - indentation here is important -$(INTERMEDIATE_OUTPUT)/src/keymap.c: $(KEYMAP_JSON) +$(INTERMEDIATE_OUTPUT)/src/keymap.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) $(eval CMD=$(QMK_BIN) json2c --quiet --output $(KEYMAP_C) $(KEYMAP_JSON)) @$(BUILD_CMD) -$(INTERMEDIATE_OUTPUT)/src/config.h: $(KEYMAP_JSON) +$(INTERMEDIATE_OUTPUT)/src/config.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) $(eval CMD=$(QMK_BIN) generate-config-h --quiet --output $(KEYMAP_H) $(KEYMAP_JSON)) @$(BUILD_CMD) -$(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) +$(INTERMEDIATE_OUTPUT)/src/keymap.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) $(eval CMD=$(QMK_BIN) generate-keymap-h --quiet --output $(INTERMEDIATE_OUTPUT)/src/keymap.h $(KEYMAP_JSON)) @$(BUILD_CMD) generated-files: $(INTERMEDIATE_OUTPUT)/src/config.h $(INTERMEDIATE_OUTPUT)/src/keymap.c $(INTERMEDIATE_OUTPUT)/src/keymap.h -$(INTERMEDIATE_OUTPUT)/src/community_modules.h: $(KEYMAP_JSON) +endif + +# Community modules +$(INTERMEDIATE_OUTPUT)/src/community_modules.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) - $(eval CMD=$(QMK_BIN) generate-community-modules-h --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(KEYMAP_JSON)) + $(eval CMD=$(QMK_BIN) generate-community-modules-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(KEYMAP_JSON)) @$(BUILD_CMD) -$(INTERMEDIATE_OUTPUT)/src/community_modules.c: $(KEYMAP_JSON) +$(INTERMEDIATE_OUTPUT)/src/community_modules.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) - $(eval CMD=$(QMK_BIN) generate-community-modules-c --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(KEYMAP_JSON)) + $(eval CMD=$(QMK_BIN) generate-community-modules-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(KEYMAP_JSON)) @$(BUILD_CMD) SRC += $(INTERMEDIATE_OUTPUT)/src/community_modules.c generated-files: $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c -endif include $(BUILDDEFS_PATH)/converters.mk @@ -362,40 +397,6 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/post_config.h)","") POST_CONFIG_H += $(KEYBOARD_PATH_5)/post_config.h endif -# Create dependencies on DD keyboard config - structure validated elsewhere -DD_CONFIG_FILES := -ifneq ("$(wildcard $(KEYBOARD_PATH_1)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/info.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_2)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/info.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_3)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/info.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_4)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/info.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/info.json -endif - -ifneq ("$(wildcard $(KEYBOARD_PATH_1)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_1)/keyboard.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_2)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_2)/keyboard.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_3)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_3)/keyboard.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_4)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_4)/keyboard.json -endif -ifneq ("$(wildcard $(KEYBOARD_PATH_5)/keyboard.json)","") - DD_CONFIG_FILES += $(KEYBOARD_PATH_5)/keyboard.json -endif - CONFIG_H += $(INTERMEDIATE_OUTPUT)/src/info_config.h KEYBOARD_SRC += $(INTERMEDIATE_OUTPUT)/src/default_keyboard.c diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index 96a1c1fc94..cc192e20e4 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -5,7 +5,9 @@ from milc import cli from milc.attrdict import AttrDict import qmk.path -from qmk.commands import dump_lines, parse_configurator_json +from qmk.info import get_modules +from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.commands import dump_lines from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE @@ -95,14 +97,14 @@ def _render_api_implementations(api, module): return lines -def _render_core_implementation(api, keymap_json): +def _render_core_implementation(api, modules): lines = [] lines.append('') with _api_guard(lines, api): lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') if api.ret_type == 'bool': lines.append(' return true') - for module in keymap_json['modules']: + for module in modules: if api.ret_type == 'bool': lines.append(f' && {api.name}_{module}({api.call_params})') else: @@ -115,7 +117,8 @@ def _render_core_implementation(api, keymap_json): @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") -@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.h for.') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') @cli.subcommand('Creates a community_modules.h from a keymap.json file.') def generate_community_modules_h(cli): """Creates a community_modules.h from a keymap.json file @@ -137,13 +140,12 @@ def generate_community_modules_h(cli): '', ] - keymap_json = parse_configurator_json(cli.args.filename) + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + for api in MODULE_API_LIST: + lines.extend(_render_api_header(api)) - for api in MODULE_API_LIST: - lines.extend(_render_api_header(api)) - - if keymap_json and 'modules' in keymap_json: - for module in keymap_json['modules']: + for module in modules: lines.append('') lines.append(f'// From module: {module}') for api in MODULE_API_LIST: @@ -159,7 +161,8 @@ def generate_community_modules_h(cli): @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") -@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') @cli.subcommand('Creates a community_modules.c from a keymap.json file.') def generate_community_modules_c(cli): """Creates a community_modules.c from a keymap.json file @@ -174,15 +177,14 @@ def generate_community_modules_c(cli): '#include "community_modules.h"', ] - keymap_json = parse_configurator_json(cli.args.filename) + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: - if keymap_json and 'modules' in keymap_json: - - for module in keymap_json['modules']: + for module in modules: for api in MODULE_API_LIST: lines.extend(_render_api_implementations(api, module)) for api in MODULE_API_LIST: - lines.extend(_render_core_implementation(api, keymap_json)) + lines.extend(_render_core_implementation(api, modules)) dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index ea44f1e58a..0a888f863c 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -6,7 +6,7 @@ from dotty_dict import dotty from argcomplete.completers import FilesCompleter from milc import cli -from qmk.info import info_json +from qmk.info import info_json, get_modules from qmk.json_schema import json_load from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.commands import dump_lines, parse_configurator_json @@ -100,10 +100,11 @@ def generate_rules_mk(cli): if converter: rules_mk_lines.append(generate_rule('CONVERT_TO', converter)) - if user_keymap and 'modules' in user_keymap: + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: rules_mk_lines.append('') rules_mk_lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLED=TRUE') - for module in user_keymap['modules']: + for module in modules: rules_mk_lines.append('') rules_mk_lines.append(f'COMMUNITY_MODULES += {module}') rules_mk_lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module.upper()}=TRUE') diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index e662407474..5925b57258 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -52,6 +52,11 @@ def show_keymap(kb_info_json, title_caps=True): if keymap_path and keymap_path.suffix == '.json': keymap_data = json.load(keymap_path.open(encoding='utf-8')) + + # cater for layout-less keymap.json + if 'layout' not in keymap_data: + return + layout_name = keymap_data['layout'] layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 38ca89697c..08b15cf799 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -1052,3 +1052,30 @@ def keymap_json(keyboard, keymap, force_layout=None): _extract_config_h(kb_info_json, parse_config_h_file(keymap_config)) return kb_info_json + + +def get_modules(keyboard, keymap_filename): + """Get the modules for a keyboard/keymap. + """ + modules = [] + + if keymap_filename: + keymap_json = parse_configurator_json(keymap_filename) + + if keymap_json: + kb = keymap_json.get('keyboard', None) + if not kb: + kb = keyboard + + if kb: + kb_info_json = info_json(kb) + if kb_info_json: + modules.extend(kb_info_json.get('modules', [])) + + modules.extend(keymap_json.get('modules', [])) + + elif keyboard: + kb_info_json = info_json(keyboard) + modules.extend(kb_info_json.get('modules', [])) + + return list(dict.fromkeys(modules)) # remove dupes From 54673745a6d51b84715827e75058a3b28110f567 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 01:17:16 +1100 Subject: [PATCH 08/25] nitpicks --- data/schemas/keyboard.jsonschema | 6 ++++++ lib/python/qmk/cli/generate/rules_mk.py | 1 - lib/python/qmk/commands.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index ec87680fa0..21996f55be 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -463,6 +463,12 @@ "rows": {"$ref": "qmk.definitions.v1#/mcu_pin_array"} } }, + "modules": { + "type": "array", + "items": { + "type": "string" + } + }, "mouse_key": { "type": "object", "properties": { diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 0a888f863c..84d7e11853 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -55,7 +55,6 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict): def generate_rules_mk(cli): """Generates a rules.mk file from info.json. """ - user_keymap = None converter = None # Determine our keyboard/keymap if cli.args.filename: diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 38e032765f..0e1876ca7a 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -104,7 +104,7 @@ def dump_lines(output_file, lines, quiet=True, remove_repeated_newlines=False): """ generated = '\n'.join(lines) + '\n' if remove_repeated_newlines: - if '\n\n\n' in generated: + while '\n\n\n' in generated: generated = generated.replace('\n\n\n', '\n\n') if output_file and output_file.name != '-': output_file.parent.mkdir(parents=True, exist_ok=True) From 1354424fd70984ccc9a6242dde893c96b7340b56 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 09:22:49 +1100 Subject: [PATCH 09/25] First stab at keycodes. --- data/constants/keycodes/keycodes_0.0.7.hjson | 7 +++ data/schemas/community_module.jsonschema | 16 +++++ lib/python/qmk/cli/format/json.py | 13 +++- .../qmk/cli/generate/community_modules.py | 59 ++++++++++--------- lib/python/qmk/community_modules.py | 48 +++++++++++++++ lib/python/qmk/json_encoders.py | 21 +++++++ modules/hello_world/hello_world.c | 16 +++++ modules/hello_world/qmk_module.json | 7 +++ quantum/keycodes.h | 3 + 9 files changed, 159 insertions(+), 31 deletions(-) create mode 100644 data/constants/keycodes/keycodes_0.0.7.hjson create mode 100644 data/schemas/community_module.jsonschema create mode 100644 lib/python/qmk/community_modules.py create mode 100644 modules/hello_world/qmk_module.json diff --git a/data/constants/keycodes/keycodes_0.0.7.hjson b/data/constants/keycodes/keycodes_0.0.7.hjson new file mode 100644 index 0000000000..52e1a50443 --- /dev/null +++ b/data/constants/keycodes/keycodes_0.0.7.hjson @@ -0,0 +1,7 @@ +{ + "ranges": { + "0x77C0/0x003F": { + "define": "QK_COMMUNITY_MODULE" + } + } +} diff --git a/data/schemas/community_module.jsonschema b/data/schemas/community_module.jsonschema new file mode 100644 index 0000000000..34362a918b --- /dev/null +++ b/data/schemas/community_module.jsonschema @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema#", + "$id": "qmk.community_module.v1", + "title": "Community Module Information", + "type": "object", + "required": ["module_name", "maintainer"] + "properties": { + "module_name": {"$ref": "qmk.definitions.v1#/text_identifier"}, + "maintainer": {"$ref": "qmk.definitions.v1#/text_identifier"}, + "url": { + "type": "string", + "format": "uri" + }, + "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"}, + } +} diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 3670294434..61f5254184 100755 --- a/lib/python/qmk/cli/format/json.py +++ b/lib/python/qmk/cli/format/json.py @@ -9,7 +9,7 @@ from milc import cli from qmk.info import info_json from qmk.json_schema import json_load, validate -from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder +from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder, UserspaceJSONEncoder, CommunityModuleJSONEncoder from qmk.path import normpath @@ -30,6 +30,13 @@ def _detect_json_format(file, json_data): except ValidationError: pass + if json_encoder is None: + try: + validate(json_data, 'qmk.community_module.v1') + json_encoder = CommunityModuleJSONEncoder + except ValidationError: + pass + if json_encoder is None: try: validate(json_data, 'qmk.keyboard.v1') @@ -54,6 +61,8 @@ def _get_json_encoder(file, json_data): json_encoder = KeymapJSONEncoder elif cli.args.format == 'userspace': json_encoder = UserspaceJSONEncoder + elif cli.args.format == 'community_module': + json_encoder = CommunityModuleJSONEncoder else: # This should be impossible cli.log.error('Unknown format: %s', cli.args.format) @@ -61,7 +70,7 @@ def _get_json_encoder(file, json_data): @cli.argument('json_file', arg_only=True, type=normpath, help='JSON file to format') -@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') +@cli.argument('-f', '--format', choices=['auto', 'keyboard', 'keymap', 'userspace', 'community_module'], default='auto', arg_only=True, help='JSON formatter to use (Default: autodetect)') @cli.argument('-i', '--inplace', action='store_true', arg_only=True, help='If set, will operate in-place on the input file') @cli.argument('-p', '--print', action='store_true', arg_only=True, help='If set, will print the formatted json to stdout ') @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index cc192e20e4..e634521f77 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -2,40 +2,17 @@ import contextlib from argcomplete.completers import FilesCompleter from milc import cli -from milc.attrdict import AttrDict import qmk.path from qmk.info import get_modules from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.commands import dump_lines from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE - - -class ModuleAPI(AttrDict): - def __init__(self, **kwargs): - super().__init__() - for key, value in kwargs.items(): - self[key] = value - - -MODULE_API_LIST = [ - ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), - ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), - ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), - ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', guard=None, header=None), - ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', guard=None, header=None), - ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', guard="defined(OS_DETECTION_ENABLE)", header="os_detection.h"), -] - -MODULE_API_VERSION = '20250122' +from qmk.community_modules import MODULE_API_LIST, MODULE_API_VERSION, load_module_jsons @contextlib.contextmanager -def _api_guard(lines, api): +def _render_api_guard(lines, api): if api.guard: lines.append(f'#if {api.guard}') yield @@ -47,15 +24,35 @@ def _render_api_header(api): lines = [] if api.header: lines.append('') - with _api_guard(lines, api): + with _render_api_guard(lines, api): lines.append(f'#include <{api.header}>') return lines +def _render_keycodes(module_jsons): + lines = [] + lines.append('') + lines.append('enum {') + first = True + for module_json in module_jsons: + keycodes = module_json.get('keycodes', []) + for keycode in keycodes: + key = keycode.get('key', None) + if first: + lines.append(f' {key} = QK_COMMUNITY_MODULE,') + first = False + else: + lines.append(f' {key},') + lines.append(' LAST_COMMUNITY_MODULE_KEY') + lines.append('};') + lines.append('_Static_assert(LAST_COMMUNITY_MODULE_KEY <= QK_COMMUNITY_MODULE_MAX, "Too many community module keycodes");') + return lines + + def _render_api_declarations(api, module, user_kb=True): lines = [] lines.append('') - with _api_guard(lines, api): + with _render_api_guard(lines, api): if user_kb: lines.append(f'{api.ret_type} {api.name}_{module}_user({api.args});') lines.append(f'{api.ret_type} {api.name}_{module}_kb({api.args});') @@ -66,7 +63,7 @@ def _render_api_declarations(api, module, user_kb=True): def _render_api_implementations(api, module): lines = [] lines.append('') - with _api_guard(lines, api): + with _render_api_guard(lines, api): # _user lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}_user({api.args}) {{') if api.ret_type == 'bool': @@ -100,7 +97,7 @@ def _render_api_implementations(api, module): def _render_core_implementation(api, modules): lines = [] lines.append('') - with _api_guard(lines, api): + with _render_api_guard(lines, api): lines.append(f'{api.ret_type} {api.name}_modules({api.args}) {{') if api.ret_type == 'bool': lines.append(' return true') @@ -132,6 +129,7 @@ def generate_community_modules_h(cli): '#pragma once', '#include ', '#include ', + '#include ', '', f'#define COMMUNITY_MODULES_API_VERSION {MODULE_API_VERSION}', f'#define ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(x) _Static_assert((x) <= COMMUNITY_MODULES_API_VERSION, "Community module requires higher version of QMK modules API -- needs: " #x ", current: {MODULE_API_VERSION}.")', @@ -141,7 +139,10 @@ def generate_community_modules_h(cli): ] modules = get_modules(cli.args.keyboard, cli.args.filename) + module_jsons = load_module_jsons(modules) if len(modules) > 0: + lines.extend(_render_keycodes(module_jsons)) + for api in MODULE_API_LIST: lines.extend(_render_api_header(api)) diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py new file mode 100644 index 0000000000..fa9b1ac84b --- /dev/null +++ b/lib/python/qmk/community_modules.py @@ -0,0 +1,48 @@ +import os +from pathlib import Path + +from milc.attrdict import AttrDict + +from qmk.json_schema import json_load, validate +from qmk.util import truthy + +class ModuleAPI(AttrDict): + def __init__(self, **kwargs): + super().__init__() + for key, value in kwargs.items(): + self[key] = value + + +MODULE_API_LIST = [ + ModuleAPI(ret_type='void', name='keyboard_pre_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='keyboard_post_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='bool', name='pre_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='bool', name='process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='void', name='post_process_record', args='uint16_t keycode, keyrecord_t *record', call_params='keycode, record', guard=None, header=None), + ModuleAPI(ret_type='void', name='housekeeping_task', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='suspend_power_down', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='void', name='suspend_wakeup_init', args='void', call_params='', guard=None, header=None), + ModuleAPI(ret_type='bool', name='shutdown', args='bool jump_to_bootloader', call_params='jump_to_bootloader', guard=None, header=None), + ModuleAPI(ret_type='bool', name='process_detected_host_os', args='os_variant_t os', call_params='os', guard="defined(OS_DETECTION_ENABLE)", header="os_detection.h"), +] + +MODULE_API_VERSION = '20250122' + +def load_module_jsons(modules): + """Load the module JSON files, matching the specified order. + """ + module_jsons = [] + for module in modules: + + module_json_path = Path('modules') / module / 'qmk_module.json' + if not module_json_path.exists(): + raise FileNotFoundError(f'Module JSON file not found: {module_json_path}') + + module_json = json_load(module_json_path) + + if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False): + validate(module_json, 'qmk.community_module.v1') + + module_jsons.append(module_json) + + return module_jsons diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 0e4ad1d220..25a01e9e21 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py @@ -235,3 +235,24 @@ class UserspaceJSONEncoder(QMKJSONEncoder): return '01build_targets' return key + + +class CommunityModuleJSONEncoder(QMKJSONEncoder): + """Custom encoder to make qmk_module.json's a little nicer to work with. + """ + def sort_dict(self, item): + """Sorts the hashes in a nice way. + """ + key = item[0] + + if self.indentation_level == 1: + if key == 'module_name': + return '00module_name' + if key == 'maintainer': + return '01maintainer' + if key == 'url': + return '02url' + if key == 'keycodes': + return '03keycodes' + + return key diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index 0f02640cc4..c3bbbc64b8 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -21,3 +21,19 @@ void housekeeping_task_hello_world(void) { print("Hello, world! I'm a QMK based keyboard!\n"); } } + +bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) { + if (!process_record_hello_world_kb(keycode, record)) { + return false; + } + + switch (keycode) { + case CM_HELLO: + if (record->event.pressed) { + SEND_STRING("Hello there."); + break; + } + } + + return true; +} diff --git a/modules/hello_world/qmk_module.json b/modules/hello_world/qmk_module.json new file mode 100644 index 0000000000..87410e08e6 --- /dev/null +++ b/modules/hello_world/qmk_module.json @@ -0,0 +1,7 @@ +{ + "module_name": "Hello World", + "maintainer": "QMK Maintainers", + "keycodes": [ + { "key": "CM_HELLO" } + ] +} diff --git a/quantum/keycodes.h b/quantum/keycodes.h index 5929e35687..b4fc38f5ff 100644 --- a/quantum/keycodes.h +++ b/quantum/keycodes.h @@ -76,6 +76,8 @@ enum qk_keycode_ranges { QK_MACRO_MAX = 0x777F, QK_CONNECTION = 0x7780, QK_CONNECTION_MAX = 0x77BF, + QK_COMMUNITY_MODULE = 0x77C0, + QK_COMMUNITY_MODULE_MAX = 0x77FF, QK_LIGHTING = 0x7800, QK_LIGHTING_MAX = 0x78FF, QK_QUANTUM = 0x7C00, @@ -1476,6 +1478,7 @@ enum qk_keycode_defines { #define IS_QK_STENO(code) ((code) >= QK_STENO && (code) <= QK_STENO_MAX) #define IS_QK_MACRO(code) ((code) >= QK_MACRO && (code) <= QK_MACRO_MAX) #define IS_QK_CONNECTION(code) ((code) >= QK_CONNECTION && (code) <= QK_CONNECTION_MAX) +#define IS_QK_COMMUNITY_MODULE(code) ((code) >= QK_COMMUNITY_MODULE && (code) <= QK_COMMUNITY_MODULE_MAX) #define IS_QK_LIGHTING(code) ((code) >= QK_LIGHTING && (code) <= QK_LIGHTING_MAX) #define IS_QK_QUANTUM(code) ((code) >= QK_QUANTUM && (code) <= QK_QUANTUM_MAX) #define IS_QK_KB(code) ((code) >= QK_KB && (code) <= QK_KB_MAX) From 1c4c6ae2803c8f8639627f8f78b917c0ae876eeb Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 09:26:14 +1100 Subject: [PATCH 10/25] Appease the gods of formatting. --- lib/python/qmk/cli/generate/community_modules.py | 2 +- lib/python/qmk/community_modules.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index e634521f77..78e71e5086 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -45,7 +45,7 @@ def _render_keycodes(module_jsons): lines.append(f' {key},') lines.append(' LAST_COMMUNITY_MODULE_KEY') lines.append('};') - lines.append('_Static_assert(LAST_COMMUNITY_MODULE_KEY <= QK_COMMUNITY_MODULE_MAX, "Too many community module keycodes");') + lines.append('_Static_assert((int)LAST_COMMUNITY_MODULE_KEY <= (int)(QK_COMMUNITY_MODULE_MAX+1), "Too many community module keycodes");') return lines diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py index fa9b1ac84b..de7eb9cfb1 100644 --- a/lib/python/qmk/community_modules.py +++ b/lib/python/qmk/community_modules.py @@ -6,6 +6,7 @@ from milc.attrdict import AttrDict from qmk.json_schema import json_load, validate from qmk.util import truthy + class ModuleAPI(AttrDict): def __init__(self, **kwargs): super().__init__() @@ -28,6 +29,7 @@ MODULE_API_LIST = [ MODULE_API_VERSION = '20250122' + def load_module_jsons(modules): """Load the module JSON files, matching the specified order. """ From c6454b55852e1ef2dedc1f4134361ee693316066 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 21:42:33 +1100 Subject: [PATCH 11/25] Add support for `features` object. --- data/schemas/community_module.jsonschema | 1 + data/schemas/keyboard.jsonschema | 11 +++--- lib/python/qmk/cli/generate/rules_mk.py | 48 +++++++++++++++++------- lib/python/qmk/json_encoders.py | 4 +- modules/hello_world/hello_world.c | 14 +++---- modules/hello_world/qmk_module.json | 3 ++ 6 files changed, 52 insertions(+), 29 deletions(-) diff --git a/data/schemas/community_module.jsonschema b/data/schemas/community_module.jsonschema index 34362a918b..a3474476df 100644 --- a/data/schemas/community_module.jsonschema +++ b/data/schemas/community_module.jsonschema @@ -12,5 +12,6 @@ "format": "uri" }, "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"}, + "features": {"$ref": "qmk.keyboard.v1#/definitions/features_config"}, } } diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 21996f55be..c626765c3b 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -31,6 +31,11 @@ "pins": {"$ref": "qmk.definitions.v1#/mcu_pin_array"} } } + "features_config": { + "$ref": "qmk.definitions.v1#/boolean_array", + "propertyNames": {"$ref": "qmk.definitions.v1#/snake_case"}, + "not": {"required": ["lto"]} + }, }, "type": "object", "not": {"required": ["vendorId", "productId"]}, // reject via keys... @@ -328,11 +333,7 @@ "enabled": {"type": "boolean"} } }, - "features": { - "$ref": "qmk.definitions.v1#/boolean_array", - "propertyNames": {"$ref": "qmk.definitions.v1#/snake_case"}, - "not": {"required": ["lto"]} - }, + "features": { "$ref": "#/definitions/features_config" }, "indicators": { "type": "object", "properties": { diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 84d7e11853..35e4f7e7c6 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -12,6 +12,7 @@ from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.commands import dump_lines, parse_configurator_json from qmk.path import normpath, FileType from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE +from qmk.community_modules import load_module_jsons def generate_rule(rules_key, rules_value): @@ -46,6 +47,37 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict): return generate_rule(rules_key, rules_value) +def generate_features_rules(features_dict): + lines = [] + for feature, enabled in features_dict.items(): + feature = feature.upper() + enabled = 'yes' if enabled else 'no' + lines.append(generate_rule(f'{feature}_ENABLE', enabled)) + return lines + + +def generate_modules_rules(keyboard, filename): + lines = [] + modules = get_modules(keyboard, filename) + if len(modules) > 0: + lines.append('') + lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLED=TRUE') + for module in modules: + lines.append('') + lines.append(f'COMMUNITY_MODULES += {module}') + lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module.upper()}=TRUE') + lines.append(f'VPATH += modules/{module}') + lines.append(f'include modules/{module}/rules.mk') + + module_jsons = load_module_jsons(modules) + for module_json in module_jsons: + if 'features' in module_json: + lines.append('') + lines.append(f'# Module: {module_json["module_name"]}') + lines.extend(generate_features_rules(module_json['features'])) + return lines + + @cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.') @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @@ -80,10 +112,7 @@ def generate_rules_mk(cli): # Iterate through features to enable/disable them if 'features' in kb_info_json: - for feature, enabled in kb_info_json['features'].items(): - feature = feature.upper() - enabled = 'yes' if enabled else 'no' - rules_mk_lines.append(generate_rule(f'{feature}_ENABLE', enabled)) + rules_mk_lines.extend(generate_features_rules(kb_info_json['features'])) # Set SPLIT_TRANSPORT, if needed if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom': @@ -99,16 +128,7 @@ def generate_rules_mk(cli): if converter: rules_mk_lines.append(generate_rule('CONVERT_TO', converter)) - modules = get_modules(cli.args.keyboard, cli.args.filename) - if len(modules) > 0: - rules_mk_lines.append('') - rules_mk_lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLED=TRUE') - for module in modules: - rules_mk_lines.append('') - rules_mk_lines.append(f'COMMUNITY_MODULES += {module}') - rules_mk_lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module.upper()}=TRUE') - rules_mk_lines.append(f'VPATH += modules/{module}') - rules_mk_lines.append(f'include modules/{module}/rules.mk') + rules_mk_lines.extend(generate_modules_rules(cli.args.keyboard, cli.args.filename)) # Show the results dump_lines(cli.args.output, rules_mk_lines) diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 25a01e9e21..c6822c22b2 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py @@ -252,7 +252,9 @@ class CommunityModuleJSONEncoder(QMKJSONEncoder): return '01maintainer' if key == 'url': return '02url' + if key == 'features': + return '03features' if key == 'keycodes': - return '03keycodes' + return '04keycodes' return key diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index c3bbbc64b8..8e12ea636a 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -4,22 +4,18 @@ ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(20250122); -static uint32_t init_timestamp = 0; +uint32_t delayed_hello_world(uint32_t trigger_time, void *cb_arg) { + print("Hello, world! I'm a QMK based keyboard!\n"); + return 0; +} void keyboard_post_init_hello_world(void) { keyboard_post_init_hello_world_kb(); - - init_timestamp = timer_read32(); + defer_exec(10000, delayed_hello_world, NULL); } void housekeeping_task_hello_world(void) { housekeeping_task_hello_world_kb(); - - static bool welcomed = false; - if(!welcomed && timer_elapsed32(init_timestamp) > 10000) { - welcomed = true; - print("Hello, world! I'm a QMK based keyboard!\n"); - } } bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) { diff --git a/modules/hello_world/qmk_module.json b/modules/hello_world/qmk_module.json index 87410e08e6..1dbb1f6fbf 100644 --- a/modules/hello_world/qmk_module.json +++ b/modules/hello_world/qmk_module.json @@ -1,6 +1,9 @@ { "module_name": "Hello World", "maintainer": "QMK Maintainers", + "features": { + "deferred_exec": true + }, "keycodes": [ { "key": "CM_HELLO" } ] From fa53413a5ddaaddec679ab5d90b6922d5436fb88 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 21:47:45 +1100 Subject: [PATCH 12/25] Ensure modules are subject to formatting rules. --- .github/workflows/format.yml | 1 + lib/python/qmk/cli/format/c.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 74c518fe05..b4e32f981b 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -10,6 +10,7 @@ on: - 'lib/arm_atsam/**' - 'lib/lib8tion/**' - 'lib/python/**' + - 'modules/**' - 'platforms/**' - 'quantum/**' - 'tests/**' diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py index a58aef3fbc..65818155b0 100644 --- a/lib/python/qmk/cli/format/c.py +++ b/lib/python/qmk/cli/format/c.py @@ -10,7 +10,7 @@ from qmk.path import normpath from qmk.c_parse import c_source_files c_file_suffixes = ('c', 'h', 'cpp', 'hpp') -core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms') +core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms', 'modules') ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards') From d3300daaa7a096ad0baf3c5ac063bf136f93a4a4 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 21:49:17 +1100 Subject: [PATCH 13/25] Automatic addition of `community_module` label. --- .github/labeler.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index 270cd1a813..82f9672bb7 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -54,3 +54,7 @@ dd: - data/constants/** - data/mappings/** - data/schemas/** +community_module: + - changed-files: + - any-glob-to-any-file: + - modules/** From 8b47562c5d3e912a9596251c2617a88f21431ff1 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 22:57:54 +1100 Subject: [PATCH 14/25] Userspace modules. --- lib/python/qmk/cli/generate/rules_mk.py | 7 ++-- lib/python/qmk/community_modules.py | 51 +++++++++++++++++-------- modules/hello_world/hello_world.c | 4 -- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 35e4f7e7c6..7436194492 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -12,7 +12,7 @@ from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.commands import dump_lines, parse_configurator_json from qmk.path import normpath, FileType from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE -from qmk.community_modules import load_module_jsons +from qmk.community_modules import find_module_path, load_module_jsons def generate_rule(rules_key, rules_value): @@ -63,11 +63,12 @@ def generate_modules_rules(keyboard, filename): lines.append('') lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLED=TRUE') for module in modules: + module_path = find_module_path(module) lines.append('') lines.append(f'COMMUNITY_MODULES += {module}') lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module.upper()}=TRUE') - lines.append(f'VPATH += modules/{module}') - lines.append(f'include modules/{module}/rules.mk') + lines.append(f'VPATH += {module_path}') + lines.append(f'-include {module_path}/rules.mk') module_jsons = load_module_jsons(modules) for module_json in module_jsons: diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py index de7eb9cfb1..49cde79c7a 100644 --- a/lib/python/qmk/community_modules.py +++ b/lib/python/qmk/community_modules.py @@ -1,10 +1,12 @@ import os -from pathlib import Path from milc.attrdict import AttrDict from qmk.json_schema import json_load, validate from qmk.util import truthy +from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE + +COMMUNITY_MODULE_JSON_FILENAME = 'qmk_module.json' class ModuleAPI(AttrDict): @@ -30,21 +32,38 @@ MODULE_API_LIST = [ MODULE_API_VERSION = '20250122' +def find_module_path(module): + """Find a module by name. + """ + search_dirs = [] + if HAS_QMK_USERSPACE: + search_dirs.append(QMK_USERSPACE / 'modules') + search_dirs.append(QMK_FIRMWARE / 'modules') + + for search_dir in search_dirs: + module_path = search_dir / module + if module_path.exists() and (module_path / COMMUNITY_MODULE_JSON_FILENAME).exists(): + return module_path + + return None + + +def load_module_json(module): + """Load a module JSON file. + """ + module_path = find_module_path(module) + if not module_path: + raise FileNotFoundError(f'Module not found: {module}') + + module_json = json_load(module_path / COMMUNITY_MODULE_JSON_FILENAME) + + if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False): + validate(module_json, 'qmk.community_module.v1') + + return module_json + + def load_module_jsons(modules): """Load the module JSON files, matching the specified order. """ - module_jsons = [] - for module in modules: - - module_json_path = Path('modules') / module / 'qmk_module.json' - if not module_json_path.exists(): - raise FileNotFoundError(f'Module JSON file not found: {module_json_path}') - - module_json = json_load(module_json_path) - - if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False): - validate(module_json, 'qmk.community_module.v1') - - module_jsons.append(module_json) - - return module_jsons + return list(map(load_module_json, modules)) diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index 8e12ea636a..9a4a70f492 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -14,10 +14,6 @@ void keyboard_post_init_hello_world(void) { defer_exec(10000, delayed_hello_world, NULL); } -void housekeeping_task_hello_world(void) { - housekeeping_task_hello_world_kb(); -} - bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) { if (!process_record_hello_world_kb(keycode, record)) { return false; From 37e3ee051cde03213dc75d96f5324c3af0041b04 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 24 Jan 2025 23:06:35 +1100 Subject: [PATCH 15/25] Add onekey. --- .../handwired/onekey/keymaps/community_module/keymap.c | 7 +++++++ .../handwired/onekey/keymaps/community_module/keymap.json | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 keyboards/handwired/onekey/keymaps/community_module/keymap.c create mode 100644 keyboards/handwired/onekey/keymaps/community_module/keymap.json diff --git a/keyboards/handwired/onekey/keymaps/community_module/keymap.c b/keyboards/handwired/onekey/keymaps/community_module/keymap.c new file mode 100644 index 0000000000..e43e3a4d19 --- /dev/null +++ b/keyboards/handwired/onekey/keymaps/community_module/keymap.c @@ -0,0 +1,7 @@ +// Copyright 2025 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later +#include QMK_KEYBOARD_H + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { + LAYOUT_ortho_1x1(CM_HELLO) +}; diff --git a/keyboards/handwired/onekey/keymaps/community_module/keymap.json b/keyboards/handwired/onekey/keymaps/community_module/keymap.json new file mode 100644 index 0000000000..0e2961f55c --- /dev/null +++ b/keyboards/handwired/onekey/keymaps/community_module/keymap.json @@ -0,0 +1,3 @@ +{ + "modules": ["hello_world"] +} From d7e374be12abc7b7e9dd59f34d830b421787911b Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 29 Jan 2025 23:57:43 +1100 Subject: [PATCH 16/25] Allow for modules in subdirectories. --- lib/python/qmk/community_modules.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py index 49cde79c7a..3357dd3b61 100644 --- a/lib/python/qmk/community_modules.py +++ b/lib/python/qmk/community_modules.py @@ -5,6 +5,7 @@ from milc.attrdict import AttrDict from qmk.json_schema import json_load, validate from qmk.util import truthy from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE +from qmk.path import is_under_qmk_firmware, is_under_qmk_userspace COMMUNITY_MODULE_JSON_FILENAME = 'qmk_module.json' @@ -32,19 +33,31 @@ MODULE_API_LIST = [ MODULE_API_VERSION = '20250122' -def find_module_path(module): - """Find a module by name. +def find_available_module_paths(): + """Find all available modules. """ search_dirs = [] if HAS_QMK_USERSPACE: search_dirs.append(QMK_USERSPACE / 'modules') search_dirs.append(QMK_FIRMWARE / 'modules') + modules = [] for search_dir in search_dirs: - module_path = search_dir / module - if module_path.exists() and (module_path / COMMUNITY_MODULE_JSON_FILENAME).exists(): - return module_path + for module_json_path in search_dir.rglob(COMMUNITY_MODULE_JSON_FILENAME): + modules.append(module_json_path.parent) + return modules + +def find_module_path(module): + """Find a module by name. + """ + for module_path in find_available_module_paths(): + try: + p = module_path.resolve(strict=True) + except OSError: + continue + if (is_under_qmk_firmware(p) or is_under_qmk_userspace(p)) and str(p.as_posix())[-len(module):] == module: # allow for a `modules/` relative path to be specified, such as `qmk/hello_world` + return module_path return None From ad901327106ad7ffe0ad3482a555ad09c8c66ac2 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Wed, 5 Feb 2025 21:26:59 +1100 Subject: [PATCH 17/25] Subdirs and relative pathing. --- .../qmk/cli/generate/community_modules.py | 43 +++++++++++-------- lib/python/qmk/cli/generate/rules_mk.py | 7 ++- lib/python/qmk/community_modules.py | 21 ++++++--- modules/hello_world/rules.mk | 1 - 4 files changed, 46 insertions(+), 26 deletions(-) delete mode 100644 modules/hello_world/rules.mk diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index 78e71e5086..e8f3dcbc96 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -1,5 +1,6 @@ import contextlib from argcomplete.completers import FilesCompleter +from pathlib import Path from milc import cli @@ -35,14 +36,18 @@ def _render_keycodes(module_jsons): lines.append('enum {') first = True for module_json in module_jsons: + module_name = Path(module_json['module']).name + lines.append(f' // From module: {module_name}') keycodes = module_json.get('keycodes', []) - for keycode in keycodes: - key = keycode.get('key', None) - if first: - lines.append(f' {key} = QK_COMMUNITY_MODULE,') - first = False - else: - lines.append(f' {key},') + if len(keycodes) > 0: + for keycode in keycodes: + key = keycode.get('key', None) + if first: + lines.append(f' {key} = QK_COMMUNITY_MODULE,') + first = False + else: + lines.append(f' {key},') + lines.append('') lines.append(' LAST_COMMUNITY_MODULE_KEY') lines.append('};') lines.append('_Static_assert((int)LAST_COMMUNITY_MODULE_KEY <= (int)(QK_COMMUNITY_MODULE_MAX+1), "Too many community module keycodes");') @@ -61,11 +66,12 @@ def _render_api_declarations(api, module, user_kb=True): def _render_api_implementations(api, module): + module_name = Path(module).name lines = [] lines.append('') with _render_api_guard(lines, api): # _user - lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}_user({api.args}) {{') + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_user({api.args}) {{') if api.ret_type == 'bool': lines.append(' return true;') else: @@ -74,22 +80,22 @@ def _render_api_implementations(api, module): lines.append('') # _kb - lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}_kb({api.args}) {{') + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}_kb({api.args}) {{') if api.ret_type == 'bool': - lines.append(f' if(!{api.name}_{module}_user({api.call_params})) {{ return false; }}') + lines.append(f' if(!{api.name}_{module_name}_user({api.call_params})) {{ return false; }}') lines.append(' return true;') else: - lines.append(f' {api.name}_{module}_user({api.call_params});') + lines.append(f' {api.name}_{module_name}_user({api.call_params});') lines.append('}') lines.append('') # module (non-suffixed) - lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module}({api.args}) {{') + lines.append(f'__attribute__((weak)) {api.ret_type} {api.name}_{module_name}({api.args}) {{') if api.ret_type == 'bool': - lines.append(f' if(!{api.name}_{module}_kb({api.call_params})) {{ return false; }}') + lines.append(f' if(!{api.name}_{module_name}_kb({api.call_params})) {{ return false; }}') lines.append(' return true;') else: - lines.append(f' {api.name}_{module}_kb({api.call_params});') + lines.append(f' {api.name}_{module_name}_kb({api.call_params});') lines.append('}') return lines @@ -102,10 +108,11 @@ def _render_core_implementation(api, modules): if api.ret_type == 'bool': lines.append(' return true') for module in modules: + module_name = Path(module).name if api.ret_type == 'bool': - lines.append(f' && {api.name}_{module}({api.call_params})') + lines.append(f' && {api.name}_{module_name}({api.call_params})') else: - lines.append(f' {api.name}_{module}({api.call_params});') + lines.append(f' {api.name}_{module_name}({api.call_params});') if api.ret_type == 'bool': lines.append(' ;') lines.append('}') @@ -150,7 +157,7 @@ def generate_community_modules_h(cli): lines.append('') lines.append(f'// From module: {module}') for api in MODULE_API_LIST: - lines.extend(_render_api_declarations(api, module)) + lines.extend(_render_api_declarations(api, Path(module).name)) lines.append('') lines.append('// Core wrapper') @@ -183,7 +190,7 @@ def generate_community_modules_c(cli): for module in modules: for api in MODULE_API_LIST: - lines.extend(_render_api_implementations(api, module)) + lines.extend(_render_api_implementations(api, Path(module).name)) for api in MODULE_API_LIST: lines.extend(_render_core_implementation(api, modules)) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 7436194492..be42043a3a 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -64,10 +64,13 @@ def generate_modules_rules(keyboard, filename): lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLED=TRUE') for module in modules: module_path = find_module_path(module) + if not module_path: + raise FileNotFoundError(f"Module '{module}' not found.") lines.append('') - lines.append(f'COMMUNITY_MODULES += {module}') - lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module.upper()}=TRUE') + lines.append(f'COMMUNITY_MODULES += {module_path.name}') # use module_path here instead of module as it may be a subdirectory + lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module_path.name.upper()}=TRUE') lines.append(f'VPATH += {module_path}') + lines.append(f'SRC += $(wildcard {module_path}/{module_path.name}.c)') lines.append(f'-include {module_path}/rules.mk') module_jsons = load_module_jsons(modules) diff --git a/lib/python/qmk/community_modules.py b/lib/python/qmk/community_modules.py index 3357dd3b61..48e1cf20bf 100644 --- a/lib/python/qmk/community_modules.py +++ b/lib/python/qmk/community_modules.py @@ -1,11 +1,13 @@ import os +from pathlib import Path + from milc.attrdict import AttrDict from qmk.json_schema import json_load, validate from qmk.util import truthy from qmk.constants import QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE -from qmk.path import is_under_qmk_firmware, is_under_qmk_userspace +from qmk.path import under_qmk_firmware, under_qmk_userspace COMMUNITY_MODULE_JSON_FILENAME = 'qmk_module.json' @@ -52,11 +54,17 @@ def find_module_path(module): """Find a module by name. """ for module_path in find_available_module_paths(): - try: - p = module_path.resolve(strict=True) - except OSError: + # Ensure the module directory is under QMK Firmware or QMK Userspace + relative_path = under_qmk_firmware(module_path) + if not relative_path: + relative_path = under_qmk_userspace(module_path) + if not relative_path: continue - if (is_under_qmk_firmware(p) or is_under_qmk_userspace(p)) and str(p.as_posix())[-len(module):] == module: # allow for a `modules/` relative path to be specified, such as `qmk/hello_world` + + lhs = str(relative_path.as_posix())[len('modules/'):] + rhs = str(Path(module).as_posix()) + + if relative_path and lhs == rhs: return module_path return None @@ -73,6 +81,9 @@ def load_module_json(module): if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False): validate(module_json, 'qmk.community_module.v1') + module_json['module'] = module + module_json['module_path'] = module_path + return module_json diff --git a/modules/hello_world/rules.mk b/modules/hello_world/rules.mk deleted file mode 100644 index 0326df8935..0000000000 --- a/modules/hello_world/rules.mk +++ /dev/null @@ -1 +0,0 @@ -SRC += hello_world.c From 368972afa6288339a6819cd4224311ad453ac5ec Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Thu, 6 Feb 2025 10:06:27 +1100 Subject: [PATCH 18/25] Merge issue. --- quantum/quantum.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quantum/quantum.c b/quantum/quantum.c index d0f2f4dc75..adb14d64b6 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -355,6 +355,9 @@ bool process_record_quantum(keyrecord_t *record) { #endif process_record_modules(keycode, record) && // modules must run before kb process_record_kb(keycode, record) && +#if defined(VIA_ENABLE) + process_record_via(keycode, record) && +#endif #if defined(SECURE_ENABLE) process_secure(keycode, record) && #endif From e86d8d248ff2a6b9f04d3d5967ad2e302d18be5c Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Thu, 6 Feb 2025 16:48:26 +1100 Subject: [PATCH 19/25] Preprocessor consistency. --- lib/python/qmk/cli/generate/rules_mk.py | 4 ++-- quantum/quantum.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index be42043a3a..50d7d6b71a 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -61,14 +61,14 @@ def generate_modules_rules(keyboard, filename): modules = get_modules(keyboard, filename) if len(modules) > 0: lines.append('') - lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLED=TRUE') + lines.append('OPT_DEFS += -DCOMMUNITY_MODULES_ENABLE=TRUE') for module in modules: module_path = find_module_path(module) if not module_path: raise FileNotFoundError(f"Module '{module}' not found.") lines.append('') lines.append(f'COMMUNITY_MODULES += {module_path.name}') # use module_path here instead of module as it may be a subdirectory - lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module_path.name.upper()}=TRUE') + lines.append(f'OPT_DEFS += -DCOMMUNITY_MODULE_{module_path.name.upper()}_ENABLE=TRUE') lines.append(f'VPATH += {module_path}') lines.append(f'SRC += $(wildcard {module_path}/{module_path.name}.c)') lines.append(f'-include {module_path}/rules.mk') diff --git a/quantum/quantum.h b/quantum/quantum.h index 856120334e..59a415ead4 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -244,7 +244,7 @@ extern layer_state_t layer_state; # include "layer_lock.h" #endif -#ifdef COMMUNITY_MODULES_ENABLED +#ifdef COMMUNITY_MODULES_ENABLE # include "community_modules.h" #endif From 3a5c075cf780d0b8c5b914ba242177729eefbdb4 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 7 Feb 2025 23:07:57 +1100 Subject: [PATCH 20/25] Add support for module-level `introspection.c` to be automatically injected. --- builddefs/build_keyboard.mk | 7 ++++- .../qmk/cli/generate/community_modules.py | 31 ++++++++++++++++++- quantum/keymap_introspection.c | 7 +++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/builddefs/build_keyboard.mk b/builddefs/build_keyboard.mk index 9ae7b213a8..786d1263bc 100644 --- a/builddefs/build_keyboard.mk +++ b/builddefs/build_keyboard.mk @@ -270,9 +270,14 @@ $(INTERMEDIATE_OUTPUT)/src/community_modules.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES $(eval CMD=$(QMK_BIN) generate-community-modules-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(KEYMAP_JSON)) @$(BUILD_CMD) +$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.inc: $(KEYMAP_JSON) $(DD_CONFIG_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) generate-community-modules-introspection-inc -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.inc $(KEYMAP_JSON)) + @$(BUILD_CMD) + SRC += $(INTERMEDIATE_OUTPUT)/src/community_modules.c -generated-files: $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c +generated-files: $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.inc include $(BUILDDEFS_PATH)/converters.mk diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index e8f3dcbc96..10fe7bf8e3 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -9,7 +9,7 @@ from qmk.info import get_modules from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.commands import dump_lines from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE -from qmk.community_modules import MODULE_API_LIST, MODULE_API_VERSION, load_module_jsons +from qmk.community_modules import MODULE_API_LIST, MODULE_API_VERSION, load_module_jsons, find_module_path @contextlib.contextmanager @@ -196,3 +196,32 @@ def generate_community_modules_c(cli): lines.extend(_render_core_implementation(api, modules)) dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) + + +@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.subcommand('Creates a community_modules_introspection.inc from a keymap.json file.') +def generate_community_modules_introspection_inc(cli): + """Creates a community_modules_introspection.inc from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + for module in modules: + module_path = find_module_path(module) + lines.append(f'#if __has_include("{module_path}/introspection.c")') + lines.append(f'#include "{module_path}/introspection.c"') + lines.append(f'#endif // __has_include("{module_path}/introspection.c")') + lines.append('') + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) diff --git a/quantum/keymap_introspection.c b/quantum/keymap_introspection.c index 236b54ce98..cd95868baa 100644 --- a/quantum/keymap_introspection.c +++ b/quantum/keymap_introspection.c @@ -171,3 +171,10 @@ __attribute__((weak)) const key_override_t* key_override_get(uint16_t key_overri } #endif // defined(KEY_OVERRIDE_ENABLE) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Community modules + +#if defined(COMMUNITY_MODULES_ENABLE) +# include "community_modules_introspection.inc" +#endif // defined(COMMUNITY_MODULES_ENABLE) From 6f4db948e308ff62442e2656705bd163509f37b8 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 7 Feb 2025 23:20:02 +1100 Subject: [PATCH 21/25] Add `introspection.c` example. --- modules/hello_world/hello_world.c | 4 +++- modules/hello_world/introspection.c | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 modules/hello_world/introspection.c diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index 9a4a70f492..0dbe2ce817 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -4,8 +4,10 @@ ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(20250122); +extern int keymap_byte_count(void); // example of use of introspection + uint32_t delayed_hello_world(uint32_t trigger_time, void *cb_arg) { - print("Hello, world! I'm a QMK based keyboard!\n"); + printf("Hello, world! I'm a QMK based keyboard! The keymap array size is %d bytes.\n", keymap_byte_count()); return 0; } diff --git a/modules/hello_world/introspection.c b/modules/hello_world/introspection.c new file mode 100644 index 0000000000..f33371c8f7 --- /dev/null +++ b/modules/hello_world/introspection.c @@ -0,0 +1,6 @@ +// Copyright 2025 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +int keymap_byte_count(void) { + return sizeof(keymaps); +} From dd2ea4aae0cce1631a7fd9fff1419e3c2afe4a11 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 7 Feb 2025 23:30:48 +1100 Subject: [PATCH 22/25] Add `introspection.h` support and example. --- builddefs/build_keyboard.mk | 11 ++++-- .../qmk/cli/generate/community_modules.py | 35 +++++++++++++++++-- modules/hello_world/hello_world.c | 6 ++-- modules/hello_world/introspection.c | 8 +++-- modules/hello_world/introspection.h | 9 +++++ quantum/keymap_introspection.c | 8 +++-- 6 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 modules/hello_world/introspection.h diff --git a/builddefs/build_keyboard.mk b/builddefs/build_keyboard.mk index 786d1263bc..03053ebd7a 100644 --- a/builddefs/build_keyboard.mk +++ b/builddefs/build_keyboard.mk @@ -270,14 +270,19 @@ $(INTERMEDIATE_OUTPUT)/src/community_modules.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES $(eval CMD=$(QMK_BIN) generate-community-modules-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(KEYMAP_JSON)) @$(BUILD_CMD) -$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.inc: $(KEYMAP_JSON) $(DD_CONFIG_FILES) +$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c: $(KEYMAP_JSON) $(DD_CONFIG_FILES) @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) - $(eval CMD=$(QMK_BIN) generate-community-modules-introspection-inc -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.inc $(KEYMAP_JSON)) + $(eval CMD=$(QMK_BIN) generate-community-modules-introspection-c -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(KEYMAP_JSON)) + @$(BUILD_CMD) + +$(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h: $(KEYMAP_JSON) $(DD_CONFIG_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) generate-community-modules-introspection-h -kb $(KEYBOARD) --quiet --output $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h $(KEYMAP_JSON)) @$(BUILD_CMD) SRC += $(INTERMEDIATE_OUTPUT)/src/community_modules.c -generated-files: $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.inc +generated-files: $(INTERMEDIATE_OUTPUT)/src/community_modules.h $(INTERMEDIATE_OUTPUT)/src/community_modules.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.c $(INTERMEDIATE_OUTPUT)/src/community_modules_introspection.h include $(BUILDDEFS_PATH)/converters.mk diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index 10fe7bf8e3..cb8c3b9a77 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -202,9 +202,38 @@ def generate_community_modules_c(cli): @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') @cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') -@cli.subcommand('Creates a community_modules_introspection.inc from a keymap.json file.') -def generate_community_modules_introspection_inc(cli): - """Creates a community_modules_introspection.inc from a keymap.json file +@cli.subcommand('Creates a community_modules_introspection.h from a keymap.json file.') +def generate_community_modules_introspection_h(cli): + """Creates a community_modules_introspection.h from a keymap.json file + """ + if cli.args.output and cli.args.output.name == '-': + cli.args.output = None + + lines = [ + GPL2_HEADER_C_LIKE, + GENERATED_HEADER_C_LIKE, + '', + ] + + modules = get_modules(cli.args.keyboard, cli.args.filename) + if len(modules) > 0: + for module in modules: + module_path = find_module_path(module) + lines.append(f'#if __has_include("{module_path}/introspection.h")') + lines.append(f'#include "{module_path}/introspection.h"') + lines.append(f'#endif // __has_include("{module_path}/introspection.h")') + lines.append('') + + dump_lines(cli.args.output, lines, cli.args.quiet, remove_repeated_newlines=True) + + +@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') +@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate community_modules.c for.') +@cli.argument('filename', nargs='?', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') +@cli.subcommand('Creates a community_modules_introspection.c from a keymap.json file.') +def generate_community_modules_introspection_c(cli): + """Creates a community_modules_introspection.c from a keymap.json file """ if cli.args.output and cli.args.output.name == '-': cli.args.output = None diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index 0dbe2ce817..adcf21593a 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include QMK_KEYBOARD_H +#include "introspection.h" + ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(20250122); -extern int keymap_byte_count(void); // example of use of introspection - uint32_t delayed_hello_world(uint32_t trigger_time, void *cb_arg) { - printf("Hello, world! I'm a QMK based keyboard! The keymap array size is %d bytes.\n", keymap_byte_count()); + printf("Hello, world! I'm a QMK based keyboard! The keymap array size is %d bytes.\n", hello_world_introspection().total_size); return 0; } diff --git a/modules/hello_world/introspection.c b/modules/hello_world/introspection.c index f33371c8f7..2c32a074f5 100644 --- a/modules/hello_world/introspection.c +++ b/modules/hello_world/introspection.c @@ -1,6 +1,10 @@ // Copyright 2025 Nick Brassel (@tzarc) // SPDX-License-Identifier: GPL-2.0-or-later -int keymap_byte_count(void) { - return sizeof(keymaps); +hello_world_introspection_t hello_world_introspection(void) { + hello_world_introspection_t introspection = { + .total_size = sizeof(keymaps), + .layer_count = sizeof(keymaps) / sizeof(keymaps[0]), + }; + return introspection; } diff --git a/modules/hello_world/introspection.h b/modules/hello_world/introspection.h new file mode 100644 index 0000000000..3aba0f2051 --- /dev/null +++ b/modules/hello_world/introspection.h @@ -0,0 +1,9 @@ +// Copyright 2025 Nick Brassel (@tzarc) +// SPDX-License-Identifier: GPL-2.0-or-later + +typedef struct hello_world_introspection_t { + int total_size; + int layer_count; +} hello_world_introspection_t; + +hello_world_introspection_t hello_world_introspection(void); diff --git a/quantum/keymap_introspection.c b/quantum/keymap_introspection.c index cd95868baa..57377e31b4 100644 --- a/quantum/keymap_introspection.c +++ b/quantum/keymap_introspection.c @@ -12,6 +12,10 @@ #include "keymap_introspection.h" #include "util.h" +#if defined(COMMUNITY_MODULES_ENABLE) +# include "community_modules_introspection.h" +#endif // defined(COMMUNITY_MODULES_ENABLE) + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Key mapping @@ -173,8 +177,8 @@ __attribute__((weak)) const key_override_t* key_override_get(uint16_t key_overri #endif // defined(KEY_OVERRIDE_ENABLE) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Community modules +// Community modules (must be last in this file!) #if defined(COMMUNITY_MODULES_ENABLE) -# include "community_modules_introspection.inc" +# include "community_modules_introspection.c" #endif // defined(COMMUNITY_MODULES_ENABLE) From b4981aefe4eeabce629ecc1a913ab62f57154c1d Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 7 Feb 2025 23:33:55 +1100 Subject: [PATCH 23/25] Ordering so module introspection types can be used in `keymap.c`. --- quantum/keymap_introspection.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quantum/keymap_introspection.c b/quantum/keymap_introspection.c index 57377e31b4..23e842353a 100644 --- a/quantum/keymap_introspection.c +++ b/quantum/keymap_introspection.c @@ -1,6 +1,10 @@ // Copyright 2022 Nick Brassel (@tzarc) // SPDX-License-Identifier: GPL-2.0-or-later +#if defined(COMMUNITY_MODULES_ENABLE) +# include "community_modules_introspection.h" +#endif // defined(COMMUNITY_MODULES_ENABLE) + // Pull the actual keymap code so that we can inspect stuff from it #include KEYMAP_C @@ -12,10 +16,6 @@ #include "keymap_introspection.h" #include "util.h" -#if defined(COMMUNITY_MODULES_ENABLE) -# include "community_modules_introspection.h" -#endif // defined(COMMUNITY_MODULES_ENABLE) - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Key mapping From 4758f6c695e2b000c3fdc449fd535372f3aee754 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 7 Feb 2025 23:36:58 +1100 Subject: [PATCH 24/25] Header ordering ftw. --- modules/hello_world/hello_world.c | 2 +- modules/hello_world/introspection.h | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index adcf21593a..c259a25742 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -7,7 +7,7 @@ ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(20250122); uint32_t delayed_hello_world(uint32_t trigger_time, void *cb_arg) { - printf("Hello, world! I'm a QMK based keyboard! The keymap array size is %d bytes.\n", hello_world_introspection().total_size); + printf("Hello, world! I'm a QMK based keyboard! The keymap array size is %d bytes.\n", (int)hello_world_introspection().total_size); return 0; } diff --git a/modules/hello_world/introspection.h b/modules/hello_world/introspection.h index 3aba0f2051..fd3d7f24a0 100644 --- a/modules/hello_world/introspection.h +++ b/modules/hello_world/introspection.h @@ -1,9 +1,10 @@ // Copyright 2025 Nick Brassel (@tzarc) // SPDX-License-Identifier: GPL-2.0-or-later +#include QMK_KEYBOARD_H typedef struct hello_world_introspection_t { - int total_size; - int layer_count; + int16_t total_size; + int16_t layer_count; } hello_world_introspection_t; hello_world_introspection_t hello_world_introspection(void); From 519bd5ac45432e6fcadd333b7d446b99469ebc8e Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 7 Feb 2025 23:51:53 +1100 Subject: [PATCH 25/25] Keycode alias support. --- lib/python/qmk/cli/generate/community_modules.py | 2 ++ lib/python/qmk/json_encoders.py | 5 +++++ modules/hello_world/hello_world.c | 2 +- modules/hello_world/qmk_module.json | 5 ++++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/python/qmk/cli/generate/community_modules.py b/lib/python/qmk/cli/generate/community_modules.py index cb8c3b9a77..80228ab473 100644 --- a/lib/python/qmk/cli/generate/community_modules.py +++ b/lib/python/qmk/cli/generate/community_modules.py @@ -47,6 +47,8 @@ def _render_keycodes(module_jsons): first = False else: lines.append(f' {key},') + for alias in keycode.get('aliases', []): + lines.append(f' {alias} = {key},') lines.append('') lines.append(' LAST_COMMUNITY_MODULE_KEY') lines.append('};') diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index c6822c22b2..e83a381d52 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py @@ -256,5 +256,10 @@ class CommunityModuleJSONEncoder(QMKJSONEncoder): return '03features' if key == 'keycodes': return '04keycodes' + elif self.indentation_level == 3: # keycodes + if key == 'key': + return '00key' + if key == 'aliases': + return '01aliases' return key diff --git a/modules/hello_world/hello_world.c b/modules/hello_world/hello_world.c index c259a25742..07c9c0625e 100644 --- a/modules/hello_world/hello_world.c +++ b/modules/hello_world/hello_world.c @@ -22,7 +22,7 @@ bool process_record_hello_world(uint16_t keycode, keyrecord_t *record) { } switch (keycode) { - case CM_HELLO: + case COMMUNITY_MODULE_HELLO: if (record->event.pressed) { SEND_STRING("Hello there."); break; diff --git a/modules/hello_world/qmk_module.json b/modules/hello_world/qmk_module.json index 1dbb1f6fbf..4f269cb4e9 100644 --- a/modules/hello_world/qmk_module.json +++ b/modules/hello_world/qmk_module.json @@ -5,6 +5,9 @@ "deferred_exec": true }, "keycodes": [ - { "key": "CM_HELLO" } + { + "key": "COMMUNITY_MODULE_HELLO", + "aliases": ["CM_HELO"] + } ] }