mirror of
https://github.com/BotChain-Robots/dev-tools.git
synced 2026-07-08 15:07:22 +02:00
Cleanup for release
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
*.log
|
||||||
|
*.json
|
||||||
42
CMakeLists.txt
Normal file
42
CMakeLists.txt
Normal file
@@ -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
|
||||||
|
)
|
||||||
70
README.md
Normal file
70
README.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
3
build.sh
Executable file
3
build.sh
Executable file
@@ -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"
|
||||||
13
conanfile.txt
Normal file
13
conanfile.txt
Normal file
@@ -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
|
||||||
43
include/DeviceDetailsApp.h
Normal file
43
include/DeviceDetailsApp.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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 <ftxui/component/component.hpp>
|
||||||
|
|
||||||
|
using namespace ftxui;
|
||||||
|
|
||||||
|
class DeviceDetailsApp {
|
||||||
|
public:
|
||||||
|
DeviceDetailsApp(std::shared_ptr<RobotController> robot_controller, uint8_t device_id);
|
||||||
|
void run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<RobotController> robot_controller_;
|
||||||
|
std::shared_ptr<RemoteDebugging> remote_debugging_;
|
||||||
|
uint8_t device_id_;
|
||||||
|
int selected_tab_ = 0;
|
||||||
|
Component main_component_;
|
||||||
|
|
||||||
|
// Tab instances
|
||||||
|
std::unique_ptr<InformationTab> information_tab_;
|
||||||
|
std::unique_ptr<LoggingTab> logging_tab_;
|
||||||
|
std::unique_ptr<FirmwareUpdateTab> firmware_update_tab_;
|
||||||
|
std::unique_ptr<TaskManagerTab> task_manager_tab_;
|
||||||
|
std::unique_ptr<ConfigurationTab> configuration_tab_;
|
||||||
|
std::unique_ptr<CoredumpTab> coredump_tab_;
|
||||||
|
|
||||||
|
// Tab names
|
||||||
|
std::vector<std::string> tab_names_ = {"Information", "Logging", "Firmware Update", "TaskManager", "Configuration", "Core Dump"};
|
||||||
|
|
||||||
|
void setupUI();
|
||||||
|
};
|
||||||
31
include/DeviceListApp.h
Normal file
31
include/DeviceListApp.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include "libcontrol.h"
|
||||||
|
#include <ftxui/component/component.hpp>
|
||||||
|
|
||||||
|
using namespace ftxui;
|
||||||
|
|
||||||
|
class DeviceDetailsApp;
|
||||||
|
|
||||||
|
class DeviceListApp {
|
||||||
|
public:
|
||||||
|
DeviceListApp();
|
||||||
|
void run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<RobotController> robot_controller_;
|
||||||
|
std::vector<uint8_t> device_ids_;
|
||||||
|
std::vector<std::string> device_list_entries_;
|
||||||
|
int selected_device_ = 0;
|
||||||
|
Component main_component_;
|
||||||
|
|
||||||
|
void refreshDevices();
|
||||||
|
void setupUI();
|
||||||
|
void openDeviceDetails();
|
||||||
|
};
|
||||||
0
include/common/ModuleTypeUtils.h
Normal file
0
include/common/ModuleTypeUtils.h
Normal file
27
include/flatbuffers/OTAPacketBuilder.h
Normal file
27
include/flatbuffers/OTAPacketBuilder.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#ifndef OTAPACKETBUILDER_H
|
||||||
|
#define OTAPACKETBUILDER_H
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<uint8_t> &packet);
|
||||||
|
|
||||||
|
static const Messaging::OTAPacket *parse_ota_packet(const uint8_t *buffer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
flatbuffers::FlatBufferBuilder builder_;
|
||||||
|
};
|
||||||
|
} // namespace Flatbuffers
|
||||||
|
|
||||||
|
#endif // OTAPACKETBUILDER_H
|
||||||
15
include/flatbuffers/SerializedMessage.h
Normal file
15
include/flatbuffers/SerializedMessage.h
Normal file
@@ -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
|
||||||
128
include/flatbuffers_generated/OTAPacket_generated.h
Normal file
128
include/flatbuffers_generated/OTAPacket_generated.h
Normal file
@@ -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<uint16_t>(VT_SEQUENCE_NUMBER, 0);
|
||||||
|
}
|
||||||
|
uint16_t length() const {
|
||||||
|
return GetField<uint16_t>(VT_LENGTH, 0);
|
||||||
|
}
|
||||||
|
const ::flatbuffers::Vector<uint8_t> *payload() const {
|
||||||
|
return GetPointer<const ::flatbuffers::Vector<uint8_t> *>(VT_PAYLOAD);
|
||||||
|
}
|
||||||
|
bool Verify(::flatbuffers::Verifier &verifier) const {
|
||||||
|
return VerifyTableStart(verifier) &&
|
||||||
|
VerifyField<uint16_t>(verifier, VT_SEQUENCE_NUMBER, 2) &&
|
||||||
|
VerifyField<uint16_t>(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<uint16_t>(OTAPacket::VT_SEQUENCE_NUMBER, sequence_number, 0);
|
||||||
|
}
|
||||||
|
void add_length(uint16_t length) {
|
||||||
|
fbb_.AddElement<uint16_t>(OTAPacket::VT_LENGTH, length, 0);
|
||||||
|
}
|
||||||
|
void add_payload(::flatbuffers::Offset<::flatbuffers::Vector<uint8_t>> payload) {
|
||||||
|
fbb_.AddOffset(OTAPacket::VT_PAYLOAD, payload);
|
||||||
|
}
|
||||||
|
explicit OTAPacketBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
|
||||||
|
: fbb_(_fbb) {
|
||||||
|
start_ = fbb_.StartTable();
|
||||||
|
}
|
||||||
|
::flatbuffers::Offset<OTAPacket> Finish() {
|
||||||
|
const auto end = fbb_.EndTable(start_);
|
||||||
|
auto o = ::flatbuffers::Offset<OTAPacket>(end);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ::flatbuffers::Offset<OTAPacket> CreateOTAPacket(
|
||||||
|
::flatbuffers::FlatBufferBuilder &_fbb,
|
||||||
|
uint16_t sequence_number = 0,
|
||||||
|
uint16_t length = 0,
|
||||||
|
::flatbuffers::Offset<::flatbuffers::Vector<uint8_t>> 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<OTAPacket> CreateOTAPacketDirect(
|
||||||
|
::flatbuffers::FlatBufferBuilder &_fbb,
|
||||||
|
uint16_t sequence_number = 0,
|
||||||
|
uint16_t length = 0,
|
||||||
|
const std::vector<uint8_t> *payload = nullptr) {
|
||||||
|
auto payload__ = payload ? _fbb.CreateVector<uint8_t>(*payload) : 0;
|
||||||
|
return Messaging::CreateOTAPacket(
|
||||||
|
_fbb,
|
||||||
|
sequence_number,
|
||||||
|
length,
|
||||||
|
payload__);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const Messaging::OTAPacket *GetOTAPacket(const void *buf) {
|
||||||
|
return ::flatbuffers::GetRoot<Messaging::OTAPacket>(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const Messaging::OTAPacket *GetSizePrefixedOTAPacket(const void *buf) {
|
||||||
|
return ::flatbuffers::GetSizePrefixedRoot<Messaging::OTAPacket>(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool VerifyOTAPacketBuffer(
|
||||||
|
::flatbuffers::Verifier &verifier) {
|
||||||
|
return verifier.VerifyBuffer<Messaging::OTAPacket>(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool VerifySizePrefixedOTAPacketBuffer(
|
||||||
|
::flatbuffers::Verifier &verifier) {
|
||||||
|
return verifier.VerifySizePrefixedBuffer<Messaging::OTAPacket>(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void FinishOTAPacketBuffer(
|
||||||
|
::flatbuffers::FlatBufferBuilder &fbb,
|
||||||
|
::flatbuffers::Offset<Messaging::OTAPacket> root) {
|
||||||
|
fbb.Finish(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void FinishSizePrefixedOTAPacketBuffer(
|
||||||
|
::flatbuffers::FlatBufferBuilder &fbb,
|
||||||
|
::flatbuffers::Offset<Messaging::OTAPacket> root) {
|
||||||
|
fbb.FinishSizePrefixed(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Messaging
|
||||||
|
|
||||||
|
#endif // FLATBUFFERS_GENERATED_OTAPACKET_MESSAGING_H_
|
||||||
350
include/flatbuffers_generated/RobotModule_generated.h
Normal file
350
include/flatbuffers_generated/RobotModule_generated.h
Normal file
@@ -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<size_t>(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<size_t>(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<size_t>(e);
|
||||||
|
return EnumNamesModuleState()[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T> struct ModuleStateTraits {
|
||||||
|
static const ModuleState enum_value = ModuleState_NONE;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<> struct ModuleStateTraits<MotorState> {
|
||||||
|
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<void>> *values, const ::flatbuffers::Vector<uint8_t> *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<int32_t>(VT_ANGLE, 0);
|
||||||
|
}
|
||||||
|
bool Verify(::flatbuffers::Verifier &verifier) const {
|
||||||
|
return VerifyTableStart(verifier) &&
|
||||||
|
VerifyField<int32_t>(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<int32_t>(MotorState::VT_ANGLE, angle, 0);
|
||||||
|
}
|
||||||
|
explicit MotorStateBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
|
||||||
|
: fbb_(_fbb) {
|
||||||
|
start_ = fbb_.StartTable();
|
||||||
|
}
|
||||||
|
::flatbuffers::Offset<MotorState> Finish() {
|
||||||
|
const auto end = fbb_.EndTable(start_);
|
||||||
|
auto o = ::flatbuffers::Offset<MotorState>(end);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ::flatbuffers::Offset<MotorState> 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<uint8_t>(VT_ID, 0);
|
||||||
|
}
|
||||||
|
ModuleType module_type() const {
|
||||||
|
return static_cast<ModuleType>(GetField<int8_t>(VT_MODULE_TYPE, 0));
|
||||||
|
}
|
||||||
|
ModuleState configuration_type() const {
|
||||||
|
return static_cast<ModuleState>(GetField<uint8_t>(VT_CONFIGURATION_TYPE, 0));
|
||||||
|
}
|
||||||
|
const void *configuration() const {
|
||||||
|
return GetPointer<const void *>(VT_CONFIGURATION);
|
||||||
|
}
|
||||||
|
template<typename T> const T *configuration_as() const;
|
||||||
|
const MotorState *configuration_as_MotorState() const {
|
||||||
|
return configuration_type() == ModuleState_MotorState ? static_cast<const MotorState *>(configuration()) : nullptr;
|
||||||
|
}
|
||||||
|
bool Verify(::flatbuffers::Verifier &verifier) const {
|
||||||
|
return VerifyTableStart(verifier) &&
|
||||||
|
VerifyField<uint8_t>(verifier, VT_ID, 1) &&
|
||||||
|
VerifyField<int8_t>(verifier, VT_MODULE_TYPE, 1) &&
|
||||||
|
VerifyField<uint8_t>(verifier, VT_CONFIGURATION_TYPE, 1) &&
|
||||||
|
VerifyOffset(verifier, VT_CONFIGURATION) &&
|
||||||
|
VerifyModuleState(verifier, configuration(), configuration_type()) &&
|
||||||
|
verifier.EndTable();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<> inline const MotorState *RobotModule::configuration_as<MotorState>() 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<uint8_t>(RobotModule::VT_ID, id, 0);
|
||||||
|
}
|
||||||
|
void add_module_type(ModuleType module_type) {
|
||||||
|
fbb_.AddElement<int8_t>(RobotModule::VT_MODULE_TYPE, static_cast<int8_t>(module_type), 0);
|
||||||
|
}
|
||||||
|
void add_configuration_type(ModuleState configuration_type) {
|
||||||
|
fbb_.AddElement<uint8_t>(RobotModule::VT_CONFIGURATION_TYPE, static_cast<uint8_t>(configuration_type), 0);
|
||||||
|
}
|
||||||
|
void add_configuration(::flatbuffers::Offset<void> configuration) {
|
||||||
|
fbb_.AddOffset(RobotModule::VT_CONFIGURATION, configuration);
|
||||||
|
}
|
||||||
|
explicit RobotModuleBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
|
||||||
|
: fbb_(_fbb) {
|
||||||
|
start_ = fbb_.StartTable();
|
||||||
|
}
|
||||||
|
::flatbuffers::Offset<RobotModule> Finish() {
|
||||||
|
const auto end = fbb_.EndTable(start_);
|
||||||
|
auto o = ::flatbuffers::Offset<RobotModule>(end);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ::flatbuffers::Offset<RobotModule> CreateRobotModule(
|
||||||
|
::flatbuffers::FlatBufferBuilder &_fbb,
|
||||||
|
uint8_t id = 0,
|
||||||
|
ModuleType module_type = ModuleType_SPLITTER,
|
||||||
|
ModuleState configuration_type = ModuleState_NONE,
|
||||||
|
::flatbuffers::Offset<void> 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<const MotorState *>(obj);
|
||||||
|
return verifier.VerifyTable(ptr);
|
||||||
|
}
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool VerifyModuleStateVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset<void>> *values, const ::flatbuffers::Vector<uint8_t> *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<ModuleState>(i))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const RobotModule *GetRobotModule(const void *buf) {
|
||||||
|
return ::flatbuffers::GetRoot<RobotModule>(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const RobotModule *GetSizePrefixedRobotModule(const void *buf) {
|
||||||
|
return ::flatbuffers::GetSizePrefixedRoot<RobotModule>(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool VerifyRobotModuleBuffer(
|
||||||
|
::flatbuffers::Verifier &verifier) {
|
||||||
|
return verifier.VerifyBuffer<RobotModule>(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool VerifySizePrefixedRobotModuleBuffer(
|
||||||
|
::flatbuffers::Verifier &verifier) {
|
||||||
|
return verifier.VerifySizePrefixedBuffer<RobotModule>(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void FinishRobotModuleBuffer(
|
||||||
|
::flatbuffers::FlatBufferBuilder &fbb,
|
||||||
|
::flatbuffers::Offset<RobotModule> root) {
|
||||||
|
fbb.Finish(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void FinishSizePrefixedRobotModuleBuffer(
|
||||||
|
::flatbuffers::FlatBufferBuilder &fbb,
|
||||||
|
::flatbuffers::Offset<RobotModule> root) {
|
||||||
|
fbb.FinishSizePrefixed(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // FLATBUFFERS_GENERATED_ROBOTMODULE_H_
|
||||||
28
include/rpc/RemoteConfig.h
Normal file
28
include/rpc/RemoteConfig.h
Normal file
@@ -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 <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "libcontrol.h"
|
||||||
|
#include "flatbuffers_generated/RobotModule_generated.h"
|
||||||
|
|
||||||
|
class RemoteConfig {
|
||||||
|
public:
|
||||||
|
RemoteConfig(std::shared_ptr<RobotController> 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<RobotController> m_robot_controller;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //NEW_DEV_TOOLS_REMOTECONFIG_H
|
||||||
24
include/rpc/RemoteDebugging.h
Normal file
24
include/rpc/RemoteDebugging.h
Normal file
@@ -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 <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "libcontrol.h"
|
||||||
|
|
||||||
|
class RemoteDebugging {
|
||||||
|
public:
|
||||||
|
RemoteDebugging(std::shared_ptr<RobotController> 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<RobotController> m_robot_controller;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //NEW_DEV_TOOLS_REMOTEDEBUGGING_H
|
||||||
40
include/rpc/RemoteManagement.h
Normal file
40
include/rpc/RemoteManagement.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// Created by Johnathon Slightham on 2026-02-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef REMOTEMANAGEMENT_H
|
||||||
|
#define REMOTEMANAGEMENT_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "flatbuffers/OTAPacketBuilder.h"
|
||||||
|
#include "libcontrol.h"
|
||||||
|
|
||||||
|
class RemoteManagement {
|
||||||
|
public:
|
||||||
|
RemoteManagement(uint8_t module_id, const std::string &path,
|
||||||
|
std::shared_ptr<RobotController> controller)
|
||||||
|
: m_module_id(module_id), m_file(path, std::ios::binary),
|
||||||
|
m_builder(std::make_unique<Flatbuffers::OTAPacketBuilder>()),
|
||||||
|
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<uint8_t> &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<Flatbuffers::OTAPacketBuilder> m_builder;
|
||||||
|
std::shared_ptr<RobotController> m_robot_controller;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // REMOTEMANAGEMENT_H
|
||||||
78
include/tabs/ConfigurationTab.h
Normal file
78
include/tabs/ConfigurationTab.h
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include "libcontrol.h"
|
||||||
|
#include "rpc/RemoteConfig.h"
|
||||||
|
#include <ftxui/component/component.hpp>
|
||||||
|
|
||||||
|
using namespace ftxui;
|
||||||
|
|
||||||
|
class ConfigurationTab {
|
||||||
|
public:
|
||||||
|
ConfigurationTab(std::shared_ptr<RobotController> robot_controller, uint8_t device_id);
|
||||||
|
Component createComponent();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<RobotController> robot_controller_;
|
||||||
|
std::shared_ptr<RemoteConfig> 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<std::string> 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<std::string> communication_method_options_ = {
|
||||||
|
"Wireless"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
void setModuleId();
|
||||||
|
void setModuleType();
|
||||||
|
void setWiFiSSID();
|
||||||
|
void setWiFiPassword();
|
||||||
|
void setCommunicationMethod();
|
||||||
|
void restartDevice();
|
||||||
|
std::string getModuleTypeString(ModuleType type);
|
||||||
|
};
|
||||||
32
include/tabs/CoredumpTab.h
Normal file
32
include/tabs/CoredumpTab.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <chrono>
|
||||||
|
#include <atomic>
|
||||||
|
#include "libcontrol.h"
|
||||||
|
#include "rpc/RemoteDebugging.h"
|
||||||
|
#include <ftxui/component/component.hpp>
|
||||||
|
|
||||||
|
using namespace ftxui;
|
||||||
|
|
||||||
|
class CoredumpTab {
|
||||||
|
public:
|
||||||
|
CoredumpTab(std::shared_ptr<RemoteDebugging> remote_debugging, uint8_t device_id);
|
||||||
|
Component createComponent();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<RemoteDebugging> 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<std::chrono::system_clock> coredump_last_updated_;
|
||||||
|
mutable std::atomic<bool> 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<std::string> splitLines(const std::string& data);
|
||||||
|
};
|
||||||
28
include/tabs/FirmwareUpdateTab.h
Normal file
28
include/tabs/FirmwareUpdateTab.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <atomic>
|
||||||
|
#include "libcontrol.h"
|
||||||
|
#include "rpc/RemoteManagement.h"
|
||||||
|
#include <ftxui/component/component.hpp>
|
||||||
|
|
||||||
|
using namespace ftxui;
|
||||||
|
|
||||||
|
class FirmwareUpdateTab {
|
||||||
|
public:
|
||||||
|
FirmwareUpdateTab(std::shared_ptr<RobotController> robot_controller, uint8_t device_id);
|
||||||
|
Component createComponent();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<RobotController> robot_controller_;
|
||||||
|
std::shared_ptr<RemoteManagement> 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;
|
||||||
|
};
|
||||||
23
include/tabs/InformationTab.h
Normal file
23
include/tabs/InformationTab.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <chrono>
|
||||||
|
#include "libcontrol.h"
|
||||||
|
#include <ftxui/component/component.hpp>
|
||||||
|
|
||||||
|
using namespace ftxui;
|
||||||
|
|
||||||
|
class InformationTab {
|
||||||
|
public:
|
||||||
|
InformationTab(std::shared_ptr<RobotController> robot_controller, uint8_t device_id);
|
||||||
|
Component createComponent();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<RobotController> 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<std::chrono::system_clock>& time_point);
|
||||||
|
};
|
||||||
32
include/tabs/LoggingTab.h
Normal file
32
include/tabs/LoggingTab.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <chrono>
|
||||||
|
#include <atomic>
|
||||||
|
#include "libcontrol.h"
|
||||||
|
#include "rpc/RemoteDebugging.h"
|
||||||
|
#include <ftxui/component/component.hpp>
|
||||||
|
|
||||||
|
using namespace ftxui;
|
||||||
|
|
||||||
|
class LoggingTab {
|
||||||
|
public:
|
||||||
|
LoggingTab(std::shared_ptr<RemoteDebugging> remote_debugging, uint8_t device_id);
|
||||||
|
Component createComponent();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<RemoteDebugging> 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<std::chrono::system_clock> logs_last_updated_;
|
||||||
|
mutable std::atomic<bool> 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<std::string> splitLogLines(const std::string& log_data);
|
||||||
|
};
|
||||||
30
include/tabs/TaskManagerTab.h
Normal file
30
include/tabs/TaskManagerTab.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <chrono>
|
||||||
|
#include <atomic>
|
||||||
|
#include "libcontrol.h"
|
||||||
|
#include "rpc/RemoteDebugging.h"
|
||||||
|
#include <ftxui/component/component.hpp>
|
||||||
|
|
||||||
|
using namespace ftxui;
|
||||||
|
|
||||||
|
class TaskManagerTab {
|
||||||
|
public:
|
||||||
|
TaskManagerTab(std::shared_ptr<RemoteDebugging> remote_debugging, uint8_t device_id);
|
||||||
|
Component createComponent();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<RemoteDebugging> 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<std::chrono::system_clock> task_data_last_updated_;
|
||||||
|
mutable std::atomic<bool> 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;
|
||||||
|
};
|
||||||
71
src/DeviceDetailsApp.cpp
Normal file
71
src/DeviceDetailsApp.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#include "DeviceDetailsApp.h"
|
||||||
|
#include <ftxui/component/screen_interactive.hpp>
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
DeviceDetailsApp::DeviceDetailsApp(std::shared_ptr<RobotController> robot_controller, uint8_t device_id)
|
||||||
|
: robot_controller_(robot_controller),
|
||||||
|
remote_debugging_(std::make_shared<RemoteDebugging>(robot_controller)),
|
||||||
|
device_id_(device_id) {
|
||||||
|
// Create tab instances
|
||||||
|
information_tab_ = std::make_unique<InformationTab>(robot_controller_, device_id_);
|
||||||
|
logging_tab_ = std::make_unique<LoggingTab>(remote_debugging_, device_id_);
|
||||||
|
firmware_update_tab_ = std::make_unique<FirmwareUpdateTab>(robot_controller_, device_id_);
|
||||||
|
task_manager_tab_ = std::make_unique<TaskManagerTab>(remote_debugging_, device_id_);
|
||||||
|
configuration_tab_ = std::make_unique<ConfigurationTab>(robot_controller_, device_id_);
|
||||||
|
coredump_tab_ = std::make_unique<CoredumpTab>(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<int>(device_id_))) | bold
|
||||||
|
}),
|
||||||
|
separator(),
|
||||||
|
tab_toggle->Render() | hcenter,
|
||||||
|
separator(),
|
||||||
|
tab_container->Render() | flex
|
||||||
|
}) | border;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
98
src/DeviceListApp.cpp
Normal file
98
src/DeviceListApp.cpp
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
#include "DeviceListApp.h"
|
||||||
|
#include "DeviceDetailsApp.h"
|
||||||
|
#include <ftxui/component/screen_interactive.hpp>
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
DeviceListApp::DeviceListApp() : robot_controller_(std::make_shared<RobotController>()) {
|
||||||
|
refreshDevices();
|
||||||
|
setupUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceListApp::run() {
|
||||||
|
auto screen = ScreenInteractive::Fullscreen();
|
||||||
|
screen.Loop(main_component_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceListApp::refreshDevices() {
|
||||||
|
// Find connected modules (returns std::unordered_set<uint8_t>)
|
||||||
|
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<int>(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<int>(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<int>(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();
|
||||||
|
}
|
||||||
24
src/flatbuffers/OTAPacketBuilder.cpp
Normal file
24
src/flatbuffers/OTAPacketBuilder.cpp
Normal file
@@ -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<uint8_t> &packet) {
|
||||||
|
builder_.Clear();
|
||||||
|
|
||||||
|
const auto packet_vector = builder_.CreateVector(packet);
|
||||||
|
|
||||||
|
const auto message = Messaging::CreateOTAPacket(
|
||||||
|
builder_, packet_num, static_cast<int>(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<Messaging::OTAPacket>(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Flatbuffers
|
||||||
15
src/main.cpp
Normal file
15
src/main.cpp
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
53
src/rpc/RemoteConfig.cpp
Normal file
53
src/rpc/RemoteConfig.cpp
Normal file
@@ -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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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, {});
|
||||||
|
}
|
||||||
32
src/rpc/RemoteDebugging.cpp
Normal file
32
src/rpc/RemoteDebugging.cpp
Normal file
@@ -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 "";
|
||||||
|
}
|
||||||
110
src/rpc/RemoteManagement.cpp
Normal file
110
src/rpc/RemoteManagement.cpp
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
//
|
||||||
|
// Created by Johnathon Slightham on 2026-02-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#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<uint8_t> buffer(OTA_CHUNK_SIZE);
|
||||||
|
m_file.read(reinterpret_cast<char *>(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<uint8_t> &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<uint8_t> 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<double>(m_sequence_num) / static_cast<double>(m_total_packets);
|
||||||
|
}
|
||||||
221
src/tabs/ConfigurationTab.cpp
Normal file
221
src/tabs/ConfigurationTab.cpp
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
#include "tabs/ConfigurationTab.h"
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
#include <format>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
ConfigurationTab::ConfigurationTab(std::shared_ptr<RobotController> robot_controller, uint8_t device_id)
|
||||||
|
: robot_controller_(robot_controller),
|
||||||
|
remote_config_(std::make_shared<RemoteConfig>(robot_controller)),
|
||||||
|
device_id_(device_id) {
|
||||||
|
// Initialize module ID input with current device ID
|
||||||
|
module_id_input_ = std::to_string(static_cast<int>(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<uint16_t>(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<ModuleType>(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<uint8_t>(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<int>(type);
|
||||||
|
if (type_value >= 0 && type_value < static_cast<int>(module_type_options_.size())) {
|
||||||
|
return module_type_options_[type_value];
|
||||||
|
}
|
||||||
|
return std::format("Unknown Type {}", type_value);
|
||||||
|
}
|
||||||
122
src/tabs/CoredumpTab.cpp
Normal file
122
src/tabs/CoredumpTab.cpp
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#include "tabs/CoredumpTab.h"
|
||||||
|
#include <ftxui/component/screen_interactive.hpp>
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
CoredumpTab::CoredumpTab(std::shared_ptr<RemoteDebugging> remote_debugging, uint8_t device_id)
|
||||||
|
: remote_debugging_(remote_debugging),
|
||||||
|
device_id_(device_id),
|
||||||
|
coredump_last_updated_(std::chrono::time_point<std::chrono::system_clock>{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
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<std::string> 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<std::chrono::seconds>(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<std::string> CoredumpTab::splitLines(const std::string& data) {
|
||||||
|
std::vector<std::string> 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;
|
||||||
|
}
|
||||||
115
src/tabs/FirmwareUpdateTab.cpp
Normal file
115
src/tabs/FirmwareUpdateTab.cpp
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#include "tabs/FirmwareUpdateTab.h"
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
#include <format>
|
||||||
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
FirmwareUpdateTab::FirmwareUpdateTab(std::shared_ptr<RobotController> 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<RemoteManagement>(
|
||||||
|
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<bool> 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<int>(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<float>(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);
|
||||||
|
});
|
||||||
|
}
|
||||||
148
src/tabs/InformationTab.cpp
Normal file
148
src/tabs/InformationTab.cpp
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#include "tabs/InformationTab.h"
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
InformationTab::InformationTab(std::shared_ptr<RobotController> 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<int>(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<int>(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<int>(neighbour.device_id),
|
||||||
|
static_cast<int>(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<int>(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<int>(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<std::chrono::system_clock>& time_point) {
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto duration = std::chrono::duration_cast<std::chrono::seconds>(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");
|
||||||
|
}
|
||||||
|
}
|
||||||
141
src/tabs/LoggingTab.cpp
Normal file
141
src/tabs/LoggingTab.cpp
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#include "tabs/LoggingTab.h"
|
||||||
|
#include <ftxui/component/screen_interactive.hpp>
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
LoggingTab::LoggingTab(std::shared_ptr<RemoteDebugging> remote_debugging, uint8_t device_id)
|
||||||
|
: remote_debugging_(remote_debugging),
|
||||||
|
device_id_(device_id),
|
||||||
|
logs_last_updated_(std::chrono::time_point<std::chrono::system_clock>{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
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<std::string> 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<std::chrono::seconds>(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<std::string> LoggingTab::splitLogLines(const std::string& log_data) {
|
||||||
|
std::vector<std::string> 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;
|
||||||
|
}
|
||||||
72
src/tabs/TaskManagerTab.cpp
Normal file
72
src/tabs/TaskManagerTab.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#include "tabs/TaskManagerTab.h"
|
||||||
|
#include <ftxui/component/screen_interactive.hpp>
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
TaskManagerTab::TaskManagerTab(std::shared_ptr<RemoteDebugging> remote_debugging, uint8_t device_id)
|
||||||
|
: remote_debugging_(remote_debugging),
|
||||||
|
device_id_(device_id),
|
||||||
|
task_data_last_updated_(std::chrono::time_point<std::chrono::system_clock>{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
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<std::chrono::seconds>(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_;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user