diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a1fe20531..a29c35173 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -9855,8 +9855,10 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, return true; } +#if MESH_BATTERY_CHARGE_CONTROL if (mesh::power::handleBatteryChargeCommand(board, command, reply, reply_capacity)) return true; +#endif // Hook for future variant-specific commands not covered by the shared, // runtime-aware FEM handlers above. diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 09c4605d3..e5937e429 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -469,12 +469,14 @@ public: } void handleCommand(const char* command) { +#if MESH_BATTERY_CHARGE_CONTROL char charge_reply[160]; if (mesh::power::handleBatteryChargeCommand(board, command, charge_reply, sizeof(charge_reply))) { Serial.println(charge_reply); return; } +#endif #if defined(NRF52_POWER_MANAGEMENT) char voltage_reply[160]; if (mesh::power::handleVoltagePolicyCommand(command, voltage_reply, diff --git a/src/MeshCore.h b/src/MeshCore.h index ad3b5ebde..a1888083e 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -4,6 +4,7 @@ #include #include #include "helpers/UsbLogging.h" +#include "helpers/BatteryChargeSupport.h" #define MAX_HASH_SIZE 8 #define PUB_KEY_SIZE 32 @@ -239,6 +240,7 @@ public: // True while a board can identify an active battery-charging source. virtual bool isChargerActive() { return false; } +#if MESH_BATTERY_CHARGE_CONTROL // Actual charger voltage, in millivolts. Battery reporting and protection // thresholds are separate capabilities. A successful setter must verify // hardware readback and persist the setting for the next boot. @@ -250,6 +252,7 @@ public: virtual const char* getBatteryChargeTargetUnsupportedReason() const { return "charge.voltage is not supported on this board"; } +#endif // Optional, source-specific power detection. Boards that can distinguish a USB // supply from a solar charger override these; defaults keep every other board diff --git a/src/helpers/BatteryChargeCLI.h b/src/helpers/BatteryChargeCLI.h index 306a4f64d..ff2c0dda2 100644 --- a/src/helpers/BatteryChargeCLI.h +++ b/src/helpers/BatteryChargeCLI.h @@ -4,6 +4,7 @@ #include #include #include +#include "BatteryChargeSupport.h" namespace mesh { namespace power { diff --git a/src/helpers/BatteryChargeSupport.h b/src/helpers/BatteryChargeSupport.h new file mode 100644 index 000000000..1d91631f5 --- /dev/null +++ b/src/helpers/BatteryChargeSupport.h @@ -0,0 +1,10 @@ +#pragma once + +// Keep charger commands and virtual hooks off boards which cannot implement +// them. Small STM32 images have no flash budget for unused charger controls. +#if defined(TBEAM_SUPREME_SX1262) || defined(TBEAM_SX1262) || \ + defined(TBEAM_SX1276) || defined(HELTEC_MESH_SOLAR) +#define MESH_BATTERY_CHARGE_CONTROL 1 +#else +#define MESH_BATTERY_CHARGE_CONTROL 0 +#endif diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index cfa7a6ac0..e4517f976 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -2841,7 +2841,9 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { PrefsSaveReplyGuard save_reply(_prefs_save_failures, reply); mesh::cli::normalizeCommandVerb(command); +#if MESH_BATTERY_CHARGE_CONTROL if (mesh::power::handleBatteryChargeCommand(*_board, command, reply, 160)) return; +#endif if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return; if (handleManagementCommand(command, reply)) return; if (mesh::wireless::control().handle(command, reply, 160, millis(), diff --git a/test/test_battery_charge_cli.py b/test/test_battery_charge_cli.py index 3fe29e5fc..702f10ff7 100644 --- a/test/test_battery_charge_cli.py +++ b/test/test_battery_charge_cli.py @@ -13,6 +13,38 @@ SANITIZER_FLAGS = [] if os.name == "nt" else [ class BatteryChargeCliTest(unittest.TestCase): + def test_charger_api_is_scoped_to_supported_boards(self): + # Unused virtual hooks still consume flash in small Companion images. + # Compile the actual MainBoard interface for every supported macro and + # representative unrelated platforms, not only the capability macro. + source = r''' +#include +#include +template class HasChargeControl { + template static std::true_type probe(decltype(&B::getBatteryChargeTarget)); + template static std::false_type probe(...); +public: + static constexpr bool value = decltype(probe(nullptr))::value; +}; +static_assert(MESH_BATTERY_CHARGE_CONTROL == EXPECT_CHARGE_CONTROL, "capability scope"); +static_assert(HasChargeControl::value == bool(EXPECT_CHARGE_CONTROL), + "charger virtual hooks must not consume unrelated board flash"); +''' + supported = ("TBEAM_SX1262", "TBEAM_SX1276", "TBEAM_SUPREME_SX1262", + "HELTEC_MESH_SOLAR") + unrelated = (None, "STM32", "NRF52", "ESP32", "HELTEC_V4") + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "test.cpp" + path.write_text(source, encoding="ascii") + for macro in supported + unrelated: + with self.subTest(board=macro): + expected = int(macro in supported) + flags = [f"-D{macro}"] if macro else [] + subprocess.run(["c++", "-std=c++11", "-fsyntax-only", + f"-DEXPECT_CHARGE_CONTROL={expected}", *flags, + "-I", str(ROOT / "src"), str(path)], + check=True, capture_output=True) + def test_commands_failures_precision_and_bounded_replies(self): source = r''' #include @@ -121,7 +153,7 @@ int main() { path = Path(temp) (path / "test.cpp").write_text(source, encoding="ascii") binary = path / "test" - subprocess.run(["c++", "-std=c++11", "-Wall", "-Wextra", + subprocess.run(["c++", "-std=c++11", "-Wall", "-Wextra", "-DTBEAM_SX1262", *SANITIZER_FLAGS, "-I", str(ROOT / "src"), str(path / "test.cpp"), "-o", str(binary)], check=True, capture_output=True) @@ -159,6 +191,7 @@ int main() { (path / "Arduino.h").write_text( "#pragma once\n#include \ninline uint16_t meshSolarGetBattVoltage() {return 7400;}\n", encoding="ascii") + (path / "meshSolarApp.h").write_text('#include "Arduino.h"\n', encoding="ascii") (path / "helpers/NRF52Board.h").write_text(r''' #pragma once #include @@ -172,7 +205,7 @@ using NRF52BoardDCDC = NRF52Board; ''', encoding="ascii") (path / "test.cpp").write_text(source, encoding="ascii") binary = path / "test" - subprocess.run(["c++", "-std=c++11", "-I", temp, "-I", str(ROOT / "src"), + subprocess.run(["c++", "-std=c++11", "-DHELTEC_MESH_SOLAR", "-I", temp, "-I", str(ROOT / "src"), "-I", str(ROOT / "variants/heltec_mesh_solar"), str(path / "test.cpp"), "-o", str(binary)], check=True, capture_output=True) diff --git a/test/test_local_cli_access.py b/test/test_local_cli_access.py index e70ea1a84..7eab0cf76 100644 --- a/test/test_local_cli_access.py +++ b/test/test_local_cli_access.py @@ -16,6 +16,7 @@ PREAMBLE = r''' #include #include #include +#define TBEAM_SX1262 1 #include #include #include