π¬π§ English Β· π·πΊ Π ΡΡΡΠΊΠΈΠΉ
Generate C source code for an embedded LCD1602 menu system from a declarative configuration. You describe the menu tree in YAML/JSON β the generator produces a complete set of C files (data tables, navigation, drawing, editing, callbacks) ready to be compiled into your firmware.
- Why this project
- How it works
- Repository layout
- Quick start
- Configuration
- Node reference
- GUI
- Generated files
- Internationalization (i18n / gettext)
- Tests
- Documentation
Writing an LCD menu system in C by hand is repetitive and error-prone: data tables, navigation links, value drawing and editing callbacks, cyclic/limited navigation rules β every device needs a slightly different version of the same boilerplate.
This project treats the menu as data. Instead of writing C, you write a small declarative configuration:
- types β C types (byte, uword, string, β¦)
- roles β how a value behaves (
simple,factor,fixed,callback) - controls β which encoder actions are supported (
click,position) - navigation β cyclic or limited (
cyclic/limit) - callbacks β your own handlers or auto-generated ones
The generator validates the configuration, flattens the tree into a navigable structure, and renders Jinja2 templates into C files that compile into your firmware.
config/config.yaml
β MenuConfig loads all configuration files
β MenuValidator JSON Schema + custom validation of the tree
β MenuFlattener expands the tree into a flat list + navigation links
β FlatNode/BaseFlatNode node built from a composition of managers
β MenuCraft coordinator, aggregates data for the templates
β MenuGenerator renders Jinja2 templates β C files
The pipeline is fully data-driven: the same generator handles any menu β from a single "Start" screen to a multi-level settings tree with dozens of nodes.
MenuCraft/
βββ generate_menu.py # β run this: the only entry point at the root
βββ generate_menu/ # Python package with all sources
β βββ cli.py # command-line interface (argparse)
β βββ i18n.py # gettext helper
β βββ common.py # JSON/YAML loaders and helpers
β βββ menu_config.py # loads & validates all config files
β βββ menu_data.py # type/role/control/navigation rules
β βββ menu_validator.py # schema + custom validation
β βββ menu_flattener.py # tree β flat list, navigation links
β βββ base_flat_node.py # base node (manager composition)
β βββ flat_node.py # final node class
β βββ menucraft.py # coordinator (delegates to the aggregator)
β βββ menu_data_aggregator.py # cached aggregations
β βββ menu_generator.py # Jinja2 rendering β C files
β βββ locale/ # gettext catalogs (messages.pot, ru/...)
β βββ managers/ # per-node managers
βββ config/ # YAML/JSON configuration files
βββ menu/ # the menu tree (menu.yaml / menu.json)
βββ templates/ # Jinja2 templates (*.jinja)
βββ output/ # generated C files (include/ + sources, git-ignored)
βββ docs/ # documentation (see below)
βββ tests/ # unit & smoke tests (pytest)
βββ test/ # integration tests (pytest)
βββ conftest.py # shared pytest fixtures
βββ pytest.ini
βββ requirements.txt
# 1. Install dependencies
pip install -r requirements.txt
# 2. Run the generator from the project root
python generate_menu.pyThe generated C files appear in output/ (sources) and
output/include/ (headers).
π‘ The root keeps the entry point
generate_menu.pyplus the asset directories (config/,menu/,templates/,output/) and the docs; all Python sources live inside thegenerate_menu/package.
The main configuration file is config/config.yaml.
# Paths are relative to this file (the config/ directory)
menu: ../menu/menu.yaml # the menu tree
menu_schema: menu_schema.yaml # JSON Schema for validation
menu_config: menu_data.yaml # type/role/control/navigation rules
output_flattern: output/flatterned.json # flattened JSON dump (debug)
generation_files: files.yaml # which templates produce which filesconfig:
version: "1.0"
default_navigate: limit
default_control: position
default_branch_navigate: cyclic
root_navigate: cyclic
output_directory: ./output/ # where generated C files go
include_files: [pulse_config.h]
menu:
- id: start # a node is defined by id + title
title: Start
type: string # data type (see rules below)
role: fixed # role: simple / factor / fixed / callback
values: [Start, Started] # fixed set of values
default_idx: 0
navigate: cyclic # cyclic or limit
- id: settings
title: Settings
navigate: limit
items: # nested items β sub-menu
- id: hi_delay
title: Delay
type: udword
role: factor
default: 10
min: 10
max: 10000
factors: [1, 10, 100, 1000] # multipliers for the factor roletypes:
ubyte: {c_type: uint8_t}
string: {c_type: "const char*"}
roles:
simple: [byte, ubyte, word, uword, dword, udword]
fixed: [string, byte, ubyte, word, uword, dword, udword]
factor: [byte, ubyte, word, uword, dword, udword]
callback: [callback]
navigation_rules:
position:
allowed_navigate: [limit, cyclic]
default: limittemplates_path: ./templates/ # where Jinja2 templates live
files:
handle.h.jinja: include/menu.h
handle.c.jinja: menu.c
context.h.jinja: include/menu_context.h
context.c.jinja: menu_context.c
# ... every template maps to a generated C fileA node in the menu tree can use the following fields:
| Field | Meaning |
|---|---|
id |
Unique identifier of the node |
title |
Text shown on the display |
type |
Data type: byte, ubyte, word, uword, dword, udword, string, callback |
role |
Behavior: simple, factor, fixed, callback |
min / max / step |
Numeric range for simple / factor |
default |
Default numeric value |
factors |
Multipliers for the factor role |
values / default_idx |
Fixed value set for the fixed role |
controls |
Encoder controls: click, position |
navigate |
Navigation: cyclic, limit |
items |
Child nodes (sub-menu) |
click_cb, position_cb, double_click_cb, long_click_cb, event_cb, draw_value_cb |
Custom callbacks |
If a callback is not specified, it is generated automatically
(e.g. menu_draw_{type}_{role}_value_cb for drawing, {type}_{role}_{control}_{navigate}_cb
for handling).
For editing the menu tree without hand-writing YAML, an optional PyQt6 GUI is available:
python gui.pyIt's a thin wrapper around the same pipeline described above β a tree view plus a
property form for the selected node (which fields appear depends on the node's
type/role, following the same rules from config/menu_data.yaml), an explicit
Validate button, and a Generate C files button that runs the real
MenuGenerator on a background thread. A log panel at the bottom shows everything
the backend already logs, with search and copy. See docs/gui.md for
the full walkthrough, including how it keeps config/config.yaml untouched while
generating.
The generator produces a full C module:
menu_context.h/cβ menu context and core structuresmenu_type.h/cβ data types and enumsmenu_data_*.h/cβ data configuration, context, value and name tablesmenu_tree.h/cβ menu tree with navigation linksmenu_value.h/cβ value access functionsmenu_navigate.h/cβ navigation functionsmenu_edit.h/cβ editing functionsmenu_draw.h/cβ drawing functionsmenu_name.h/cβ node-name lookupmenu.h/cβ top-level entry
All user-facing messages use gettext (Babel); the primary language is English.
Choose the language with the MENU_PROCESSOR_LANG environment variable:
# English (default)
python -X utf8 generate_menu.py
# Russian (uses locale/ru catalog)
set MENU_PROCESSOR_LANG=ru
python -X utf8 generate_menu.pyIf a catalog is missing or the language is unknown, English is used as fallback.
Run the whole suite (unit + integration) from the project root:
python -m pytest -qdocs/tests.mdβ unit & smoke tests (tests/)docs/test.mdβ integration tests (test/)
| Document | Language |
|---|---|
| docs/architect.md | π¬π§ Architecture overview & recommendations |
| docs/architect_ru.md | π·πΊ ΠΠ±Π·ΠΎΡ Π°ΡΡ ΠΈΡΠ΅ΠΊΡΡΡΡ ΠΈ ΡΠ΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°ΡΠΈΠΈ |
| docs/gui.md | π¬π§ GUI (PyQt6): layout, actions, design notes |
| docs/gui_ru.md | π·πΊ GUI (PyQt6): ΡΠ°ΡΠΊΠ»Π°Π΄ΠΊΠ°, Π΄Π΅ΠΉΡΡΠ²ΠΈΡ, ΠΎΡΠΎΠ±Π΅Π½Π½ΠΎΡΡΠΈ ΡΠ΅Π°Π»ΠΈΠ·Π°ΡΠΈΠΈ |
| docs/jinja_templates.md | π¬π§ Generated C code: architecture & integration |
| docs/jinja_templates_ru.md | π·πΊ ΠΠ΅Π½Π΅ΡΠΈΡΡΠ΅ΠΌΡΠΉ C-ΠΊΠΎΠ΄: Π°ΡΡ ΠΈΡΠ΅ΠΊΡΡΡΠ° ΠΈ Π²ΡΡΡΠ°ΠΈΠ²Π°Π½ΠΈΠ΅ |
| docs/tests.md | π¬π§ Unit & smoke tests (tests/) |
| docs/tests_ru.md | π·πΊ ΠΠΎΠ΄ΡΠ»ΡΠ½ΡΠ΅ ΠΈ smoke-ΡΠ΅ΡΡΡ (tests/) |
| docs/test.md | π¬π§ Integration tests (test/) |
| docs/test_ru.md | π·πΊ ΠΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΠΎΠ½Π½ΡΠ΅ ΡΠ΅ΡΡΡ (test/) |
| docs/changes.md | π¬π§ Changelog |
| docs/changes_ru.md | π·πΊ ΠΡΡΠ½Π°Π» ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ |