diff --git a/.gitignore b/.gitignore index cc742c6c1..545b0923a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,11 @@ src/mesh/raspihttp/private_key.pem # Ignore logo (set at build time with platformio-custom.py) data/boot/logo.* + +# Ignore 3rd party plugins +plugins/* +!plugins/README.md +!plugins/sample-plugin + +# Ignore Python vendor directory +pyvendor \ No newline at end of file diff --git a/bin/mpm_pio.py b/bin/mpm_pio.py new file mode 100644 index 000000000..48c088acc --- /dev/null +++ b/bin/mpm_pio.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +Mesh Plugin Manager (MPM) - PlatformIO build integration shim. + +This script is intended to be used only as a PlatformIO extra_script. +It imports the real `mpm` package (vendored via `pyvendor/`) and +exposes the build helpers expected by the firmware build. +""" + +import os +import sys + +try: + # Provided by PlatformIO/SCons when used as `pre:` extra_script + Import("env") # type: ignore[name-defined] # noqa: F821 + IS_PLATFORMIO = True +except Exception: + IS_PLATFORMIO = False + +# Add vendored Python dependencies (installed with `pip install -r requirements.txt -t pyvendor`) +if IS_PLATFORMIO: + # Use PROJECT_DIR from PlatformIO env (__file__ is not available when exec'd by SCons) + project_dir = env["PROJECT_DIR"] # type: ignore[name-defined] # noqa: F821 + _pyvendor = os.path.join(project_dir, "pyvendor") +else: + # Standalone execution: use __file__ if available, otherwise cwd + try: + _here = os.path.dirname(__file__) + _pyvendor = os.path.abspath(os.path.join(_here, "..", "pyvendor")) + except NameError: + _pyvendor = os.path.join(os.getcwd(), "pyvendor") + +if os.path.isdir(_pyvendor) and _pyvendor not in sys.path: + sys.path.insert(0, _pyvendor) + +if IS_PLATFORMIO: + # Use the installed `mpm` package + from mpm.build import init_plugins # type: ignore[import] + from mpm.build_utils import scan_plugins # type: ignore[import] + from mpm.proto import generate_all_protobuf_files # type: ignore[import] + + # Auto-initialize when imported by PlatformIO (not when run as __main__) + if __name__ != "__main__": + try: + init_plugins(env) # type: ignore[name-defined] # noqa: F821 + except NameError: + # If env is missing for some reason, just skip auto-init + pass +else: + # Standalone execution: delegate to the real CLI for convenience + if __name__ == "__main__": + from mpm.cli import main # type: ignore[import] + + main() + diff --git a/platformio.ini b/platformio.ini index d6ff155e4..b92615db3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,7 +14,9 @@ description = Meshtastic [env] test_build_src = true -extra_scripts = bin/platformio-custom.py +extra_scripts = + bin/platformio-custom.py + pre:bin/mpm_pio.py ; note: we add src to our include search path so that lmic_project_config can override ; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile ; of code is a heap corruption bug! @@ -24,6 +26,7 @@ build_flags = -Wno-missing-field-initializers -Wno-format -Isrc -Isrc/mesh -Isrc/mesh/generated -Isrc/gps -Isrc/buzz -Wl,-Map,"${platformio.build_dir}"/output.map + -Isrc/modules -DUSE_THREAD_NAMES -DTINYGPS_OPTION_NO_CUSTOM_FIELDS -DPB_ENABLE_MALLOC=1 diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 000000000..84e8e8fdf --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,92 @@ +# Plugin Development Guide + +This directory houses plugins that extend the Meshtastic firmware. Plugins are automatically discovered and integrated into the build system. + +## Plugin Structure + +The only requirement for a plugin is that it must have a `./src` directory: + +``` +src/plugins/ +└── myplugin/ + └── src/ + ├── MyModule.h + ├── MyModule.cpp + └── mymodule.proto +``` + +- Plugin directory name can be anything +- All source files must be placed in `./src` +- Only files in `./src` are compiled (the root plugin directory and all other subdirectories are excluded from the build) + +## Python Dependencies + +Before building or working with plugins, install the Python tooling into a local vendor directory so PlatformIO can import it: + +```bash +# From the firmware repo root (directory containing platformio.ini) +python -m pip install -r requirements.txt -t pyvendor +``` + +This vendors the Mesh Plugin Manager (MPM) and its dependencies (including `nanopb`) into `pyvendor/`. The build scripts automatically add `pyvendor/` to `sys.path` when PlatformIO runs. + +## Automatic Protobuf Generation + +For convenience, the Meshtastic Plugin Manager (MPM) automatically scans for and generates protobuf files: + +- **Discovery**: MPM recursively scans plugin directories for `.proto` files +- **Options file**: Auto-detects matching `.options` files (e.g., `mymodule.proto` → `mymodule.options`) +- **Generation**: Uses the vendored `nanopb` tooling from `pyvendor/` to generate C++ files +- **Output**: Generated files are placed in the same directory as the `.proto` file +- **Timing**: Runs during PlatformIO pre-build phase (configured in `platformio.ini`) + +**Note**: Once `pyvendor/` is populated as described above, you can also use the Mesh Plugin Manager CLI from a Python environment that has `pyvendor/` on its `PYTHONPATH` to inspect or manage plugins. + +Example protobuf structure: + +``` +src/plugins/myplugin/src/ +├── mymodule.proto # Protobuf definition +├── mymodule.options # Nanopb options (optional) +├── mymodule.pb.h # Generated header +└── mymodule.pb.c # Generated implementation +``` + +## Include Path Setup + +The plugin's `src/` directory is automatically added to the compiler's include path (`CPPPATH`) during build: + +- Headers in `src/` can be included directly: `#include "MyModule.h"` +- No need to specify relative paths from other plugin files +- The build system handles this automatically via `bin/mpm.py` + +## Module Registration + +If your plugin implements a Meshtastic module, you can use the automatic registration system: + +1. Include `ModuleRegistry.h` in your module `.cpp` file +2. Place `MESHTASTIC_REGISTER_MODULE(ModuleClassName)` at the end of your implementation file +3. Your module will be automatically initialized when the firmware starts + +Example: + +```cpp +#include "MyModule.h" +#include "ModuleRegistry.h" + +// ... module implementation ... + +MESHTASTIC_REGISTER_MODULE(MyModule); +``` + +**Note**: Module registration is optional. Plugins that don't implement Meshtastic modules (e.g., utility libraries) don't need this. + +For details on writing Meshtastic modules, see the [Module API documentation](https://meshtastic.org/docs/development/device/module-api/). + +## Example Plugin + +See the `lobbs` plugin for a complete example that demonstrates: + +- Protobuf definitions with options file +- Module implementation with automatic registration +- Proper source file organization diff --git a/plugins/sample-plugin/README.md b/plugins/sample-plugin/README.md new file mode 100644 index 000000000..f60ee9b9c --- /dev/null +++ b/plugins/sample-plugin/README.md @@ -0,0 +1 @@ +Sample plugin \ No newline at end of file diff --git a/plugins/sample-plugin/src/SampleModule.cpp b/plugins/sample-plugin/src/SampleModule.cpp new file mode 100644 index 000000000..647c78bd5 --- /dev/null +++ b/plugins/sample-plugin/src/SampleModule.cpp @@ -0,0 +1,13 @@ +#include "ModuleRegistry.h" +#include "SinglePortModule.h" + +class MySampleModule : public SinglePortModule +{ + public: + MySampleModule() : SinglePortModule("my_sample_module", meshtastic_PortNum_REPLY_APP) { + LOG_INFO("MySampleModule constructor"); + } +}; + + +MESHTASTIC_REGISTER_MODULE(MySampleModule) \ No newline at end of file diff --git a/plugins/sample-plugin/src/SampleModule.h b/plugins/sample-plugin/src/SampleModule.h new file mode 100644 index 000000000..4faef7774 --- /dev/null +++ b/plugins/sample-plugin/src/SampleModule.h @@ -0,0 +1,12 @@ +#ifndef SAMPLE_MODULE_H +#define SAMPLE_MODULE_H + +#include "SinglePortModule.h" + +class MySampleModule : public SinglePortModule +{ + public: + MySampleModule() : SinglePortModule("my_sample_module", meshtastic_PortNum_REPLY_APP); +}; + +#endif \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..e45155ab7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +../mpm + diff --git a/src/modules/ModuleRegistry.cpp b/src/modules/ModuleRegistry.cpp new file mode 100644 index 000000000..5c83dc70f --- /dev/null +++ b/src/modules/ModuleRegistry.cpp @@ -0,0 +1,30 @@ +// src/modules/ModuleRegistry.cpp + +#include "ModuleRegistry.h" +#include "DebugConfiguration.h" + +// Initialize the global vector in static storage. +// This vector will be populated by the constructor-attributed functions. +std::vector g_module_init_functions; + +/** + * @brief Called by a module's constructor-attributed function to add + * its setup routine to the central list. + */ +void register_module_initializer(ModuleInitFunc func) { + // This push_back happens during C++ static initialization, before main(). + g_module_init_functions.push_back(func); +} + +/** + * @brief Initializes all modules that have self-registered. + * Called once by the core Meshtastic firmware setup routine. + */ +void init_dynamic_modules() { + LOG_INFO("Initializing dynamic modules via vector...\n"); + + // Loop through the collected pointers and execute the setup functions + for (ModuleInitFunc func : g_module_init_functions) { + func(); // Executes the module's initialization code (e.g., new MyModule()) + } +} \ No newline at end of file diff --git a/src/modules/ModuleRegistry.h b/src/modules/ModuleRegistry.h new file mode 100644 index 000000000..82f571a5e --- /dev/null +++ b/src/modules/ModuleRegistry.h @@ -0,0 +1,31 @@ +// src/modules/ModuleRegistry.h + +#ifndef MODULE_REGISTRY_H +#define MODULE_REGISTRY_H + +#include // Required for std::vector + +// Define the function pointer type for module initialization +typedef void (*ModuleInitFunc)(void); + +// The central list to hold pointers to the initialization functions. +// This is defined externally in the CPP file. +extern std::vector g_module_init_functions; + +// Function that all modules will call to register themselves +void register_module_initializer(ModuleInitFunc func); + +// Function called by the core firmware setup to initialize all modules +void init_dynamic_modules(); + +/** + * @brief Macro used by module authors to self-register a new Meshtastic Module. + * This creates a lambda that instantiates the module and automatically applies the constructor attribute. + * * @param ModuleClassName The name of the module's C++ class (e.g., MySensorModule). + */ +#define MESHTASTIC_REGISTER_MODULE(ModuleClassName) \ + static void __attribute__((constructor)) register_##ModuleClassName() { \ + register_module_initializer([]() { new ModuleClassName(); }); \ + } + +#endif // MODULE_REGISTRY_H \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index e477574dd..49df725dd 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -9,6 +9,8 @@ #include "input/UpDownInterruptImpl1.h" #include "input/i2cButton.h" #include "modules/SystemCommandsModule.h" +#include "modules/ModuleRegistry.h" + #if HAS_TRACKBALL #include "input/TrackballInterruptImpl1.h" #endif @@ -298,6 +300,9 @@ void setupModules() if (moduleConfig.has_range_test && moduleConfig.range_test.enabled) new RangeTestModule(); #endif + + init_dynamic_modules(); + // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra // acks routingModule = new RoutingModule();