From 0157c5b64326ce4eea95fbc012cdf70b2252cc4d Mon Sep 17 00:00:00 2001 From: Norbert Takacs Date: Mon, 10 Aug 2026 17:56:44 +0200 Subject: [PATCH 1/4] FIP macros: expand @use/@parameter macro references in the config parser Add a macro-expansion pre-pass to ConfigParser so a reusable .macro file can describe a FIP instrument once with @{name} placeholders, and an aircraft config binds those to datarefs/lua/constants via @use + [@page]/@parameter blocks. After substitution the macro's pages are ordinary [page]/[layer] sections that flow through the existing FIP handlers unchanged. The IniFileParser lexer now accepts '@' in identifiers so the @use/@parameter/@page tokens parse; macro image assets resolve against /macros via a synthetic macro_base_dir property (kept out of the tokenized image string so a Windows drive-letter colon can't be mistaken for a separator). find_config_file now seeds plugin_path/aircraft_path on the throwaway config so macro-using configs parse during aircraft matching. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Norbert Takacs --- src/core/ConfigParser.cpp | 255 ++++++++++++++++++++++++++++++++++++- src/core/ConfigParser.h | 16 +++ src/core/IniFileParser.cpp | 4 +- src/core/XPanel.cpp | 5 + 4 files changed, 277 insertions(+), 3 deletions(-) diff --git a/src/core/ConfigParser.cpp b/src/core/ConfigParser.cpp index 6cfd290..09554d4 100644 --- a/src/core/ConfigParser.cpp +++ b/src/core/ConfigParser.cpp @@ -91,6 +91,14 @@ int ConfigParser::parse_file(std::string file_name, Configuration& config) IniFile ini_file = ini_parser.get_parsed_ini_file(); + // Expand FIP macros (@use / [@page] / @parameter) into concrete [page]/[layer] + // sections before the normal section processing below runs on them. + if (expand_macros(ini_file, config) != EXIT_SUCCESS) + { + Logger(TLogLevel::logERROR) << "parser: error while expanding macros in config file: " << file_name << std::endl; + return EXIT_FAILURE; + } + int error_count = 0; for (auto& ini_section : ini_file.sections) { @@ -236,7 +244,14 @@ int ConfigParser::process_fip_layer_section(IniFileSection& section, Configurati //fip-images/Adf_Kompass_Ring.bmp,ref_x:0,ref_y:0,base_rot:0 if (m.size() >= 7) { - std::filesystem::path bmp_file_absolute_path = std::filesystem::path(config.aircraft_path); + // Macro-expanded layers carry an explicit base dir (the macro folder) so their + // bmp assets resolve there instead of the aircraft folder. The base dir travels + // as a header property (not through tokenize) so a Windows path's drive-letter + // colon can't be mistaken for a token separator. + std::filesystem::path bmp_base_dir = section.header.properties.count(MACRO_BASE_DIR_PROPERTY) > 0 + ? std::filesystem::path(section.header.properties[MACRO_BASE_DIR_PROPERTY]) + : std::filesystem::path(config.aircraft_path); + std::filesystem::path bmp_file_absolute_path = bmp_base_dir; bmp_file_absolute_path /= std::string(m[0]); int ref_x = 0; @@ -294,6 +309,244 @@ int ConfigParser::process_fip_layer_section(IniFileSection& section, Configurati return EXIT_SUCCESS; } +// Replace every @{name} occurrence in s with its bound value. Returns false and sets +// 'missing' if a placeholder has no binding (or is malformed), leaving s partially expanded. +static bool substitute_in_string(std::string& s, const std::map& bindings, std::string& missing) +{ + size_t pos = 0; + while ((pos = s.find("@{", pos)) != std::string::npos) + { + size_t end = s.find('}', pos + 2); + if (end == std::string::npos) + { + missing = s.substr(pos); + return false; + } + std::string name = s.substr(pos + 2, end - (pos + 2)); + auto it = bindings.find(name); + if (it == bindings.end()) + { + missing = name; + return false; + } + s.replace(pos, end - pos + 1, it->second); + pos += it->second.size(); + } + return true; +} + +// Parse an @parameter value of the form "name:,value:" into its name and value parts. +// The value part is taken verbatim (everything after ",value:") so its own ':'/',' survive. +bool ConfigParser::parse_macro_parameter(const std::string& raw, std::string& out_name, std::string& out_value) +{ + const std::string name_prefix = "name:"; + const std::string value_delim = ",value:"; + + if (raw.rfind(name_prefix, 0) != 0) + return false; + + size_t vpos = raw.find(value_delim); + if (vpos == std::string::npos || vpos < name_prefix.size()) + return false; + + out_name = raw.substr(name_prefix.size(), vpos - name_prefix.size()); + out_value = raw.substr(vpos + value_delim.size()); + + return !out_name.empty() && !out_value.empty(); +} + +int ConfigParser::substitute_placeholders_in_section(IniFileSection& section, + const std::map& bindings, + const std::string& macro_name, const std::string& page_id) +{ + std::string missing; + for (auto& key_value : section.key_value_pairs) + { + if (!substitute_in_string(key_value.second, bindings, missing)) + { + Logger(logERROR) << "parser: macro '" << macro_name << "' page '" << page_id << "': unbound parameter '" << missing << "'" << std::endl; + return EXIT_FAILURE; + } + } + for (auto& prop : section.header.properties) + { + if (!substitute_in_string(prop.second, bindings, missing)) + { + Logger(logERROR) << "parser: macro '" << macro_name << "' page '" << page_id << "': unbound parameter '" << missing << "'" << std::endl; + return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; +} + +// Load /macros/.macro, and for each [page] it defines emit a cloned +// [page] section followed by its [layer] sections with @{...} placeholders substituted from +// the matching [@page] binding block. The concrete sections are appended to 'output'. +int ConfigParser::load_and_expand_macro(const std::string& macro_name, + const std::map>& page_bindings, + Configuration& config, std::vector& output) +{ + std::filesystem::path macro_dir = std::filesystem::path(config.plugin_path) / "macros"; + std::filesystem::path macro_file = macro_dir / (macro_name + ".macro"); + + std::ifstream input_file(macro_file); + if (!input_file.is_open()) + { + Logger(logERROR) << "parser: cannot open macro file: " << macro_file.string() << std::endl; + return EXIT_FAILURE; + } + + IniFileParser macro_parser; + macro_parser.parse(input_file, macro_file.string()); + input_file.close(); + + if (macro_parser.get_number_of_errors() > 0) + { + Logger(logERROR) << "parser: error parsing macro file: " << macro_file.string() << std::endl; + return EXIT_FAILURE; + } + + IniFile macro_ini = macro_parser.get_parsed_ini_file(); + + std::string current_page_id = ""; + bool have_page = false; + const std::map empty_bindings; + const std::map* active_bindings = &empty_bindings; + + for (auto& section : macro_ini.sections) + { + if (section.header.name == TOKEN_SECTION_MACRO_INFO) + { + for (auto& key_value : section.key_value_pairs) + { + if (key_value.first == TOKEN_MACRO_DEVICE && key_value.second != "fip") + { + Logger(logERROR) << "parser: macro '" << macro_name << "' targets device '" << key_value.second << "'. Only 'fip' macros are supported" << std::endl; + return EXIT_FAILURE; + } + } + } + else if (section.header.name == TOKEN_FIP_PAGE) + { + current_page_id = section.header.id; + have_page = true; + auto it = page_bindings.find(current_page_id); + active_bindings = (it != page_bindings.end()) ? &it->second : &empty_bindings; + + IniFileSection page_section = section; + if (substitute_placeholders_in_section(page_section, *active_bindings, macro_name, current_page_id) != EXIT_SUCCESS) + return EXIT_FAILURE; + output.push_back(page_section); + } + else if (section.header.name == TOKEN_FIP_LAYER) + { + if (!have_page) + { + Logger(logERROR) << "parser: macro '" << macro_name << "' has a [layer] before any [page]" << std::endl; + return EXIT_FAILURE; + } + + IniFileSection layer_section = section; + if (substitute_placeholders_in_section(layer_section, *active_bindings, macro_name, current_page_id) != EXIT_SUCCESS) + return EXIT_FAILURE; + + // tell process_fip_layer_section to resolve this layer's bmp against the macro folder + layer_section.header.properties[MACRO_BASE_DIR_PROPERTY] = macro_dir.string(); + output.push_back(layer_section); + } + else if (section.header.name == "") + { + // synthetic root section of the macro file - nothing to emit + continue; + } + else + { + Logger(logERROR) << "parser: unsupported section '" << section.header.name << "' in macro '" << macro_name << "'" << std::endl; + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} + +// Rewrite ini_file.sections in place: expand any '@use' referenced macro inside a [screen] +// section (bound by the following [@page]/@parameter blocks) into concrete [page]/[layer] +// sections. Non-macro sections pass through unchanged. +int ConfigParser::expand_macros(IniFile& ini_file, Configuration& config) +{ + std::vector output; + std::vector& sections = ini_file.sections; + + for (size_t i = 0; i < sections.size(); ) + { + IniFileSection& section = sections[i]; + + if (section.header.name == TOKEN_SECTION_MACRO_PAGE) + { + Logger(logERROR) << "parser: [@page:...] binding block without a preceding [screen] that uses a macro. section at line " << section.header.line << std::endl; + return EXIT_FAILURE; + } + + if (section.header.name == TOKEN_SECTION_FIP_SCREEN) + { + // pull the @use macro names out of the screen section, keep the rest + std::vector macro_names; + std::vector> kept_key_values; + for (auto& key_value : section.key_value_pairs) + { + if (key_value.first == TOKEN_MACRO_USE) + macro_names.push_back(key_value.second); + else + kept_key_values.push_back(key_value); + } + section.key_value_pairs = kept_key_values; + output.push_back(section); + i++; + + if (macro_names.empty()) + continue; + + // gather the following [@page:...] binding blocks: page id -> (param name -> value) + std::map> page_bindings; + while (i < sections.size() && sections[i].header.name == TOKEN_SECTION_MACRO_PAGE) + { + IniFileSection& binding = sections[i]; + std::map params; + for (auto& key_value : binding.key_value_pairs) + { + if (key_value.first != TOKEN_MACRO_PARAMETER) + { + Logger(logERROR) << "parser: unexpected key '" << key_value.first << "' in [@page] binding block at line " << binding.header.line << std::endl; + return EXIT_FAILURE; + } + std::string param_name, param_value; + if (!parse_macro_parameter(key_value.second, param_name, param_value)) + { + Logger(logERROR) << "parser: invalid @parameter syntax at line " << binding.header.line << ": " << key_value.second << std::endl; + return EXIT_FAILURE; + } + params[param_name] = param_value; + } + page_bindings[binding.header.id] = params; + i++; + } + + for (auto& macro_name : macro_names) + { + if (load_and_expand_macro(macro_name, page_bindings, config, output) != EXIT_SUCCESS) + return EXIT_FAILURE; + } + continue; + } + + output.push_back(section); + i++; + } + + sections = output; + return EXIT_SUCCESS; +} + int ConfigParser::handle_on_vid(IniFileSectionHeader section_header, std::string key, std::string value, Configuration& config) { (void)section_header; diff --git a/src/core/ConfigParser.h b/src/core/ConfigParser.h index 937351e..3a0c856 100644 --- a/src/core/ConfigParser.h +++ b/src/core/ConfigParser.h @@ -23,6 +23,15 @@ class ConfigParser int process_fip_layer_section(IniFileSection& section, Configuration& config); + int expand_macros(IniFile& ini_file, Configuration& config); + int load_and_expand_macro(const std::string& macro_name, + const std::map>& page_bindings, + Configuration& config, std::vector& output); + int substitute_placeholders_in_section(IniFileSection& section, + const std::map& bindings, + const std::string& macro_name, const std::string& page_id); + bool parse_macro_parameter(const std::string& raw, std::string& out_name, std::string& out_value); + int handle_on_push_or_release(IniFileSectionHeader section_header, std::string key, std::string value, Configuration& config); int handle_on_lit_or_unlit_or_blink(IniFileSectionHeader section_header, std::string key, std::string value, Configuration& config); int handle_on_dynamic_speed(IniFileSectionHeader section_header, std::string key, std::string value, Configuration& config); @@ -69,6 +78,13 @@ class ConfigParser const std::string TOKEN_FIP_OFFSET_Y = "offset_y"; const std::string TOKEN_FIP_ROTATION = "rotation"; + const std::string TOKEN_MACRO_USE = "@use"; + const std::string TOKEN_SECTION_MACRO_PAGE = "@page"; + const std::string TOKEN_MACRO_PARAMETER = "@parameter"; + const std::string TOKEN_SECTION_MACRO_INFO = "macro_info"; + const std::string TOKEN_MACRO_DEVICE = "device"; + const std::string MACRO_BASE_DIR_PROPERTY = "macro_base_dir"; + const std::string DEVICE_TYPE_SAITEK_MULTI = "saitek_multi"; const std::string DEVICE_TYPE_SAITEK_RADIO = "saitek_radio"; const std::string DEVICE_TYPE_SAITEK_SWITCH = "saitek_switch"; diff --git a/src/core/IniFileParser.cpp b/src/core/IniFileParser.cpp index bbb4177..8e8a7a1 100644 --- a/src/core/IniFileParser.cpp +++ b/src/core/IniFileParser.cpp @@ -137,7 +137,7 @@ IniFileParser::Token IniFileParser::lex_token(std::string& value) case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '_': case '.': + case '_': case '.': case '@': value.push_back(c); return lex_identifier(value); case '"': @@ -248,7 +248,7 @@ IniFileParser::Token IniFileParser::lex_identifier(std::string& value) case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '_': case '-': + case '_': case '-': case '@': value.push_back(c); break; default: diff --git a/src/core/XPanel.cpp b/src/core/XPanel.cpp index 63d1ee8..23255b4 100644 --- a/src/core/XPanel.cpp +++ b/src/core/XPanel.cpp @@ -238,6 +238,11 @@ std::filesystem::path find_config_file(const std::string& aircraft_file_name, co { ConfigParser temp_parser; Configuration temp_config; + // Provide the same paths the real parse gets so configs that reference + // bmp assets or FIP macros parse successfully during aircraft matching + // instead of being skipped. + temp_config.plugin_path = plugin_path.string(); + temp_config.aircraft_path = aircraft_file_path; if (temp_parser.parse_file(entry.path().string(), temp_config) == EXIT_SUCCESS) { if (temp_config.aircraft_acf == aircraft_file_name) From d4de5f774dd26290436452acf12e13c7430251a1 Mon Sep 17 00:00:00 2001 From: Norbert Takacs Date: Mon, 10 Aug 2026 17:57:16 +0200 Subject: [PATCH 2/4] FIP macros: add unit tests and fixtures for macro expansion test_fip_macro covers expanding a macro into pages/layers (a successful parse also proves macro-folder bmp resolution) and the negative case where an unbound @{name} parameter fails the parse. Registered in the CMake/ctest build and the Windows test project. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Norbert Takacs --- test/CMakeLists.txt | 3 + test/macros/fip-images/bmp_test_padding.bmp | Bin 0 -> 102 bytes test/macros/test_instrument.macro | 26 ++++++++ test/test-fip-macro-config.ini | 18 ++++++ test/test-fip-macro-unbound-config.ini | 17 +++++ test/test.vcxproj | 1 + test/test.vcxproj.filters | 3 + test/test_fip_macro.cpp | 68 ++++++++++++++++++++ 8 files changed, 136 insertions(+) create mode 100644 test/macros/fip-images/bmp_test_padding.bmp create mode 100644 test/macros/test_instrument.macro create mode 100644 test/test-fip-macro-config.ini create mode 100644 test/test-fip-macro-unbound-config.ini create mode 100644 test/test_fip_macro.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bc551e0..a5d9833 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -44,6 +44,7 @@ set(XPANEL_TEST_SRCS test_trc1000_audio.cpp test_trc1000_pfd.cpp test_xpanel_plugin.cpp + test_fip_macro.cpp ) add_executable(xpanel_tests ${XPANEL_TEST_SRCS}) @@ -86,6 +87,7 @@ target_link_libraries(xpanel_tests PRIVATE Lua::Lua doctest::doctest) # this directory's fixture files. file(COPY ${CMAKE_SOURCE_DIR}/sample-config/board-config.ini DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) file(COPY ${CMAKE_SOURCE_DIR}/test/fip-images/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/fip-images) +file(COPY ${CMAKE_SOURCE_DIR}/test/macros/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/macros) file(COPY ${CMAKE_SOURCE_DIR}/3rdparty/FIP-SDK/fonts/fip-fonts.bmp DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) set(_test_wd ${CMAKE_CURRENT_BINARY_DIR}) @@ -102,3 +104,4 @@ add_test(NAME test_radio_panel COMMAND xpanel_tests --test-suite=test_radi add_test(NAME test_trc1000_audio COMMAND xpanel_tests --test-suite=test_trc1000_audio_panel WORKING_DIRECTORY ${_test_wd}) add_test(NAME test_trc1000_pfd COMMAND xpanel_tests --test-suite=test_trc1000_pfd_panel WORKING_DIRECTORY ${_test_wd}) add_test(NAME test_xpanel_plugin COMMAND xpanel_tests --test-suite=test_plugin WORKING_DIRECTORY ${_test_wd}) +add_test(NAME test_fip_macro COMMAND xpanel_tests --test-suite=test_fip_macro WORKING_DIRECTORY ${_test_wd}) diff --git a/test/macros/fip-images/bmp_test_padding.bmp b/test/macros/fip-images/bmp_test_padding.bmp new file mode 100644 index 0000000000000000000000000000000000000000..0dc8bd6149e83763032b217e6a1ed67d19242544 GIT binary patch literal 102 qcmZ?rO=ExnGa#h_#H>Kf48)8K5 +; SPDX-License-Identifier: GPL-3.0-or-later +; +; Test macro fixture for the FIP macro expansion unit tests. + +[macro_info] +name="test_instrument" +description="Test instrument macro" +device="fip" +version="1.0" + +[page:id="HSI"] + [layer:image="fip-images/bmp_test_padding.bmp,ref_x:0,ref_y:0,base_rot:0"] + rotation="@{course}" + offset_x="const:200" + offset_y="const:120" + + [layer:type="text"] + offset_x="const:38" + offset_y="const:218" + text="@{baro}" + +[page:id="STATIC"] + [layer:image="fip-images/bmp_test_padding.bmp,ref_x:0,ref_y:0,base_rot:0"] + offset_x="const:0" + offset_y="const:0" diff --git a/test/test-fip-macro-config.ini b/test/test-fip-macro-config.ini new file mode 100644 index 0000000..31a4045 --- /dev/null +++ b/test/test-fip-macro-config.ini @@ -0,0 +1,18 @@ +; Copyright (C) 2026 Norbert Takacs +; SPDX-License-Identifier: GPL-3.0-or-later +; +; FIP macro expansion test config: uses the test_instrument macro and binds its +; HSI page parameters. The STATIC page has no parameters, so it needs no binding block. + +log_level="TRACE" +aircraft_acf="generic.acf" + +[device:id="saitek_fip_screen"] +serial="MZB05779E2" + + [screen:id="fip-screen"] + @use="test_instrument" + + [@page:id="HSI"] + @parameter="name:course,value:dataref:sim/test/course,scale:-1.0" + @parameter="name:baro,value:lua:get_baro()" diff --git a/test/test-fip-macro-unbound-config.ini b/test/test-fip-macro-unbound-config.ini new file mode 100644 index 0000000..1392a2f --- /dev/null +++ b/test/test-fip-macro-unbound-config.ini @@ -0,0 +1,17 @@ +; Copyright (C) 2026 Norbert Takacs +; SPDX-License-Identifier: GPL-3.0-or-later +; +; Negative test config: uses the test_instrument macro but leaves the 'baro' +; parameter of the HSI page unbound, which must make parsing fail. + +log_level="TRACE" +aircraft_acf="generic.acf" + +[device:id="saitek_fip_screen"] +serial="MZB05779E2" + + [screen:id="fip-screen"] + @use="test_instrument" + + [@page:id="HSI"] + @parameter="name:course,value:dataref:sim/test/course,scale:-1.0" diff --git a/test/test.vcxproj b/test/test.vcxproj index da3bd22..77b9144 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -231,6 +231,7 @@ xcopy /y /d "$(SolutionDir)3rdparty\FIP-SDK\fonts\fip-fonts.bmp" "$(OutDir)" + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index ba3ddf7..7186a36 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -36,6 +36,9 @@ Source Files + + Source Files + Source Files diff --git a/test/test_fip_macro.cpp b/test/test_fip_macro.cpp new file mode 100644 index 0000000..85d6200 --- /dev/null +++ b/test/test_fip_macro.cpp @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Norbert Takacs + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include "XPLMDefs.h" +#include "XPLMPlanes.h" +#include "core/ConfigParser.h" +#include "fip/FIPScreen.h" + +#include "CppUnitTest.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +void test_set_aircraft_path_and_filename(char* file_name, char* path); + +namespace test +{ + // The FIP macro feature expands `@use` + `[@page]`/`@parameter` bindings into ordinary + // [page]/[layer] sections. A successful parse already proves the macro file was found and + // its bmp assets resolved against the macro folder (add_layer_to_page fails the parse + // otherwise); the assertions below check the expanded pages/layers landed as expected. + TEST_CLASS(test_fip_macro) + { + public: + TEST_METHOD(TestMacroExpansion) + { + test_set_aircraft_path_and_filename(const_cast("generic.acf"), const_cast("./")); + + Configuration config; + // plugin_path left default "" -> macros resolve under the test working dir + // (build/test), where CMake copies test/macros/. Mirrors how the existing FIP + // test resolves fip-fonts.bmp relative to the working dir. + + ConfigParser parser; + int result = parser.parse_file("../../test/test-fip-macro-config.ini", config); + Assert::AreEqual(0, result); + + Assert::AreEqual(1, (int)config.class_configs.size()); + FIPScreen* screen = config.class_configs[0].fip_screens["saitek_fip_screen"]; + Assert::IsTrue(screen != nullptr); + + // the macro defined two pages, in order: HSI (index 0) and STATIC (index 1) + Assert::AreEqual(1, screen->get_last_page_index()); + Assert::AreEqual(std::string("HSI"), screen->get_page_name(0)); + Assert::AreEqual(std::string("STATIC"), screen->get_page_name(1)); + + // HSI page: one image layer + one text layer -> last index 1 + Assert::AreEqual(1, screen->get_last_layer_index(0)); + // STATIC page: one image layer -> last index 0 + Assert::AreEqual(0, screen->get_last_layer_index(1)); + } + + TEST_METHOD(TestMacroUnboundParameterFails) + { + test_set_aircraft_path_and_filename(const_cast("generic.acf"), const_cast("./")); + + Configuration config; + + ConfigParser parser; + // the 'baro' parameter of the HSI page is left unbound -> parsing must fail + int result = parser.parse_file("../../test/test-fip-macro-unbound-config.ini", config); + Assert::AreEqual(1, result); + } + }; +} From 51e920aeabe932433fd66036a29a2f22f1fc148f Mon Sep 17 00:00:00 2001 From: Norbert Takacs Date: Mon, 10 Aug 2026 17:57:41 +0200 Subject: [PATCH 3/4] FIP macros: ship an example garmin_g5 macro and install the macros folder Add macros/garmin_g5.macro (a Garmin G5 style HSI page) with its bmp assets, and an install rule that deploys the macros/ folder next to the plugin so it resolves at runtime under /macros. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Norbert Takacs --- CMakeLists.txt | 1 + macros/fip-images/HSIb_card_hdg.bmp | Bin 0 -> 96714 bytes macros/fip-images/HSIb_mask_front.bmp | Bin 0 -> 171414 bytes macros/fip-images/HSIb_pointer_crs_tf.bmp | Bin 0 -> 59274 bytes macros/garmin_g5.macro | 53 ++++++++++++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 macros/fip-images/HSIb_card_hdg.bmp create mode 100644 macros/fip-images/HSIb_mask_front.bmp create mode 100644 macros/fip-images/HSIb_pointer_crs_tf.bmp create mode 100644 macros/garmin_g5.macro diff --git a/CMakeLists.txt b/CMakeLists.txt index ab5b5f4..2de345a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,5 +30,6 @@ add_subdirectory(test) install(DIRECTORY sample-config/ DESTINATION sample_configs) install(DIRECTORY doc/ DESTINATION doc) +install(DIRECTORY macros/ DESTINATION ${PLUGIN_INSTALL_DIR}/macros) install(FILES sample-config/board-config.ini DESTINATION ${PLUGIN_INSTALL_DIR}) install(FILES 3rdparty/FIP-SDK/fonts/fip-fonts.bmp DESTINATION ${PLUGIN_INSTALL_DIR}) diff --git a/macros/fip-images/HSIb_card_hdg.bmp b/macros/fip-images/HSIb_card_hdg.bmp new file mode 100644 index 0000000000000000000000000000000000000000..4c6f90c387b7b03c3b0cc83e56158b2b5d4a6d22 GIT binary patch literal 96714 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 F{{^A&mr?)# literal 0 HcmV?d00001 diff --git a/macros/fip-images/HSIb_mask_front.bmp b/macros/fip-images/HSIb_mask_front.bmp new file mode 100644 index 0000000000000000000000000000000000000000..ee81c9b7e9a4110de289999987b15879c32cd0aa GIT binary patch literal 171414 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 literal 0 HcmV?d00001 diff --git a/macros/fip-images/HSIb_pointer_crs_tf.bmp b/macros/fip-images/HSIb_pointer_crs_tf.bmp new file mode 100644 index 0000000000000000000000000000000000000000..95c29a1e3f61f739074a5ff2e3f2429e9b1ff825 GIT binary patch literal 59274 zcmeHQ+mhQf7>?sZva#c2yWMmtQwDm)bcPO-0$X823w>@XHyzS}+9?;pOCb98!o zdLt%D;s}zGFpM1gGB$NZI~C|j;p)qa|8`Gz;OM5U<5I=l`ybwi{Gb@Tp{+3j1uex1 zWRYbeq_r_$ZFi5a_7AsLwK>HM^OF-OF8CtCWONm0{UBJRCn9vMVqR=_Pr=b{->Q_D z`DC$RJq?+$#~2q*Z+c}SR&8Snj?mU!_2r1^-E20f(Z@(h#b-2Y=moSTLX*GX=+YQR zP_#(XD2iZb#p+`TUN`%}VzJNw-XY0sJt_^D&1WFysF-TiRV#sN61X`Z8EWf9jZqYj zz80a^X0~zTV0^yaJu%T|;KWvUWAxRFrf-C;AFe{Ho>~gu)T2u?eFlw@=bH2S=P=R(Bak7(%kDg%f_b zG~vXGjdREL?jRh&siI$gXq|{Sifz*&m5&=i)p1y99!rs%&Am82n4Wo@3_}pW2S@mP zV!vM}CHmsx0}P}1-}KqLK9$14MKX>aSi6Sh)QP+2=Nlm4IZdM3E@K!+AGz%_a1JgO zsm~m7*1F$i96jE?bFaG8m@YEhPVti_oH0u~hYGL`1DG1h0H*isY=d5tZ!{&b$svZ$ z2s4fxUmr=WMgPI8ZeAQkQajy8`oj@?#}FaLo_wfHQRoXnfW<40U{9XsVH6Ha6AW-) zSu`4b;|QjnZ_aT`c|e*lK&e5@eUgV90fgDC7{(oBi(-#jId(jJ{^}L`xIa8J8aMZb zIWajK64Ll?B#9l~Mnvg^oDb0ASsn7$Icqx25l46Ly6=jchH4mj)pLYx3vj#Dp>ntYWZz)H#q}twe5Un$J%}rN(10xAx?u@2qSG_%~4@g43i_&pcVSv^_t0 zr~%fV$cV*7f>?~0(|C;S0Vb@{GH&xA0*B z)77%V!UK!*fg?a>qjv}D4~z_j zxt?%j<7W71I8spOIvO`Tn;b2z@(eq1&NYJe^%=j#ibuZWtK7cfN2H+`MmIV>n=ZRN zE#Zg|@8s$-TefP-^qH4yszjY=&=$&;nI865sd>KAYNNz12xO^Aj#lfnXvFulwM2l! zFw0DGv{Z1!DWzQv0^pNNU8IXvt(ht_hIi3RJUipW7rUZTNhXTAML>jWeRe0F85ls< zT)K)%Z_T0q5(HFm^}lyV97*8r6s6*bJ?KW&jx~uVjye${DWv2m={t*+IFi8KDN4;z z-y3(WINGZhId)3|7@Z4pKz5U(Db?0CB}cs<#6cW!TvvBB42ntiI7-yRMxxT6fBmKE zLNzSfT&8N0kVliMNh*bxfx&GP;bOMU9M&Ts#;)W?O< zmsNAr|6UMrWajWubJVvUsopdMyH!ffxHLK`D!BUhy=OD!lbTdYM~AK%hg5L&pP!F$ zSghhaJG&zqs!}TEq!=I|!iCxMw@-9$(#5XHlV+)iGnCTS3IPs7yyK|`_cqB{YWU?H zoVCZfn=MVwflO0LJvvTc4SH>O?pCgxr;54ywACUYWj4$6HjPubTj$csuJLzKm6=jmLOuCN_)}Myr0|y~J%= zvPOA{BeRs@nM;juZIlfevV?FFWx}x?f5hDw|`lG|tw8J)@a8j*G=#AhN-lnZS zB6<00WwoIz$#U~T-tjSK_9kjchcqw^L#$gLABMI)!r5Gkr=>_gAFS%<`z9Fmj-T%h z-u=9u^_e3}l|>`g&kmlq9BGiWnuO;z*lxhv%J-9_WxjUPmy^=%+f#7;`^r-#9Y$f+K(;-HmkEJTX9rYr4E&768Ls2PQ<%c zjJ@kzHyrhH{(uSx#gW+I0M-|2%aLB1n0i0}n=%zmAC|K%)eE9Ul&Uc$BTWL;g5AQV zygo!Z7-#7QS`%tmp2A(y0ZowB`Yof;!sT!JU%T_ZuaB8e7a7l%ARr(7nDNR-URpGIM=7V2x z$j_4HXi|}aE{YHKABhcU2{}(onz(rEPBB|F3@xp=7u8V=AwUQa0)zk|KnM^5ga9Ex X2oM5<03kpK5CVh%AwUR>6@mW&O_E)5 literal 0 HcmV?d00001 diff --git a/macros/garmin_g5.macro b/macros/garmin_g5.macro new file mode 100644 index 0000000..1445aed --- /dev/null +++ b/macros/garmin_g5.macro @@ -0,0 +1,53 @@ +; Copyright (C) 2026 Norbert Takacs +; SPDX-License-Identifier: GPL-3.0-or-later +; +; Example FIP macro: a Garmin G5 style HSI page. +; +; A macro describes a reusable instrument once. Values written as "@{name}" are +; parameters that the aircraft config binds to a dataref / lua / constant with +; @parameter lines. The bmp assets referenced below live next to this file, under +; /macros/fip-images/. +; +; To use it from an aircraft config: +; +; [device:id="saitek_fip_screen"] +; serial="YOUR_DEVICE_SERIAL" +; [screen:id="fip-screen"] +; @use="garmin_g5" +; +; [@page:id="HSI"] +; @parameter="name:hsi_course,value:dataref:AirbusFBW/HSICourse,scale:-1.0" +; @parameter="name:hsi_course_rotation,value:lua:hsi_course_rotation()" +; @parameter="name:current_baro_hpa,value:lua:get_current_baro_hpa()" + +[macro_info] +name="garmin_g5" +description="Garmin G5 Electronic Flight Instrument created for General Aviation" +device="fip" +version="1.0" + +[page:id="HSI"] + [layer:image="fip-images/HSIb_card_hdg.bmp,ref_x:90,ref_y:90,base_rot:0"] + rotation="@{hsi_course}" + offset_x="const:200" + offset_y="const:120" + + [layer:image="fip-images/HSIb_pointer_crs_tf.bmp,ref_x:70,ref_y:71,base_rot:0"] + rotation="@{hsi_course_rotation}" + offset_x="const:200" + offset_y="const:120" + + [layer:image="fip-images/HSIb_mask_front.bmp,ref_x:120,ref_y:120,base_rot:0"] + rotation="const:0" + offset_x="const:200" + offset_y="const:120" + + [layer:type="text"] + offset_x="const:3" + offset_y="const:218" + text="const:QNH" + + [layer:type="text"] + offset_x="const:38" + offset_y="const:218" + text="@{current_baro_hpa}" From ad01cf1760b162118dfbb8619731e10201bde74b Mon Sep 17 00:00:00 2001 From: Norbert Takacs Date: Mon, 10 Aug 2026 17:57:49 +0200 Subject: [PATCH 4/4] FIP macros: document the macro file format and usage Add a FIP macros section to doc/documentation.md covering the .macro file format, the @use/[@page]/@parameter binding syntax, @{name} placeholders, and macro-folder asset resolution. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Norbert Takacs --- doc/documentation.md | 66 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/doc/documentation.md b/doc/documentation.md index 6ecb4ed..92a568e 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -758,6 +758,72 @@ text="lua:fip_text_test()" ``` +## FIP macros +Defining a FIP page layer by layer (as described above) is flexible but verbose, and it has to be +repeated for every aircraft even when the instrument is the same - only the datarefs differ. Macros +let you describe an instrument (for example a Garmin G5) **once** as a reusable template with named +parameters, and then use it from any aircraft config by binding those parameters to the aircraft's +datarefs, lua functions or constants. + +### Where macros live +Macro files are placed in a `macros` subfolder next to the plugin, i.e. +`.../resources/plugins/xpanel/64/macros/`. Each macro is a single file named `.macro`. Any bmp +assets a macro references are resolved **relative to the `macros` folder** (e.g. +`macros/fip-images/...`), so a macro is fully self-contained and does not depend on files in the +aircraft folder. + +### The macro file format +A `.macro` file uses the same syntax as a normal FIP `[screen]`/`[page]`/`[layer]` definition, with +one addition: any value may be written as a parameter placeholder `"@{name}"`. The optional +`[macro_info]` section carries metadata; `device` must be `fip`. + +```ini +[macro_info] +name="garmin_g5" +description="Garmin G5 Electronic Flight Instrument created for General Aviation" +device="fip" +version="1.0" + +[page:id="HSI"] + [layer:image="fip-images/HSIb_card_hdg.bmp,ref_x:90,ref_y:90,base_rot:0"] + rotation="@{hsi_course}" + offset_x="const:200" + offset_y="const:120" + + [layer:type="text"] + offset_x="const:38" + offset_y="const:218" + text="@{current_baro_hpa}" +``` + +A macro can define several pages. Any value that is not a placeholder (`const:`, `dataref:`, `lua:`) +is used as-is, so a page can mix fixed and parameterized values, or be fully static. + +### Using a macro from an aircraft config +Add `@use=""` inside a `[screen:...]` section to pull in all of the macro's pages. For +each page that has placeholders, add a `[@page:id=""]` block and bind every placeholder with +an `@parameter` line of the form `name:,value:`. The `` is exactly what +you would otherwise write for that field (`dataref:...`, `lua:...` or `const:...`). + +```ini +[device:id="saitek_fip_screen"] +serial="MZB05779E2" + + [screen:id="fip-screen"] + @use="garmin_g5" + + [@page:id="HSI"] + @parameter="name:hsi_course,value:dataref:AirbusFBW/HSICourse,scale:-1.0" + @parameter="name:current_baro_hpa,value:lua:get_current_baro_hpa()" +``` + +Notes: +* Every `@{name}` used on a page must be bound by an `@parameter` in that page's `[@page]` block; + an unbound parameter is a configuration error. +* Pages that contain no placeholders need no `[@page]` block. +* You may `@use` more than one macro in the same screen; page ids must be unique across them. +* A macro shipped with the plugin, `macros/garmin_g5.macro`, is installed as a working example. + ## Generate new fonts for text layers## The plugin has been released with a simple font set (fip-fonts.bmp). If you'd like to generate a new font collection you can use [bmfont](http://www.angelcode.com/products/bmfont/) tool.