From 78bc3b4b33524b6b7d43d6e1f99989062982358b Mon Sep 17 00:00:00 2001 From: Johnathon Slightham Date: Tue, 31 Mar 2026 23:09:11 -0400 Subject: [PATCH] Cleanup for release --- .gitignore | 4 + CMakeLists.txt | 42 +++ README.md | 70 ++++ build.sh | 3 + conanfile.txt | 13 + include/DeviceDetailsApp.h | 43 +++ include/DeviceListApp.h | 31 ++ include/common/ModuleTypeUtils.h | 0 include/flatbuffers/OTAPacketBuilder.h | 27 ++ include/flatbuffers/SerializedMessage.h | 15 + .../OTAPacket_generated.h | 128 +++++++ .../RobotModule_generated.h | 350 ++++++++++++++++++ include/rpc/RemoteConfig.h | 28 ++ include/rpc/RemoteDebugging.h | 24 ++ include/rpc/RemoteManagement.h | 40 ++ include/tabs/ConfigurationTab.h | 78 ++++ include/tabs/CoredumpTab.h | 32 ++ include/tabs/FirmwareUpdateTab.h | 28 ++ include/tabs/InformationTab.h | 23 ++ include/tabs/LoggingTab.h | 32 ++ include/tabs/TaskManagerTab.h | 30 ++ src/DeviceDetailsApp.cpp | 71 ++++ src/DeviceListApp.cpp | 98 +++++ src/flatbuffers/OTAPacketBuilder.cpp | 24 ++ src/main.cpp | 15 + src/rpc/RemoteConfig.cpp | 53 +++ src/rpc/RemoteDebugging.cpp | 32 ++ src/rpc/RemoteManagement.cpp | 110 ++++++ src/tabs/ConfigurationTab.cpp | 221 +++++++++++ src/tabs/CoredumpTab.cpp | 122 ++++++ src/tabs/FirmwareUpdateTab.cpp | 115 ++++++ src/tabs/InformationTab.cpp | 148 ++++++++ src/tabs/LoggingTab.cpp | 141 +++++++ src/tabs/TaskManagerTab.cpp | 72 ++++ 34 files changed, 2263 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100755 build.sh create mode 100644 conanfile.txt create mode 100644 include/DeviceDetailsApp.h create mode 100644 include/DeviceListApp.h create mode 100644 include/common/ModuleTypeUtils.h create mode 100644 include/flatbuffers/OTAPacketBuilder.h create mode 100644 include/flatbuffers/SerializedMessage.h create mode 100644 include/flatbuffers_generated/OTAPacket_generated.h create mode 100644 include/flatbuffers_generated/RobotModule_generated.h create mode 100644 include/rpc/RemoteConfig.h create mode 100644 include/rpc/RemoteDebugging.h create mode 100644 include/rpc/RemoteManagement.h create mode 100644 include/tabs/ConfigurationTab.h create mode 100644 include/tabs/CoredumpTab.h create mode 100644 include/tabs/FirmwareUpdateTab.h create mode 100644 include/tabs/InformationTab.h create mode 100644 include/tabs/LoggingTab.h create mode 100644 include/tabs/TaskManagerTab.h create mode 100644 src/DeviceDetailsApp.cpp create mode 100644 src/DeviceListApp.cpp create mode 100644 src/flatbuffers/OTAPacketBuilder.cpp create mode 100644 src/main.cpp create mode 100644 src/rpc/RemoteConfig.cpp create mode 100644 src/rpc/RemoteDebugging.cpp create mode 100644 src/rpc/RemoteManagement.cpp create mode 100644 src/tabs/ConfigurationTab.cpp create mode 100644 src/tabs/CoredumpTab.cpp create mode 100644 src/tabs/FirmwareUpdateTab.cpp create mode 100644 src/tabs/InformationTab.cpp create mode 100644 src/tabs/LoggingTab.cpp create mode 100644 src/tabs/TaskManagerTab.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaad033 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/build +*.log +*.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e222c22 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.15) + +project(DevTools) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(WIN32) + add_compile_options(/permissive-) + add_compile_definitions(NOMINMAX) +endif() + +find_package(libcontrol REQUIRED) +find_package(spdlog REQUIRED) +find_package(librpc REQUIRED) +find_package(ftxui REQUIRED) + +add_executable(DevTools + src/main.cpp + src/DeviceListApp.cpp + src/DeviceDetailsApp.cpp + src/tabs/InformationTab.cpp + src/tabs/LoggingTab.cpp + src/tabs/FirmwareUpdateTab.cpp + src/tabs/TaskManagerTab.cpp + src/tabs/ConfigurationTab.cpp + src/tabs/CoredumpTab.cpp + src/flatbuffers/OTAPacketBuilder.cpp + src/rpc/RemoteDebugging.cpp + src/rpc/RemoteManagement.cpp + src/rpc/RemoteConfig.cpp +) + +target_include_directories(DevTools PUBLIC include) + +target_link_libraries(DevTools + PRIVATE + libcontrol::libcontrol + spdlog::spdlog + librpc::librpc + ftxui::ftxui +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3932584 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Developer Tools +A TUI to interface with BotChain devices. +Provides the following features: +- Device discovery +- View device metadata, sensor data, and connections. +- View logs from device. +- Apply an Over The Air (OTA) update. +- Change settings (device ID, type, wifi, etc.) +- View crash dumps from device + +## Platform Support +- MacOS (Apple silicon) +- MacOS (x86) +- Ubuntu (x86) +- Windows (x86) + +## Setup +### MacOS +Install xcode command line tools (if you do not already have them) +``` +xcode-select --install +``` + +Install conan and dependencies +``` +brew install conan cmake ninja clang-format@21 +``` + +Generate a conan profile +``` +conan profile detect --force +``` + +Follow the required artifactory setup. + +### Ubuntu +On newer versions of Ubuntu, the package manager is responsible for managing python packages. We use `pipx` to create a virtual environment. + +Install `pipx` and dependencies +``` +sudo apt install pipx cmake ninja-build +``` + +Install clang-format version 21. + +Install conan +``` +pipx install conan +``` + +Generate a conan profile +``` +conan profile detect --force +``` + +Follow the required artifactory setup. + +### Required Artifactory Setup +These instructions should only be followed after you have completed all setup steps for your platform. + +Add the artifactory +``` +conan remote add artifactory http://jslightham.com:8081/artifactory/api/conan/botchain +``` + +## Running the App +``` +# Macos and Linux users can run, +./build.sh +``` diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..5523fbc --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +conan install . --build=missing --output-folder=build -s build_type=Release +cmake -S . -B "build" -DCMAKE_TOOLCHAIN_FILE="build/Release/generators/conan_toolchain.cmake" -DCMAKE_BUILD_TYPE="Release" +cmake --build "./build" --config "Release" diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 0000000..6191325 --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,13 @@ +[requires] +libcontrol/1.0.4 +flatbuffers/24.12.23 +spdlog/1.16.0 +librpc/1.1.9 +ftxui/6.1.9 + +[generators] +CMakeDeps +CMakeToolchain + +[layout] +cmake_layout diff --git a/include/DeviceDetailsApp.h b/include/DeviceDetailsApp.h new file mode 100644 index 0000000..cf57522 --- /dev/null +++ b/include/DeviceDetailsApp.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +#include "libcontrol.h" +#include "rpc/RemoteDebugging.h" +#include "tabs/InformationTab.h" +#include "tabs/LoggingTab.h" +#include "tabs/FirmwareUpdateTab.h" +#include "tabs/TaskManagerTab.h" +#include "tabs/ConfigurationTab.h" +#include "tabs/CoredumpTab.h" +#include + +using namespace ftxui; + +class DeviceDetailsApp { +public: + DeviceDetailsApp(std::shared_ptr robot_controller, uint8_t device_id); + void run(); + +private: + std::shared_ptr robot_controller_; + std::shared_ptr remote_debugging_; + uint8_t device_id_; + int selected_tab_ = 0; + Component main_component_; + + // Tab instances + std::unique_ptr information_tab_; + std::unique_ptr logging_tab_; + std::unique_ptr firmware_update_tab_; + std::unique_ptr task_manager_tab_; + std::unique_ptr configuration_tab_; + std::unique_ptr coredump_tab_; + + // Tab names + std::vector tab_names_ = {"Information", "Logging", "Firmware Update", "TaskManager", "Configuration", "Core Dump"}; + + void setupUI(); +}; diff --git a/include/DeviceListApp.h b/include/DeviceListApp.h new file mode 100644 index 0000000..fd63466 --- /dev/null +++ b/include/DeviceListApp.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "libcontrol.h" +#include + +using namespace ftxui; + +class DeviceDetailsApp; + +class DeviceListApp { +public: + DeviceListApp(); + void run(); + +private: + std::shared_ptr robot_controller_; + std::vector device_ids_; + std::vector device_list_entries_; + int selected_device_ = 0; + Component main_component_; + + void refreshDevices(); + void setupUI(); + void openDeviceDetails(); +}; diff --git a/include/common/ModuleTypeUtils.h b/include/common/ModuleTypeUtils.h new file mode 100644 index 0000000..e69de29 diff --git a/include/flatbuffers/OTAPacketBuilder.h b/include/flatbuffers/OTAPacketBuilder.h new file mode 100644 index 0000000..03b043e --- /dev/null +++ b/include/flatbuffers/OTAPacketBuilder.h @@ -0,0 +1,27 @@ + + +#ifndef OTAPACKETBUILDER_H +#define OTAPACKETBUILDER_H + +#include + +#include "flatbuffers/SerializedMessage.h" +#include "flatbuffers/flatbuffers.h" +#include "flatbuffers_generated/OTAPacket_generated.h" + +namespace Flatbuffers { + +class OTAPacketBuilder{ + public: + OTAPacketBuilder() : builder_(1024) {} + + SerializedMessage build_ota_packet(uint16_t packet_num, const std::vector &packet); + + static const Messaging::OTAPacket *parse_ota_packet(const uint8_t *buffer); + + private: + flatbuffers::FlatBufferBuilder builder_; +}; +} // namespace Flatbuffers + +#endif // OTAPACKETBUILDER_H diff --git a/include/flatbuffers/SerializedMessage.h b/include/flatbuffers/SerializedMessage.h new file mode 100644 index 0000000..e797262 --- /dev/null +++ b/include/flatbuffers/SerializedMessage.h @@ -0,0 +1,15 @@ +// +// Created by Johnathon Slightham on 2025-07-05. +// + +#ifndef SERIALIZEDMESSAGE_H +#define SERIALIZEDMESSAGE_H + +namespace Flatbuffers { +struct SerializedMessage { + void *data; + size_t size; +}; +} // namespace Flatbuffers + +#endif // SERIALIZEDMESSAGE_H diff --git a/include/flatbuffers_generated/OTAPacket_generated.h b/include/flatbuffers_generated/OTAPacket_generated.h new file mode 100644 index 0000000..fb105a8 --- /dev/null +++ b/include/flatbuffers_generated/OTAPacket_generated.h @@ -0,0 +1,128 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_OTAPACKET_MESSAGING_H_ +#define FLATBUFFERS_GENERATED_OTAPACKET_MESSAGING_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +// static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && +// FLATBUFFERS_VERSION_MINOR == 2 && +// FLATBUFFERS_VERSION_REVISION == 10, +// "Non-compatible flatbuffers version included"); + +namespace Messaging { + +struct OTAPacket; +struct OTAPacketBuilder; + +struct OTAPacket FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OTAPacketBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SEQUENCE_NUMBER = 4, + VT_LENGTH = 6, + VT_PAYLOAD = 8 + }; + uint16_t sequence_number() const { + return GetField(VT_SEQUENCE_NUMBER, 0); + } + uint16_t length() const { + return GetField(VT_LENGTH, 0); + } + const ::flatbuffers::Vector *payload() const { + return GetPointer *>(VT_PAYLOAD); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SEQUENCE_NUMBER, 2) && + VerifyField(verifier, VT_LENGTH, 2) && + VerifyOffset(verifier, VT_PAYLOAD) && + verifier.VerifyVector(payload()) && + verifier.EndTable(); + } +}; + +struct OTAPacketBuilder { + typedef OTAPacket Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_sequence_number(uint16_t sequence_number) { + fbb_.AddElement(OTAPacket::VT_SEQUENCE_NUMBER, sequence_number, 0); + } + void add_length(uint16_t length) { + fbb_.AddElement(OTAPacket::VT_LENGTH, length, 0); + } + void add_payload(::flatbuffers::Offset<::flatbuffers::Vector> payload) { + fbb_.AddOffset(OTAPacket::VT_PAYLOAD, payload); + } + explicit OTAPacketBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOTAPacket( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t sequence_number = 0, + uint16_t length = 0, + ::flatbuffers::Offset<::flatbuffers::Vector> payload = 0) { + OTAPacketBuilder builder_(_fbb); + builder_.add_payload(payload); + builder_.add_length(length); + builder_.add_sequence_number(sequence_number); + return builder_.Finish(); +} + +inline ::flatbuffers::Offset CreateOTAPacketDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t sequence_number = 0, + uint16_t length = 0, + const std::vector *payload = nullptr) { + auto payload__ = payload ? _fbb.CreateVector(*payload) : 0; + return Messaging::CreateOTAPacket( + _fbb, + sequence_number, + length, + payload__); +} + +inline const Messaging::OTAPacket *GetOTAPacket(const void *buf) { + return ::flatbuffers::GetRoot(buf); +} + +inline const Messaging::OTAPacket *GetSizePrefixedOTAPacket(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); +} + +inline bool VerifyOTAPacketBuffer( + ::flatbuffers::Verifier &verifier) { + return verifier.VerifyBuffer(nullptr); +} + +inline bool VerifySizePrefixedOTAPacketBuffer( + ::flatbuffers::Verifier &verifier) { + return verifier.VerifySizePrefixedBuffer(nullptr); +} + +inline void FinishOTAPacketBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.Finish(root); +} + +inline void FinishSizePrefixedOTAPacketBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.FinishSizePrefixed(root); +} + +} // namespace Messaging + +#endif // FLATBUFFERS_GENERATED_OTAPACKET_MESSAGING_H_ diff --git a/include/flatbuffers_generated/RobotModule_generated.h b/include/flatbuffers_generated/RobotModule_generated.h new file mode 100644 index 0000000..58d10a9 --- /dev/null +++ b/include/flatbuffers_generated/RobotModule_generated.h @@ -0,0 +1,350 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_ROBOTMODULE_H_ +#define FLATBUFFERS_GENERATED_ROBOTMODULE_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +// static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && +// FLATBUFFERS_VERSION_MINOR == 2 && +// FLATBUFFERS_VERSION_REVISION == 10, +// "Non-compatible flatbuffers version included"); + +struct MotorState; +struct MotorStateBuilder; + +struct RobotModule; +struct RobotModuleBuilder; + +enum ModuleType : int8_t { + ModuleType_SPLITTER = 0, + ModuleType_SERVO_1 = 1, + ModuleType_DC_MOTOR = 2, + ModuleType_BATTERY = 3, + ModuleType_SERVO_2 = 4, + ModuleType_DISPLAY = 5, + ModuleType_GRIPPER = 6, + ModuleType_SPEAKER = 7, + ModuleType_IMU = 8, + ModuleType_DISTANCE_SENSOR = 9, + ModuleType_SPLITTER_2 = 10, + ModuleType_SPLITTER_3 = 11, + ModuleType_SPLITTER_4 = 12, + ModuleType_SPLITTER_5 = 13, + ModuleType_SPLITTER_6 = 14, + ModuleType_SPLITTER_7 = 15, + ModuleType_SPLITTER_8 = 16, + ModuleType_MIN = ModuleType_SPLITTER, + ModuleType_MAX = ModuleType_SPLITTER_8 +}; + +inline const ModuleType (&EnumValuesModuleType())[17] { + static const ModuleType values[] = { + ModuleType_SPLITTER, + ModuleType_SERVO_1, + ModuleType_DC_MOTOR, + ModuleType_BATTERY, + ModuleType_SERVO_2, + ModuleType_DISPLAY, + ModuleType_GRIPPER, + ModuleType_SPEAKER, + ModuleType_IMU, + ModuleType_DISTANCE_SENSOR, + ModuleType_SPLITTER_2, + ModuleType_SPLITTER_3, + ModuleType_SPLITTER_4, + ModuleType_SPLITTER_5, + ModuleType_SPLITTER_6, + ModuleType_SPLITTER_7, + ModuleType_SPLITTER_8 + }; + return values; +} + +inline const char * const *EnumNamesModuleType() { + static const char * const names[18] = { + "SPLITTER", + "SERVO_1", + "DC_MOTOR", + "BATTERY", + "SERVO_2", + "DISPLAY", + "GRIPPER", + "SPEAKER", + "IMU", + "DISTANCE_SENSOR", + "SPLITTER_2", + "SPLITTER_3", + "SPLITTER_4", + "SPLITTER_5", + "SPLITTER_6", + "SPLITTER_7", + "SPLITTER_8", + nullptr + }; + return names; +} + +inline const char *EnumNameModuleType(ModuleType e) { + if (::flatbuffers::IsOutRange(e, ModuleType_SPLITTER, ModuleType_SPLITTER_8)) return ""; + const size_t index = static_cast(e); + return EnumNamesModuleType()[index]; +} + +enum Orientation : int8_t { + Orientation_Deg0 = 0, + Orientation_Deg90 = 1, + Orientation_Deg180 = 2, + Orientation_Deg270 = 3, + Orientation_MIN = Orientation_Deg0, + Orientation_MAX = Orientation_Deg270 +}; + +inline const Orientation (&EnumValuesOrientation())[4] { + static const Orientation values[] = { + Orientation_Deg0, + Orientation_Deg90, + Orientation_Deg180, + Orientation_Deg270 + }; + return values; +} + +inline const char * const *EnumNamesOrientation() { + static const char * const names[5] = { + "Deg0", + "Deg90", + "Deg180", + "Deg270", + nullptr + }; + return names; +} + +inline const char *EnumNameOrientation(Orientation e) { + if (::flatbuffers::IsOutRange(e, Orientation_Deg0, Orientation_Deg270)) return ""; + const size_t index = static_cast(e); + return EnumNamesOrientation()[index]; +} + +enum ModuleState : uint8_t { + ModuleState_NONE = 0, + ModuleState_MotorState = 1, + ModuleState_MIN = ModuleState_NONE, + ModuleState_MAX = ModuleState_MotorState +}; + +inline const ModuleState (&EnumValuesModuleState())[2] { + static const ModuleState values[] = { + ModuleState_NONE, + ModuleState_MotorState + }; + return values; +} + +inline const char * const *EnumNamesModuleState() { + static const char * const names[3] = { + "NONE", + "MotorState", + nullptr + }; + return names; +} + +inline const char *EnumNameModuleState(ModuleState e) { + if (::flatbuffers::IsOutRange(e, ModuleState_NONE, ModuleState_MotorState)) return ""; + const size_t index = static_cast(e); + return EnumNamesModuleState()[index]; +} + +template struct ModuleStateTraits { + static const ModuleState enum_value = ModuleState_NONE; +}; + +template<> struct ModuleStateTraits { + static const ModuleState enum_value = ModuleState_MotorState; +}; + +bool VerifyModuleState(::flatbuffers::Verifier &verifier, const void *obj, ModuleState type); +bool VerifyModuleStateVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types); + +struct MotorState FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef MotorStateBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_ANGLE = 4 + }; + int32_t angle() const { + return GetField(VT_ANGLE, 0); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_ANGLE, 4) && + verifier.EndTable(); + } +}; + +struct MotorStateBuilder { + typedef MotorState Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_angle(int32_t angle) { + fbb_.AddElement(MotorState::VT_ANGLE, angle, 0); + } + explicit MotorStateBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateMotorState( + ::flatbuffers::FlatBufferBuilder &_fbb, + int32_t angle = 0) { + MotorStateBuilder builder_(_fbb); + builder_.add_angle(angle); + return builder_.Finish(); +} + +struct RobotModule FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef RobotModuleBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_ID = 4, + VT_MODULE_TYPE = 6, + VT_CONFIGURATION_TYPE = 8, + VT_CONFIGURATION = 10 + }; + uint8_t id() const { + return GetField(VT_ID, 0); + } + ModuleType module_type() const { + return static_cast(GetField(VT_MODULE_TYPE, 0)); + } + ModuleState configuration_type() const { + return static_cast(GetField(VT_CONFIGURATION_TYPE, 0)); + } + const void *configuration() const { + return GetPointer(VT_CONFIGURATION); + } + template const T *configuration_as() const; + const MotorState *configuration_as_MotorState() const { + return configuration_type() == ModuleState_MotorState ? static_cast(configuration()) : nullptr; + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_ID, 1) && + VerifyField(verifier, VT_MODULE_TYPE, 1) && + VerifyField(verifier, VT_CONFIGURATION_TYPE, 1) && + VerifyOffset(verifier, VT_CONFIGURATION) && + VerifyModuleState(verifier, configuration(), configuration_type()) && + verifier.EndTable(); + } +}; + +template<> inline const MotorState *RobotModule::configuration_as() const { + return configuration_as_MotorState(); +} + +struct RobotModuleBuilder { + typedef RobotModule Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_id(uint8_t id) { + fbb_.AddElement(RobotModule::VT_ID, id, 0); + } + void add_module_type(ModuleType module_type) { + fbb_.AddElement(RobotModule::VT_MODULE_TYPE, static_cast(module_type), 0); + } + void add_configuration_type(ModuleState configuration_type) { + fbb_.AddElement(RobotModule::VT_CONFIGURATION_TYPE, static_cast(configuration_type), 0); + } + void add_configuration(::flatbuffers::Offset configuration) { + fbb_.AddOffset(RobotModule::VT_CONFIGURATION, configuration); + } + explicit RobotModuleBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateRobotModule( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint8_t id = 0, + ModuleType module_type = ModuleType_SPLITTER, + ModuleState configuration_type = ModuleState_NONE, + ::flatbuffers::Offset configuration = 0) { + RobotModuleBuilder builder_(_fbb); + builder_.add_configuration(configuration); + builder_.add_configuration_type(configuration_type); + builder_.add_module_type(module_type); + builder_.add_id(id); + return builder_.Finish(); +} + +inline bool VerifyModuleState(::flatbuffers::Verifier &verifier, const void *obj, ModuleState type) { + switch (type) { + case ModuleState_NONE: { + return true; + } + case ModuleState_MotorState: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + default: return true; + } +} + +inline bool VerifyModuleStateVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types) { + if (!values || !types) return !values && !types; + if (values->size() != types->size()) return false; + for (::flatbuffers::uoffset_t i = 0; i < values->size(); ++i) { + if (!VerifyModuleState( + verifier, values->Get(i), types->GetEnum(i))) { + return false; + } + } + return true; +} + +inline const RobotModule *GetRobotModule(const void *buf) { + return ::flatbuffers::GetRoot(buf); +} + +inline const RobotModule *GetSizePrefixedRobotModule(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); +} + +inline bool VerifyRobotModuleBuffer( + ::flatbuffers::Verifier &verifier) { + return verifier.VerifyBuffer(nullptr); +} + +inline bool VerifySizePrefixedRobotModuleBuffer( + ::flatbuffers::Verifier &verifier) { + return verifier.VerifySizePrefixedBuffer(nullptr); +} + +inline void FinishRobotModuleBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.Finish(root); +} + +inline void FinishSizePrefixedRobotModuleBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.FinishSizePrefixed(root); +} + +#endif // FLATBUFFERS_GENERATED_ROBOTMODULE_H_ diff --git a/include/rpc/RemoteConfig.h b/include/rpc/RemoteConfig.h new file mode 100644 index 0000000..a7fa1d1 --- /dev/null +++ b/include/rpc/RemoteConfig.h @@ -0,0 +1,28 @@ +// +// Created by Johnathon Slightham on 2026-02-16. +// + +#ifndef NEW_DEV_TOOLS_REMOTECONFIG_H +#define NEW_DEV_TOOLS_REMOTECONFIG_H + +#include +#include + +#include "libcontrol.h" +#include "flatbuffers_generated/RobotModule_generated.h" + +class RemoteConfig { +public: + RemoteConfig(std::shared_ptr controller) : m_robot_controller(controller) {} + bool set_module_id(uint8_t current_module_id, uint16_t new_module_id); + bool set_module_type(uint8_t module_id, ModuleType module_type); + bool set_wifi_ssid(uint8_t module_id, const std::string& ssid); + bool set_wifi_password(uint8_t module_id, const std::string& password); + bool set_communication_method(uint8_t module_id, uint8_t communication_method); // only option is 0 (wireless) + void restart(uint8_t module_id); + +private: + std::shared_ptr m_robot_controller; +}; + +#endif //NEW_DEV_TOOLS_REMOTECONFIG_H diff --git a/include/rpc/RemoteDebugging.h b/include/rpc/RemoteDebugging.h new file mode 100644 index 0000000..d23684f --- /dev/null +++ b/include/rpc/RemoteDebugging.h @@ -0,0 +1,24 @@ +// +// Created by Johnathon Slightham on 2026-02-16. +// + +#ifndef NEW_DEV_TOOLS_REMOTEDEBUGGING_H +#define NEW_DEV_TOOLS_REMOTEDEBUGGING_H + +#include +#include + +#include "libcontrol.h" + +class RemoteDebugging { +public: + RemoteDebugging(std::shared_ptr controller) : m_robot_controller(controller) {} + std::string get_task_manager(uint8_t module_id); + std::string get_logs(uint8_t module_id); + std::string get_coredump(uint8_t module_id); + +private: + std::shared_ptr m_robot_controller; +}; + +#endif //NEW_DEV_TOOLS_REMOTEDEBUGGING_H diff --git a/include/rpc/RemoteManagement.h b/include/rpc/RemoteManagement.h new file mode 100644 index 0000000..cc2c7da --- /dev/null +++ b/include/rpc/RemoteManagement.h @@ -0,0 +1,40 @@ +// +// Created by Johnathon Slightham on 2026-02-16. +// + +#ifndef REMOTEMANAGEMENT_H +#define REMOTEMANAGEMENT_H + +#include +#include +#include + +#include "flatbuffers/OTAPacketBuilder.h" +#include "libcontrol.h" + +class RemoteManagement { + public: + RemoteManagement(uint8_t module_id, const std::string &path, + std::shared_ptr controller) + : m_module_id(module_id), m_file(path, std::ios::binary), + m_builder(std::make_unique()), + m_robot_controller(controller) { + } + bool perform_ota(); + double ota_progress(); // 0 to 1 representing % progress. + void restart(); + + private: + bool start_ota(); + bool write_ota(std::vector &transmit_buffer); + bool ota_end(); + + uint16_t m_sequence_num = 0; + uint16_t m_total_packets = 0; + uint8_t m_module_id; + std::ifstream m_file; + std::unique_ptr m_builder; + std::shared_ptr m_robot_controller; +}; + +#endif // REMOTEMANAGEMENT_H diff --git a/include/tabs/ConfigurationTab.h b/include/tabs/ConfigurationTab.h new file mode 100644 index 0000000..61bacfb --- /dev/null +++ b/include/tabs/ConfigurationTab.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include "libcontrol.h" +#include "rpc/RemoteConfig.h" +#include + +using namespace ftxui; + +class ConfigurationTab { +public: + ConfigurationTab(std::shared_ptr robot_controller, uint8_t device_id); + Component createComponent(); + +private: + std::shared_ptr robot_controller_; + std::shared_ptr remote_config_; + uint8_t device_id_; + + // Input fields + std::string module_id_input_; + int selected_module_type_ = 0; + std::string wifi_ssid_input_; + std::string wifi_password_input_; + int selected_communication_method_ = 0; + + // Status messages + std::string module_id_status_; + std::string module_type_status_; + std::string wifi_ssid_status_; + std::string wifi_password_status_; + std::string communication_method_status_; + std::string restart_status_; + + // Component members + Component module_id_input_component_; + Component module_id_button_; + Component module_type_dropdown_; + Component module_type_button_; + Component wifi_ssid_input_component_; + Component wifi_ssid_button_; + Component wifi_password_input_component_; + Component wifi_password_button_; + Component comm_method_dropdown_; + Component comm_method_button_; + Component restart_button_; + + // Module type options + std::vector module_type_options_ = { + "SPLITTER", + "SERVO_1", + "DC_MOTOR", + "BATTERY", + "SERVO_2", + "DISPLAY", + "GRIPPER", + "SPEAKER", + "IMU", + "DISTANCE_SENSOR", + "SPLITTER_2", + "SPLITTER_3", + "SPLITTER_4" + }; + + std::vector communication_method_options_ = { + "Wireless" + }; + + // Helper methods + void setModuleId(); + void setModuleType(); + void setWiFiSSID(); + void setWiFiPassword(); + void setCommunicationMethod(); + void restartDevice(); + std::string getModuleTypeString(ModuleType type); +}; diff --git a/include/tabs/CoredumpTab.h b/include/tabs/CoredumpTab.h new file mode 100644 index 0000000..dc5d03e --- /dev/null +++ b/include/tabs/CoredumpTab.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include "libcontrol.h" +#include "rpc/RemoteDebugging.h" +#include + +using namespace ftxui; + +class CoredumpTab { +public: + CoredumpTab(std::shared_ptr remote_debugging, uint8_t device_id); + Component createComponent(); + +private: + std::shared_ptr remote_debugging_; + uint8_t device_id_; + + // Caching for coredump data to avoid frequent RPC calls + mutable std::string cached_coredump_; + mutable std::chrono::time_point coredump_last_updated_; + mutable std::atomic coredump_loading_{false}; + mutable bool coredump_initial_load_done_{false}; + static constexpr std::chrono::seconds cache_duration_{10}; // 10 seconds + + std::string getCachedCoredump() const; + std::string sanitizeData(const std::string& raw_data); + std::vector splitLines(const std::string& data); +}; diff --git a/include/tabs/FirmwareUpdateTab.h b/include/tabs/FirmwareUpdateTab.h new file mode 100644 index 0000000..b63ae6c --- /dev/null +++ b/include/tabs/FirmwareUpdateTab.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include "libcontrol.h" +#include "rpc/RemoteManagement.h" +#include + +using namespace ftxui; + +class FirmwareUpdateTab { +public: + FirmwareUpdateTab(std::shared_ptr robot_controller, uint8_t device_id); + Component createComponent(); + +private: + std::shared_ptr robot_controller_; + std::shared_ptr remote_management_; + uint8_t device_id_; + + // Firmware update state + std::string firmware_file_path_; + bool update_in_progress_ = false; + int progress_ = 0; + std::string update_status_message_; + bool update_success_ = false; +}; diff --git a/include/tabs/InformationTab.h b/include/tabs/InformationTab.h new file mode 100644 index 0000000..3512b7d --- /dev/null +++ b/include/tabs/InformationTab.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include "libcontrol.h" +#include + +using namespace ftxui; + +class InformationTab { +public: + InformationTab(std::shared_ptr robot_controller, uint8_t device_id); + Component createComponent(); + +private: + std::shared_ptr robot_controller_; + uint8_t device_id_; + + std::string getModuleTypeString(ModuleType type); + std::string getConnectionTypeString(Messaging::ConnectionType type); + std::string formatTimeSince(const std::chrono::time_point& time_point); +}; diff --git a/include/tabs/LoggingTab.h b/include/tabs/LoggingTab.h new file mode 100644 index 0000000..383b426 --- /dev/null +++ b/include/tabs/LoggingTab.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include "libcontrol.h" +#include "rpc/RemoteDebugging.h" +#include + +using namespace ftxui; + +class LoggingTab { +public: + LoggingTab(std::shared_ptr remote_debugging, uint8_t device_id); + Component createComponent(); + +private: + std::shared_ptr remote_debugging_; + uint8_t device_id_; + + // Caching for RemoteDebugging data to avoid frequent RPC calls + mutable std::string cached_logs_; + mutable std::chrono::time_point logs_last_updated_; + mutable std::atomic logs_loading_{false}; + mutable bool logs_initial_load_done_{false}; + static constexpr std::chrono::seconds cache_duration_{4}; // 4 seconds + + std::string getCachedLogs() const; + std::string sanitizeLogData(const std::string& raw_data); + std::vector splitLogLines(const std::string& log_data); +}; diff --git a/include/tabs/TaskManagerTab.h b/include/tabs/TaskManagerTab.h new file mode 100644 index 0000000..688040b --- /dev/null +++ b/include/tabs/TaskManagerTab.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include "libcontrol.h" +#include "rpc/RemoteDebugging.h" +#include + +using namespace ftxui; + +class TaskManagerTab { +public: + TaskManagerTab(std::shared_ptr remote_debugging, uint8_t device_id); + Component createComponent(); + +private: + std::shared_ptr remote_debugging_; + uint8_t device_id_; + + // Caching for RemoteDebugging data to avoid frequent RPC calls + mutable std::string cached_task_data_; + mutable std::chrono::time_point task_data_last_updated_; + mutable std::atomic task_data_loading_{false}; + mutable bool task_data_initial_load_done_{false}; + static constexpr std::chrono::seconds cache_duration_{4}; // 4 seconds + + std::string getCachedTaskData() const; +}; diff --git a/src/DeviceDetailsApp.cpp b/src/DeviceDetailsApp.cpp new file mode 100644 index 0000000..a6d1958 --- /dev/null +++ b/src/DeviceDetailsApp.cpp @@ -0,0 +1,71 @@ +#include "DeviceDetailsApp.h" +#include +#include +#include + +DeviceDetailsApp::DeviceDetailsApp(std::shared_ptr robot_controller, uint8_t device_id) + : robot_controller_(robot_controller), + remote_debugging_(std::make_shared(robot_controller)), + device_id_(device_id) { + // Create tab instances + information_tab_ = std::make_unique(robot_controller_, device_id_); + logging_tab_ = std::make_unique(remote_debugging_, device_id_); + firmware_update_tab_ = std::make_unique(robot_controller_, device_id_); + task_manager_tab_ = std::make_unique(remote_debugging_, device_id_); + configuration_tab_ = std::make_unique(robot_controller_, device_id_); + coredump_tab_ = std::make_unique(remote_debugging_, device_id_); + + setupUI(); +} + +void DeviceDetailsApp::run() { + auto screen = ScreenInteractive::Fullscreen(); + screen.Loop(main_component_); +} + +void DeviceDetailsApp::setupUI() { + // Create tab toggle + auto tab_toggle = Toggle(&tab_names_, &selected_tab_); + + // Create back button + auto back_button = Button("← Back to Device List", [&] { + // Exit the details view and return to device list + auto screen = ScreenInteractive::Active(); + if (screen) { + screen->Exit(); + } + }); + + // Create tab container that shows different content based on selected tab + auto tab_container = Container::Tab({ + information_tab_->createComponent(), + logging_tab_->createComponent(), + firmware_update_tab_->createComponent(), + task_manager_tab_->createComponent(), + configuration_tab_->createComponent(), + coredump_tab_->createComponent() + }, &selected_tab_); + + // Main layout + main_component_ = Container::Vertical({ + Container::Horizontal({back_button}), + tab_toggle, + tab_container + }); + + // Renderer + main_component_ = Renderer(main_component_, [=, this] { + return vbox({ + hbox({ + back_button->Render(), + filler(), + text(std::format("Device ID: {}", static_cast(device_id_))) | bold + }), + separator(), + tab_toggle->Render() | hcenter, + separator(), + tab_container->Render() | flex + }) | border; + }); +} + diff --git a/src/DeviceListApp.cpp b/src/DeviceListApp.cpp new file mode 100644 index 0000000..2808517 --- /dev/null +++ b/src/DeviceListApp.cpp @@ -0,0 +1,98 @@ +#include "DeviceListApp.h" +#include "DeviceDetailsApp.h" +#include +#include +#include + +DeviceListApp::DeviceListApp() : robot_controller_(std::make_shared()) { + refreshDevices(); + setupUI(); +} + +void DeviceListApp::run() { + auto screen = ScreenInteractive::Fullscreen(); + screen.Loop(main_component_); +} + +void DeviceListApp::refreshDevices() { + // Find connected modules (returns std::unordered_set) + auto module_list = robot_controller_->getModuleList(); + + // Convert unordered_set to vector + device_ids_.clear(); + for (const auto module : module_list) { + device_ids_.emplace_back(module.id); + } + + // Convert to display strings + device_list_entries_.clear(); + for (const auto& device_id : device_ids_) { + device_list_entries_.push_back(std::format("Device ID: {}", static_cast(device_id))); + } + + // If no devices found, show a message + if (device_list_entries_.empty()) { + device_list_entries_.push_back("No devices found"); + } + + // Reset selection if out of bounds + if (selected_device_ >= static_cast(device_list_entries_.size())) { + selected_device_ = 0; + } +} + +void DeviceListApp::setupUI() { + // Create the device list component with Enter key handling + auto device_list = Menu(&device_list_entries_, &selected_device_); + + // Override the default behavior to handle Enter key + device_list = CatchEvent(device_list, [this](Event event) { + if (event == Event::Return) { + openDeviceDetails(); + return true; + } + return false; + }); + + // Create refresh button + auto refresh_button = Button("Refresh", [this] { + refreshDevices(); + }); + + // Layout components + main_component_ = Container::Vertical({ + device_list, + refresh_button + }); + + // Add renderer to show the UI + main_component_ = Renderer(main_component_, [=, this] { + return vbox({ + text("DevTools") | bold | hcenter, + separator(), + text(std::format("Found {} devices", device_ids_.size())) | hcenter, + separator(), + device_list->Render() | vscroll_indicator | frame | size(HEIGHT, LESS_THAN, 15), + separator(), + refresh_button->Render() | hcenter, + separator(), + text("Use ↑/↓ to navigate, Enter to view details, Ctrl+C to quit") | dim | hcenter + }) | border; + }); +} + +void DeviceListApp::openDeviceDetails() { + if (device_ids_.empty() || selected_device_ >= static_cast(device_ids_.size())) { + return; + } + + // Get the selected device ID + uint8_t selected_device_id = device_ids_[selected_device_]; + + // Create and run the device details app + DeviceDetailsApp details_app(robot_controller_, selected_device_id); + details_app.run(); + + // When we return from device details, refresh the device list + refreshDevices(); +} diff --git a/src/flatbuffers/OTAPacketBuilder.cpp b/src/flatbuffers/OTAPacketBuilder.cpp new file mode 100644 index 0000000..23c4d7c --- /dev/null +++ b/src/flatbuffers/OTAPacketBuilder.cpp @@ -0,0 +1,24 @@ + +#include "flatbuffers/OTAPacketBuilder.h" +#include "flatbuffers/SerializedMessage.h" + +namespace Flatbuffers { + + SerializedMessage OTAPacketBuilder::build_ota_packet(uint16_t packet_num, const std::vector &packet) { + builder_.Clear(); + + const auto packet_vector = builder_.CreateVector(packet); + + const auto message = Messaging::CreateOTAPacket( + builder_, packet_num, static_cast(packet.size()), packet_vector); + + builder_.Finish(message); + + return {builder_.GetBufferPointer(), builder_.GetSize()}; +} + +const Messaging::OTAPacket *OTAPacketBuilder::parse_ota_packet(const uint8_t *buffer) { + return flatbuffers::GetRoot(buffer); +} + +} // namespace Flatbuffers diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..2382748 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,15 @@ + +#include +#include "DeviceListApp.h" + +int main() { + try { + DeviceListApp app; + app.run(); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/src/rpc/RemoteConfig.cpp b/src/rpc/RemoteConfig.cpp new file mode 100644 index 0000000..5050f71 --- /dev/null +++ b/src/rpc/RemoteConfig.cpp @@ -0,0 +1,53 @@ + +#include "rpc/RemoteConfig.h" + +bool RemoteConfig::set_module_id(uint8_t current_module_id, uint16_t new_module_id) { + std::vector vec; + vec.resize(2); + std::memcpy(vec.data(), &new_module_id, 2); + m_robot_controller->remote_call(8, current_module_id, vec); + return false; +} + +bool RemoteConfig::set_module_type(uint8_t module_id, ModuleType module_type) { + std::vector vec{(uint8_t)module_type}; + const auto maybe = m_robot_controller->remote_call(9, module_id, vec); + if (maybe) { + return (*maybe)->at(0) > 0; + } + return false; +} + +bool RemoteConfig::set_wifi_ssid(uint8_t module_id, const std::string& ssid) { + std::vector vec(ssid.begin(), ssid.end()); + vec.push_back('\0'); + const auto maybe = m_robot_controller->remote_call(10, module_id, vec); + if (maybe) { + return (*maybe)->at(0) > 0; + } + return false; +} + +bool RemoteConfig::set_wifi_password(uint8_t module_id, const std::string& password) { + std::vector vec(password.begin(), password.end()); + vec.push_back('\0'); + const auto maybe = m_robot_controller->remote_call(11, module_id, vec); + if (maybe) { + return (*maybe)->at(0) > 0; + } + return false; +} + +bool RemoteConfig::set_communication_method(uint8_t module_id, uint8_t communication_method) { + std::vector vec{communication_method}; + const auto maybe = m_robot_controller->remote_call(12, module_id, vec); + if (maybe) { + return (*maybe)->at(0) > 0; + } + return false; +} + +void RemoteConfig::restart(uint8_t module_id) { + // Will never return since the module will shutdown + m_robot_controller->remote_call(7, module_id, {}); +} diff --git a/src/rpc/RemoteDebugging.cpp b/src/rpc/RemoteDebugging.cpp new file mode 100644 index 0000000..62d581b --- /dev/null +++ b/src/rpc/RemoteDebugging.cpp @@ -0,0 +1,32 @@ +// +// Created by Johnathon Slightham on 2026-02-16. +// + +#include "rpc/RemoteDebugging.h" + +std::string RemoteDebugging::get_task_manager(uint8_t module_id) { + const auto maybe = m_robot_controller->remote_call(2, module_id, {}); + if (maybe) { + std::string str((*maybe)->begin(), (*maybe)->end()); + return str; + } + return ""; +} + +std::string RemoteDebugging::get_logs(uint8_t module_id) { + const auto maybe = m_robot_controller->remote_call(3, module_id, {}); + if (maybe) { + std::string str((*maybe)->begin(), (*maybe)->end()); + return str; + } + return ""; +} + +std::string RemoteDebugging::get_coredump(uint8_t module_id) { + const auto maybe = m_robot_controller->remote_call(13, module_id, {}); + if (maybe) { + std::string str((*maybe)->begin(), (*maybe)->end()); + return str; + } + return ""; +} diff --git a/src/rpc/RemoteManagement.cpp b/src/rpc/RemoteManagement.cpp new file mode 100644 index 0000000..9ab0382 --- /dev/null +++ b/src/rpc/RemoteManagement.cpp @@ -0,0 +1,110 @@ +// +// Created by Johnathon Slightham on 2026-02-16. +// + +#include +#include + +#include "rpc/RemoteManagement.h" + +#define MAX_PACKET_TX_ATTEMPTS 5 +#define OTA_CHUNK_SIZE 1024 + +bool RemoteManagement::perform_ota() { + if (!m_file) { + return false; + } + + if (!start_ota()) { + // std::cout << "Fail to start OTA update" << std::endl; + return false; + } + + m_file.seekg(0, std::ios::end); + std::streamsize total_size = m_file.tellg(); + m_file.seekg(0, std::ios::beg); + m_total_packets = total_size/OTA_CHUNK_SIZE; + // std::cout << "Total number of chunks: " << total_size/OTA_CHUNK_SIZE << std::endl; + + while (m_file) { + // std::cout << "Top of transmit " << m_sequence_num << std::endl; + + std::vector buffer(OTA_CHUNK_SIZE); + m_file.read(reinterpret_cast(buffer.data()), buffer.size()); + std::streamsize bytes_read = m_file.gcount(); + + if (bytes_read <= 0) { + break; + } + + if (m_sequence_num == 1 && buffer[0] != 0xE9) { + // std::cout << "First byte of firmware must be 0xE9" << std::endl; + return false; + } + + // buffer.resize(bytes_read); + if (!write_ota(buffer)) { + // std::cout << "Failed to write" << std::endl; + return false; + } + } + + ota_end(); + restart(); + + return true; +} + +void RemoteManagement::restart() { + // Will never return since the module will shutdown + m_robot_controller->remote_call(7, m_module_id, {}); +} + +bool RemoteManagement::start_ota() { + // std::cout << "Starting OTA" << std::endl; + const auto maybe = m_robot_controller->remote_call(4, m_module_id, {}); + if (maybe) { + // std::cout << "Got valid response" << std::endl; + m_sequence_num = 1; + return (*maybe)->at(0) > 0; + } + return false; +} + +bool RemoteManagement::write_ota(std::vector &transmit_buffer) { + // std::cout << "Write OTA " << std::endl; + const auto [ota_packet, packet_size] = + m_builder->build_ota_packet(m_sequence_num, transmit_buffer); + std::vector vec((uint8_t *)ota_packet, (uint8_t *)ota_packet + packet_size); + int attempts = 0; + while (attempts < MAX_PACKET_TX_ATTEMPTS) { + const auto maybe = m_robot_controller->remote_call(5, m_module_id, vec); + if (maybe && (*maybe)->at(0) > 0) { + // std::cout << "Success writing OTA" << std::endl; + m_sequence_num++; + return true; + } + attempts++; + } + return false; +} + +bool RemoteManagement::ota_end() { + const auto maybe = m_robot_controller->remote_call(6, m_module_id, {}); + if (maybe) { + m_sequence_num = 0; + return (*maybe)->at(0) > 0; + } + return false; +} + +double RemoteManagement::ota_progress() { + if (m_total_packets < 1) { + return 0.0; + } + if (m_sequence_num >= m_total_packets) { + return 1.0; + } + + return static_cast(m_sequence_num) / static_cast(m_total_packets); +} diff --git a/src/tabs/ConfigurationTab.cpp b/src/tabs/ConfigurationTab.cpp new file mode 100644 index 0000000..325af7f --- /dev/null +++ b/src/tabs/ConfigurationTab.cpp @@ -0,0 +1,221 @@ +#include "tabs/ConfigurationTab.h" +#include +#include +#include + +ConfigurationTab::ConfigurationTab(std::shared_ptr robot_controller, uint8_t device_id) + : robot_controller_(robot_controller), + remote_config_(std::make_shared(robot_controller)), + device_id_(device_id) { + // Initialize module ID input with current device ID + module_id_input_ = std::to_string(static_cast(device_id_)); +} + +Component ConfigurationTab::createComponent() { + // Module ID input and button + module_id_input_component_ = Input(&module_id_input_, "Enter new module ID (0-65535)"); + module_id_button_ = Button("Set Module ID", [this] { setModuleId(); }); + + // Module Type dropdown and button + module_type_dropdown_ = Dropdown(&module_type_options_, &selected_module_type_); + module_type_button_ = Button("Set Module Type", [this] { setModuleType(); }); + + // WiFi SSID input and button + wifi_ssid_input_component_ = Input(&wifi_ssid_input_, "Enter WiFi SSID"); + wifi_ssid_button_ = Button("Set WiFi SSID", [this] { setWiFiSSID(); }); + + // WiFi Password input and button + InputOption password_option; + password_option.password = true; + wifi_password_input_component_ = Input(&wifi_password_input_, "Enter WiFi Password", password_option); + wifi_password_button_ = Button("Set WiFi Password", [this] { setWiFiPassword(); }); + + // Communication Method dropdown and button + comm_method_dropdown_ = Dropdown(&communication_method_options_, &selected_communication_method_); + comm_method_button_ = Button("Set Communication Method", [this] { setCommunicationMethod(); }); + + // Restart button + restart_button_ = Button("Restart Device", [this] { restartDevice(); }); + + auto container = Container::Vertical({ + Container::Horizontal({module_id_input_component_, module_id_button_}), + Container::Horizontal({module_type_dropdown_, module_type_button_}), + Container::Horizontal({wifi_ssid_input_component_, wifi_ssid_button_}), + Container::Horizontal({wifi_password_input_component_, wifi_password_button_}), + Container::Horizontal({comm_method_dropdown_, comm_method_button_}), + restart_button_ + }); + + return Renderer(container, [this] { + Elements content = { + text("Device Configuration") | bold | hcenter, + separator(), + text("Module ID") | bold, + hbox({ + module_id_input_component_->Render() | flex, + separator(), + module_id_button_->Render() + }), + }; + + if (!module_id_status_.empty()) { + bool success = module_id_status_.find("success") != std::string::npos; + content.push_back(text(module_id_status_) | color(success ? Color::Green : Color::Red) | dim); + } + + content.push_back(separator()); + content.push_back(text("Module Type") | bold); + content.push_back(hbox({ + module_type_dropdown_->Render() | flex, + separator(), + module_type_button_->Render() + })); + + if (!module_type_status_.empty()) { + bool success = module_type_status_.find("success") != std::string::npos; + content.push_back(text(module_type_status_) | color(success ? Color::Green : Color::Red) | dim); + } + + content.push_back(separator()); + content.push_back(text("WiFi SSID") | bold); + content.push_back(hbox({ + wifi_ssid_input_component_->Render() | flex, + separator(), + wifi_ssid_button_->Render() + })); + + if (!wifi_ssid_status_.empty()) { + bool success = wifi_ssid_status_.find("success") != std::string::npos; + content.push_back(text(wifi_ssid_status_) | color(success ? Color::Green : Color::Red) | dim); + } + + content.push_back(separator()); + content.push_back(text("WiFi Password") | bold); + content.push_back(hbox({ + wifi_password_input_component_->Render() | flex, + separator(), + wifi_password_button_->Render() + })); + + if (!wifi_password_status_.empty()) { + bool success = wifi_password_status_.find("success") != std::string::npos; + content.push_back(text(wifi_password_status_) | color(success ? Color::Green : Color::Red) | dim); + } + + content.push_back(separator()); + content.push_back(text("Communication Method") | bold); + content.push_back(hbox({ + comm_method_dropdown_->Render() | flex, + separator(), + comm_method_button_->Render() + })); + + if (!communication_method_status_.empty()) { + bool success = communication_method_status_.find("success") != std::string::npos; + content.push_back(text(communication_method_status_) | color(success ? Color::Green : Color::Red) | dim); + } + + content.push_back(separator()); + content.push_back(separator()); + content.push_back(restart_button_->Render() | hcenter); + + if (!restart_status_.empty()) { + content.push_back(separator()); + content.push_back(text(restart_status_) | color(Color::Yellow) | bold | hcenter); + } + + return vbox(content); + }); +} + +void ConfigurationTab::setModuleId() { + try { + int new_id = std::stoi(module_id_input_); + if (new_id < 0 || new_id > 65535) { + module_id_status_ = "Error: Module ID must be between 0 and 65535"; + return; + } + + bool success = remote_config_->set_module_id(device_id_, static_cast(new_id)); + if (success) { + module_id_status_ = "Module ID set successfully!"; + } else { + module_id_status_ = "Failed to set module ID"; + } + } catch (const std::exception& e) { + module_id_status_ = std::format("Error: Invalid module ID format"); + } +} + +void ConfigurationTab::setModuleType() { + ModuleType type = static_cast(selected_module_type_); + bool success = remote_config_->set_module_type(device_id_, type); + + if (success) { + module_type_status_ = std::format("Module type set to {} successfully!", module_type_options_[selected_module_type_]); + } else { + module_type_status_ = "Failed to set module type"; + } +} + +void ConfigurationTab::setWiFiSSID() { + if (wifi_ssid_input_.empty()) { + wifi_ssid_status_ = "Error: SSID cannot be empty"; + return; + } + + bool success = remote_config_->set_wifi_ssid(device_id_, wifi_ssid_input_); + + if (success) { + wifi_ssid_status_ = "WiFi SSID set successfully!"; + } else { + wifi_ssid_status_ = "Failed to set WiFi SSID"; + } +} + +void ConfigurationTab::setWiFiPassword() { + if (wifi_password_input_.empty()) { + wifi_password_status_ = "Error: Password cannot be empty"; + return; + } + + bool success = remote_config_->set_wifi_password(device_id_, wifi_password_input_); + + if (success) { + wifi_password_status_ = "WiFi password set successfully!"; + // Clear password input for security + wifi_password_input_.clear(); + } else { + wifi_password_status_ = "Failed to set WiFi password"; + } +} + +void ConfigurationTab::setCommunicationMethod() { + uint8_t method = static_cast(selected_communication_method_); + bool success = remote_config_->set_communication_method(device_id_, method); + + if (success) { + communication_method_status_ = "Communication method set successfully!"; + } else { + communication_method_status_ = "Failed to set communication method"; + } +} + +void ConfigurationTab::restartDevice() { + restart_status_ = "Restarting device..."; + + // Restart the device in a separate thread + std::thread([this]() { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + remote_config_->restart(device_id_); + restart_status_ = "Device restart command sent."; + }).detach(); +} + +std::string ConfigurationTab::getModuleTypeString(ModuleType type) { + int type_value = static_cast(type); + if (type_value >= 0 && type_value < static_cast(module_type_options_.size())) { + return module_type_options_[type_value]; + } + return std::format("Unknown Type {}", type_value); +} diff --git a/src/tabs/CoredumpTab.cpp b/src/tabs/CoredumpTab.cpp new file mode 100644 index 0000000..12ffdd8 --- /dev/null +++ b/src/tabs/CoredumpTab.cpp @@ -0,0 +1,122 @@ +#include "tabs/CoredumpTab.h" +#include +#include +#include + +CoredumpTab::CoredumpTab(std::shared_ptr remote_debugging, uint8_t device_id) + : remote_debugging_(remote_debugging), + device_id_(device_id), + coredump_last_updated_(std::chrono::time_point{}) { +} + +Component CoredumpTab::createComponent() { + return Renderer([=, this] { + std::string coredump_data = getCachedCoredump(); + + Elements content = { + text("Core Dump") | bold | hcenter, + separator() + }; + + if (coredump_loading_ && !coredump_initial_load_done_) { + content.push_back(text("Loading core dump...") | color(Color::Yellow) | hcenter); + return vbox(content); + } + + if (coredump_data.empty()) { + content.push_back(text("No core dump available or device not responding") | color(Color::Yellow) | hcenter); + } else { + std::string sanitized = sanitizeData(coredump_data); + std::vector lines = splitLines(sanitized); + + Elements line_elements; + for (const auto& line : lines) { + if (line.empty()) { + line_elements.push_back(text(" ") | dim); + } else { + line_elements.push_back(text(line)); + } + } + + if (line_elements.empty()) { + content.push_back(text("No data found after processing") | color(Color::Yellow) | hcenter); + } else { + content.push_back( + vbox(line_elements) | vscroll_indicator | frame | flex + ); + } + } + + return vbox(content); + }); +} + +std::string CoredumpTab::getCachedCoredump() const { + auto now = std::chrono::system_clock::now(); + auto time_since_update = std::chrono::duration_cast(now - coredump_last_updated_); + + if (!coredump_initial_load_done_ && !coredump_loading_) { + coredump_loading_ = true; + std::thread([this]() { + cached_coredump_ = remote_debugging_->get_coredump(device_id_); + coredump_last_updated_ = std::chrono::system_clock::now(); + coredump_initial_load_done_ = true; + coredump_loading_ = false; + auto screen = ScreenInteractive::Active(); + if (screen) { + screen->Post(Event::Custom); + } + }).detach(); + return ""; + } + + if (time_since_update >= cache_duration_ && !coredump_loading_) { + coredump_loading_ = true; + std::thread([this]() { + cached_coredump_ = remote_debugging_->get_coredump(device_id_); + coredump_last_updated_ = std::chrono::system_clock::now(); + coredump_loading_ = false; + }).detach(); + } + + return cached_coredump_; +} + +std::string CoredumpTab::sanitizeData(const std::string& raw_data) { + std::string sanitized; + sanitized.reserve(raw_data.size()); + + for (char c : raw_data) { + if ((c >= 32 && c <= 126) || c == '\n' || c == '\r' || c == '\t') { + sanitized += c; + } else if (c < 32 && c != '\n' && c != '\r' && c != '\t') { + sanitized += '?'; + } + } + + return sanitized; +} + +std::vector CoredumpTab::splitLines(const std::string& data) { + std::vector lines; + std::string current_line; + + for (size_t i = 0; i < data.size(); ++i) { + char c = data[i]; + if (c == '\n') { + lines.push_back(current_line); + current_line.clear(); + } else if (c == '\r') { + lines.push_back(current_line); + current_line.clear(); + if (i + 1 < data.size() && data[i + 1] == '\n') { + ++i; + } + } else { + current_line += c; + } + } + + lines.push_back(current_line); + return lines; +} diff --git a/src/tabs/FirmwareUpdateTab.cpp b/src/tabs/FirmwareUpdateTab.cpp new file mode 100644 index 0000000..38e1a52 --- /dev/null +++ b/src/tabs/FirmwareUpdateTab.cpp @@ -0,0 +1,115 @@ +#include "tabs/FirmwareUpdateTab.h" +#include +#include +#include +#include + +FirmwareUpdateTab::FirmwareUpdateTab(std::shared_ptr robot_controller, uint8_t device_id) + : robot_controller_(robot_controller), + remote_management_(nullptr), + device_id_(device_id) { +} + +Component FirmwareUpdateTab::createComponent() { + auto file_input = Input(&firmware_file_path_, "Enter firmware file path..."); + + auto upload_button = Button("Upload Firmware", [&, this] { + if (!firmware_file_path_.empty() && !update_in_progress_) { + update_in_progress_ = true; + progress_ = 0; + update_status_message_ = "Initializing firmware update..."; + update_success_ = false; + + // Create RemoteManagement instance with the firmware file + remote_management_ = std::make_shared( + device_id_, + firmware_file_path_, + robot_controller_ + ); + + // Start the OTA update process in a background thread to avoid blocking UI + std::thread([this]() { + update_status_message_ = "Starting OTA process..."; + + // Start a separate thread to monitor progress + std::atomic ota_running{true}; + std::thread progress_monitor([this, &ota_running]() { + while (ota_running) { + if (remote_management_) { + double progress_percent = remote_management_->ota_progress(); + progress_ = static_cast(progress_percent * 100); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + }); + + bool success = remote_management_->perform_ota(); + + ota_running = false; + progress_monitor.join(); + + if (success) { + progress_ = 100; + update_status_message_ = "Firmware update completed successfully!"; + update_success_ = true; + } else { + progress_ = 0; + update_status_message_ = "Firmware update failed. Please check the file and try again."; + update_success_ = false; + } + + update_in_progress_ = false; + }).detach(); + } + }); + + auto restart_button = Button("Restart Device", [&, this] { + if (remote_management_ && !update_in_progress_) { + update_status_message_ = "Restarting device..."; + std::thread([this]() { + remote_management_->restart(); + update_status_message_ = "Device restart command sent."; + }).detach(); + } + }); + + auto container = Container::Vertical({ + file_input, + upload_button, + restart_button + }); + + return Renderer(container, [=, this] { + Elements content = { + text("Firmware Update") | bold | hcenter, + separator(), + hbox({ + text("Firmware File: "), + file_input->Render() | flex + }), + separator(), + upload_button->Render() | hcenter, + separator() + }; + + if (update_in_progress_) { + content.push_back(text(update_status_message_) | bold | hcenter | color(Color::Blue)); + content.push_back(separator()); + content.push_back(gauge(static_cast(progress_) / 100.0f) | color(Color::Blue)); + content.push_back(text(std::format("{}%", progress_)) | hcenter); + } else if (!update_status_message_.empty()) { + // Show the last status message + auto status_color = update_success_ ? Color::Green : Color::Red; + content.push_back(text(update_status_message_) | bold | hcenter | color(status_color)); + + if (update_success_) { + content.push_back(separator()); + content.push_back(restart_button->Render() | hcenter); + content.push_back(separator()); + content.push_back(text("You can now restart the device to apply the firmware.") | dim | hcenter); + } + } + + return vbox(content); + }); +} diff --git a/src/tabs/InformationTab.cpp b/src/tabs/InformationTab.cpp new file mode 100644 index 0000000..99685fb --- /dev/null +++ b/src/tabs/InformationTab.cpp @@ -0,0 +1,148 @@ +#include "tabs/InformationTab.h" +#include +#include + +InformationTab::InformationTab(std::shared_ptr robot_controller, uint8_t device_id) + : robot_controller_(robot_controller), device_id_(device_id) { +} + +Component InformationTab::createComponent() { + return Renderer([=, this] { + // Try to get the module data + auto module_opt = robot_controller_->getModule(device_id_); + + Elements content = { + text("Device Information") | bold | hcenter, + separator() + }; + + if (!module_opt.has_value()) { + content.push_back(text("Module not found or unavailable") | color(Color::Red) | hcenter); + return vbox(content); + } + + auto module_weak = module_opt.value(); + if (module_weak.expired()) { + content.push_back(text("Module reference expired") | color(Color::Red) | hcenter); + return vbox(content); + } + + auto module = module_weak.lock(); + if (!module) { + content.push_back(text("Failed to access module") | color(Color::Red) | hcenter); + return vbox(content); + } + + // Get real module data + uint8_t device_id = module->get_device_id(); + ModuleType module_type = module->get_type(); + Messaging::ConnectionType connection_type = module->get_connection_type(); + uint8_t leader = module->get_leader(); + auto last_updated = module->get_last_updated_time(); + auto neighbours = module->get_neighbours(); + + // Create information display with real data + Elements info_elements = { + hbox({text("Device ID: ") | bold, text(std::to_string(static_cast(device_id)))}), + hbox({text("Module Type: ") | bold, text(getModuleTypeString(module_type))}), + hbox({text("Connection Type: ") | bold, text(getConnectionTypeString(connection_type))}), + hbox({text("Leader ID: ") | bold, text(std::to_string(static_cast(leader)))}), + hbox({text("Last Updated: ") | bold, text(formatTimeSince(last_updated))}), + separator(), + text("Neighbours:") | bold + }; + + // Add neighbour information + if (neighbours.empty()) { + info_elements.push_back(text("• No neighbours detected") | dim); + } else { + for (const auto& neighbour : neighbours) { + info_elements.push_back( + text(std::format("• Device ID: {} (Orientation: {})", + static_cast(neighbour.device_id), + static_cast(neighbour.orientation))) | dim + ); + } + } + + info_elements.push_back(separator()); + + // Try to get sensor values (these may throw or return defaults if not implemented) + try { + double position = module->get_position(); + std::string text_value = module->get_text(); + + info_elements.push_back(text("Sensor Values:") | bold); + if (position != 0.0) { // Assume 0.0 means no position data + info_elements.push_back( + text(std::format("• Position: {:.2f}", position)) | dim + ); + } + if (!text_value.empty()) { + info_elements.push_back( + text(std::format("• Text: {}", text_value)) | dim + ); + } + if (position == 0.0 && text_value.empty()) { + info_elements.push_back(text("• No sensor data available") | dim); + } + } catch (...) { + info_elements.push_back(text("• Sensor data unavailable") | dim); + } + + content.insert(content.end(), info_elements.begin(), info_elements.end()); + + return vbox(content) | flex; + }); +} + +std::string InformationTab::getModuleTypeString(ModuleType type) { + // Provide meaningful names for common module types + int type_value = static_cast(type); + switch (type_value) { + case 0: return "MMMF Splitter"; + case 1: return "Arm Servo"; + case 2: return "DC Motor"; + case 3: return "Power"; + case 4: return "Pivot Servo"; + case 5: return "Display"; + case 6: return "Gripper"; + case 7: return "Speaker"; + case 8: return "IMU"; + case 9: return "Distance Sensor"; + case 10: return "MMMM Splitter"; + case 11: return "MMF Triangle Splitter"; + case 12: return "MMM Triangle Splitter"; + default: return std::format("Module Type {}", type_value); + } +} + +std::string InformationTab::getConnectionTypeString(Messaging::ConnectionType type) { + // Provide meaningful names for common connection types + int type_value = static_cast(type); + switch (type_value) { + case 0: return "Wi-Fi"; + case 1: return "Wired Hop"; + default: return std::format("Connection Type {}", type_value); + } +} + +std::string InformationTab::formatTimeSince(const std::chrono::time_point& time_point) { + auto now = std::chrono::system_clock::now(); + auto duration = std::chrono::duration_cast(now - time_point); + + auto seconds = duration.count(); + + if (seconds < 60) { + return std::format("{} seconds ago", seconds); + } else if (seconds < 3600) { + auto minutes = seconds / 60; + return std::format("{} minute{} ago", minutes, minutes == 1 ? "" : "s"); + } else if (seconds < 86400) { + auto hours = seconds / 3600; + return std::format("{} hour{} ago", hours, hours == 1 ? "" : "s"); + } else { + auto days = seconds / 86400; + return std::format("{} day{} ago", days, days == 1 ? "" : "s"); + } +} diff --git a/src/tabs/LoggingTab.cpp b/src/tabs/LoggingTab.cpp new file mode 100644 index 0000000..e732edb --- /dev/null +++ b/src/tabs/LoggingTab.cpp @@ -0,0 +1,141 @@ +#include "tabs/LoggingTab.h" +#include +#include +#include + +LoggingTab::LoggingTab(std::shared_ptr remote_debugging, uint8_t device_id) + : remote_debugging_(remote_debugging), + device_id_(device_id), + logs_last_updated_(std::chrono::time_point{}) { +} + +Component LoggingTab::createComponent() { + return Renderer([=, this] { + // Get cached log data (refreshed at most every 4 seconds) + std::string log_data = getCachedLogs(); + + Elements content = { + text("Device Logs") | bold | hcenter, + separator() + }; + + // Check if we're still loading the initial data + if (logs_loading_ && !logs_initial_load_done_) { + content.push_back(text("Loading logs...") | color(Color::Yellow) | hcenter); + return vbox(content); + } + + if (log_data.empty()) { + content.push_back(text("No logs available or device not responding") | color(Color::Yellow) | hcenter); + } else { + // Sanitize the log data + std::string sanitized_logs = sanitizeLogData(log_data); + + // Split the logs by newlines and create text elements for each line + std::vector log_lines = splitLogLines(sanitized_logs); + + Elements log_elements; + for (size_t i = 0; i < log_lines.size(); ++i) { + const auto& line = log_lines[i]; + if (line.empty()) { + // Show empty lines as a visual placeholder + log_elements.push_back(text(" ") | dim); + } else { + log_elements.push_back(text(line)); + } + } + + if (log_elements.empty()) { + content.push_back(text("No log lines found after processing") | color(Color::Yellow) | hcenter); + } else { + content.push_back( + vbox(log_elements) | vscroll_indicator | frame | flex + ); + } + } + + return vbox(content); + }); +} + +std::string LoggingTab::getCachedLogs() const { + auto now = std::chrono::system_clock::now(); + auto time_since_update = std::chrono::duration_cast(now - logs_last_updated_); + + // If this is the first load and not already loading, start async fetch + if (!logs_initial_load_done_ && !logs_loading_) { + logs_loading_ = true; + std::thread([this]() { + cached_logs_ = remote_debugging_->get_logs(device_id_); + logs_last_updated_ = std::chrono::system_clock::now(); + logs_initial_load_done_ = true; + logs_loading_ = false; + // Force screen refresh to show the loaded data + auto screen = ScreenInteractive::Active(); + if (screen) { + screen->Post(Event::Custom); + } + }).detach(); + return ""; // Return empty on first call, will show loading message + } + + // Update cache if it's been more than 4 seconds and not currently loading + if (time_since_update >= cache_duration_ && !logs_loading_) { + logs_loading_ = true; + std::thread([this]() { + cached_logs_ = remote_debugging_->get_logs(device_id_); + logs_last_updated_ = std::chrono::system_clock::now(); + logs_loading_ = false; + }).detach(); + } + + return cached_logs_; +} + +std::string LoggingTab::sanitizeLogData(const std::string& raw_data) { + std::string sanitized; + sanitized.reserve(raw_data.size()); // Reserve space to avoid reallocations + + for (char c : raw_data) { + // Allow printable ASCII characters and newlines + if ((c >= 32 && c <= 126) || c == '\n' || c == '\r' || c == '\t') { + sanitized += c; + } else if (c < 32 && c != '\n' && c != '\r' && c != '\t') { + // Replace non-printable control characters with a placeholder + sanitized += '?'; + } + // Skip characters outside ASCII range (>126) + } + + return sanitized; +} + +std::vector LoggingTab::splitLogLines(const std::string& log_data) { + std::vector lines; + std::string current_line; + + for (size_t i = 0; i < log_data.size(); ++i) { + char c = log_data[i]; + + if (c == '\n') { + // Handle \n or \r\n + lines.push_back(current_line); + current_line.clear(); + } else if (c == '\r') { + // Handle \r or \r\n + lines.push_back(current_line); + current_line.clear(); + // Skip the \n if it follows \r + if (i + 1 < log_data.size() && log_data[i + 1] == '\n') { + ++i; + } + } else { + current_line += c; + } + } + + // Add the last line even if it doesn't end with a newline + lines.push_back(current_line); + + return lines; +} diff --git a/src/tabs/TaskManagerTab.cpp b/src/tabs/TaskManagerTab.cpp new file mode 100644 index 0000000..6167667 --- /dev/null +++ b/src/tabs/TaskManagerTab.cpp @@ -0,0 +1,72 @@ +#include "tabs/TaskManagerTab.h" +#include +#include +#include + +TaskManagerTab::TaskManagerTab(std::shared_ptr remote_debugging, uint8_t device_id) + : remote_debugging_(remote_debugging), + device_id_(device_id), + task_data_last_updated_(std::chrono::time_point{}) { +} + +Component TaskManagerTab::createComponent() { + return Renderer([=, this] { + // Get cached task manager data (refreshed at most every 4 seconds) + std::string task_data = getCachedTaskData(); + + Elements content = { + text("Task Manager") | bold | hcenter, + separator() + }; + + // Check if we're still loading the initial data + if (task_data_loading_ && !task_data_initial_load_done_) { + content.push_back(text("Loading task manager data...") | color(Color::Yellow) | hcenter); + return vbox(content); + } + + if (task_data.empty()) { + content.push_back(text("No task manager data available or device not responding") | color(Color::Yellow) | hcenter); + } else { + content.push_back( + paragraph(task_data) | vscroll_indicator | frame | flex + ); + } + + return vbox(content); + }); +} + +std::string TaskManagerTab::getCachedTaskData() const { + auto now = std::chrono::system_clock::now(); + auto time_since_update = std::chrono::duration_cast(now - task_data_last_updated_); + + // If this is the first load and not already loading, start async fetch + if (!task_data_initial_load_done_ && !task_data_loading_) { + task_data_loading_ = true; + std::thread([this]() { + cached_task_data_ = remote_debugging_->get_task_manager(device_id_); + task_data_last_updated_ = std::chrono::system_clock::now(); + task_data_initial_load_done_ = true; + task_data_loading_ = false; + // Force screen refresh to show the loaded data + auto screen = ScreenInteractive::Active(); + if (screen) { + screen->Post(Event::Custom); + } + }).detach(); + return ""; // Return empty on first call, will show loading message + } + + // Update cache if it's been more than 4 seconds and not currently loading + if (time_since_update >= cache_duration_ && !task_data_loading_) { + task_data_loading_ = true; + std::thread([this]() { + cached_task_data_ = remote_debugging_->get_task_manager(device_id_); + task_data_last_updated_ = std::chrono::system_clock::now(); + task_data_loading_ = false; + }).detach(); + } + + return cached_task_data_; +}