Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ _wasi_sdk_fetch

output/
.devcontainer/devcontainer-lock.json

.venv/
__pycache__/
imgui.ini
31 changes: 19 additions & 12 deletions cmake/NativeTargets.cmake
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

option(DISABLE_COMPANION "Disable building companion and simulators" OFF)
option(EDGE_TX_BUILD_TESTS "Build native test targets" ON)

if(NOT DISABLE_COMPANION)
include(QtDefs)
Expand Down Expand Up @@ -34,14 +35,18 @@ if(WIN32)
endif()

# google tests
include(FetchGtest)
if(EDGE_TX_BUILD_TESTS)
include(FetchGtest)
endif()

add_custom_target(tests-radio
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/gtests-radio
DEPENDS gtests-radio
)
if(EDGE_TX_BUILD_TESTS)
add_custom_target(tests-radio
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/gtests-radio
DEPENDS gtests-radio
)
endif()

if(Qt6Core_FOUND AND NOT DISABLE_COMPANION)
if(Qt6Core_FOUND AND NOT DISABLE_COMPANION AND EDGE_TX_BUILD_TESTS)
add_subdirectory(${COMPANION_SRC_DIRECTORY})
add_custom_target(tests-companion
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/gtests-companion
Expand All @@ -54,12 +59,14 @@ if(Qt6Core_FOUND AND NOT DISABLE_COMPANION)
DEPENDS tests-radio tests-companion
)
else()
add_custom_target(gtests
DEPENDS gtests-radio
)
add_custom_target(tests
DEPENDS tests-radio
)
if(EDGE_TX_BUILD_TESTS)
add_custom_target(gtests
DEPENDS gtests-radio
)
add_custom_target(tests
DEPENDS tests-radio
)
endif()
endif()

set(IGNORE "${ARM_TOOLCHAIN_DIR}")
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[project]
name = "edgetx-dev"
version = "0.0.0"
requires-python = ">=3.11"
dependencies = [
"clang",
"jinja2",
"lz4",
"pillow",
"pydantic",
"typing-extensions",
]

[tool.uv]
package = false
2 changes: 1 addition & 1 deletion radio/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ if(NATIVE_BUILD)

add_subdirectory(targets/simu)

if(NOT WASI)
if(NOT WASI AND EDGE_TX_BUILD_TESTS)
add_subdirectory(tests)
endif()
endif()
Expand Down
7 changes: 6 additions & 1 deletion radio/src/targets/simu/arg_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ bool ArgumentParser::parse(int argc, char *argv[]) {
} else if (arg == "--settings") {
if (!getNextArg(argc, argv, i, settings_path, "settings"))
return false;
} else if (arg == "--automation-stdio") {
automation_stdio = true;
} else if (arg == "-h" || arg == "--help") {
help_requested = true;
return true;
Expand All @@ -38,7 +40,7 @@ bool ArgumentParser::parse(int argc, char *argv[]) {

void ArgumentParser::printUsage() const {
printf("usage: %s [--width width] [--height height] [--storage path] "
"[--settings path] [-h | --help]\n",
"[--settings path] [--automation-stdio] [-h | --help]\n",
program_name.c_str());
}

Expand All @@ -49,6 +51,7 @@ void ArgumentParser::printHelp() const {
printf(" --height height Set the height (integer)\n");
printf(" --storage path Set the storage path\n");
printf(" --settings path Set the settings path\n");
printf(" --automation-stdio Enable newline command automation over stdin/stdout\n");
printf(" -h, --help Show this help message\n");
}

Expand All @@ -66,6 +69,8 @@ const std::string &ArgumentParser::getSettingsPath() const {
return settings_path;
}

bool ArgumentParser::isAutomationStdioEnabled() const { return automation_stdio; }

bool ArgumentParser::hasWidth() const { return width != -1; }

bool ArgumentParser::hasHeight() const { return height != -1; }
Expand Down
2 changes: 2 additions & 0 deletions radio/src/targets/simu/arg_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class ArgumentParser {
std::string storage_path;
std::string settings_path;
bool help_requested = false;
bool automation_stdio = false;
std::string program_name;

public:
Expand All @@ -24,6 +25,7 @@ class ArgumentParser {
int getHeight() const;
const std::string &getStoragePath() const;
const std::string &getSettingsPath() const;
bool isAutomationStdioEnabled() const;

// Check if option was provided
bool hasWidth() const;
Expand Down
202 changes: 202 additions & 0 deletions radio/src/targets/simu/sdl_simu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@
#include <fstream>
#include <filesystem>
#include <regex>
#include <sstream>
#include <string>
#include <vector>

#if !defined(_WIN32)
#include <sys/select.h>
#include <unistd.h>
#endif

#include "hal/adc_driver.h"
#include "hal/rotary_encoder.h"
Expand Down Expand Up @@ -83,6 +90,7 @@
static SDL_Window* window;
static SDL_Renderer* renderer;
static SDL_Texture* screen_frame_buffer;
static bool automation_stdio = false;

static GimbalState stick_left = {{0.5f, 0.5f}, false};
static GimbalState stick_right = {{0.5f, 0.5f}, false};
Expand All @@ -99,6 +107,194 @@ extern volatile rotenc_t rotencValue;

int pots[MAX_POTS] = {0};

static std::string json_escape(const std::string& value)
{
std::string escaped;
escaped.reserve(value.size());
for (char ch : value) {
switch (ch) {
case '\\': escaped += "\\\\"; break;
case '"': escaped += "\\\""; break;
case '\n': escaped += "\\n"; break;
case '\r': escaped += "\\r"; break;
case '\t': escaped += "\\t"; break;
default: escaped += ch; break;
}
}
return escaped;
}

static void automation_reply_ok(const std::string& extra = "")
{
std::cout << "{\"ok\":true";
if (!extra.empty()) std::cout << "," << extra;
std::cout << "}" << std::endl;
}

static void automation_reply_error(const std::string& message)
{
std::cout << "{\"ok\":false,\"error\":\"" << json_escape(message) << "\"}" << std::endl;
}

static bool key_from_name(const std::string& name, uint8_t& key)
{
static const std::pair<const char*, uint8_t> keys[] = {
{"MENU", KEY_MENU}, {"EXIT", KEY_EXIT}, {"ENTER", KEY_ENTER},
{"PAGEUP", KEY_PAGEUP}, {"PAGEDN", KEY_PAGEDN}, {"UP", KEY_UP},
{"DOWN", KEY_DOWN}, {"LEFT", KEY_LEFT}, {"RIGHT", KEY_RIGHT},
{"PLUS", KEY_PLUS}, {"MINUS", KEY_MINUS}, {"MODEL", KEY_MODEL},
{"TELE", KEY_TELE}, {"SYS", KEY_SYS}, {"SHIFT", KEY_SHIFT},
{"BIND", KEY_BIND},
};

for (const auto& candidate : keys) {
if (name == candidate.first) {
key = candidate.second;
return true;
}
}

return false;
}

static bool write_ppm_screenshot(const std::string& path)
{
uint32_t width = simuLcdGetWidth();
uint32_t height = simuLcdGetHeight();
uint32_t depth = simuLcdGetDepth();
uint32_t bytes_per_pixel = depth == 16 ? 2 : 1;
std::vector<uint8_t> lcd(width * height * bytes_per_pixel);
uint32_t copied = simuLcdCopy(lcd.data(), lcd.size());
if (copied == 0) return false;

std::ofstream out(path, std::ios::binary);
if (!out) return false;

out << "P6\n" << width << " " << height << "\n255\n";

if (depth == 16) {
const uint16_t* pixels = reinterpret_cast<const uint16_t*>(lcd.data());
for (uint32_t i = 0; i < width * height; i++) {
uint16_t z = pixels[i];
uint8_t rgb[3] = {
static_cast<uint8_t>(((z & 0xF800) >> 8) + ((z & 0xE000) >> 13)),
static_cast<uint8_t>(((z & 0x07E0) >> 3) + ((z & 0x0600) >> 9)),
static_cast<uint8_t>(((z & 0x001F) << 3) + ((z & 0x001C) >> 2)),
};
out.write(reinterpret_cast<const char*>(rgb), sizeof(rgb));
}
} else {
const uint8_t black[3] = {0, 0, 0};
const uint8_t white[3] = {255, 255, 255};
for (uint32_t i = 0; i < width * height; i++) {
out.write(reinterpret_cast<const char*>(lcd[i] ? black : white), 3);
}
}

return true;
}

static void automation_handle_command(const std::string& line)
{
std::istringstream in(line);
std::string command;
in >> command;

if (command.empty()) return;

if (command == "status") {
std::ostringstream extra;
extra << "\"running\":" << (simuIsRunning() ? "true" : "false")
<< ",\"width\":" << simuLcdGetWidth()
<< ",\"height\":" << simuLcdGetHeight()
<< ",\"depth\":" << simuLcdGetDepth();
automation_reply_ok(extra.str());
} else if (command == "press" || command == "long_press") {
std::string key_name;
int duration_ms = command == "long_press" ? 800 : 120;
in >> key_name;
if (key_name.empty()) {
automation_reply_error("missing key name");
return;
}
in >> duration_ms;

uint8_t key;
if (!key_from_name(key_name, key)) {
automation_reply_error("unknown key: " + key_name);
return;
}

simuSetKey(key, true);
SDL_Delay(std::max(1, duration_ms));
simuSetKey(key, false);
automation_reply_ok();
} else if (command == "rotate") {
int steps = 0;
in >> steps;
simuRotaryEncoderEvent(steps);
automation_reply_ok();
} else if (command == "touch") {
int x = 0, y = 0, duration_ms = 120;
in >> x >> y >> duration_ms;
simuTouchDown(x, y);
SDL_Delay(std::max(1, duration_ms));
simuTouchUp();
automation_reply_ok();
} else if (command == "wait") {
int duration_ms = 0;
in >> duration_ms;
SDL_Delay(std::max(0, duration_ms));
automation_reply_ok();
} else if (command == "screenshot_ppm") {
std::string path;
in >> path;
if (path.empty()) {
automation_reply_error("missing screenshot path");
return;
}
Comment on lines +249 to +255

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle spaces in screenshot paths.

Using in >> path extracts up to the first whitespace, which will truncate paths containing spaces. Use std::getline to safely capture the full rest of the line as the path.

💚 Proposed fix
   } else if (command == "screenshot_ppm") {
     std::string path;
-    in >> path;
+    std::getline(in >> std::ws, path);
     if (path.empty()) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (command == "screenshot_ppm") {
std::string path;
in >> path;
if (path.empty()) {
automation_reply_error("missing screenshot path");
return;
}
} else if (command == "screenshot_ppm") {
std::string path;
std::getline(in >> std::ws, path);
if (path.empty()) {
automation_reply_error("missing screenshot path");
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/targets/simu/sdl_simu.cpp` around lines 249 - 255, Update the
screenshot_ppm command handling to use std::getline on the input stream instead
of formatted extraction, capturing the complete path including spaces. Preserve
the existing empty-path validation and automation_reply_error behavior.

if (!write_ppm_screenshot(path)) {
automation_reply_error("failed to write screenshot");
return;
}
std::ostringstream extra;
extra << "\"path\":\"" << json_escape(path) << "\""
<< ",\"width\":" << simuLcdGetWidth()
<< ",\"height\":" << simuLcdGetHeight()
<< ",\"depth\":" << simuLcdGetDepth();
automation_reply_ok(extra.str());
} else if (command == "stop") {
SDL_Event event;
event.type = SDL_QUIT;
SDL_PushEvent(&event);
automation_reply_ok();
} else {
automation_reply_error("unknown command: " + command);
}
}

static void automation_poll_stdin()
{
if (!automation_stdio) return;

#if defined(_WIN32)
return;
#else
timeval timeout = {0, 0};
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);

int ready = select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &timeout);
if (ready > 0 && FD_ISSET(STDIN_FILENO, &readfds)) {
std::string line;
if (std::getline(std::cin, line)) {
automation_handle_command(line);
}
}
#endif
}

Comment on lines +276 to +297

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid mixing select() with std::cin to prevent deadlocks and blocking.

std::cin uses an internal stream buffer. If multiple commands arrive in the OS pipe simultaneously, std::cin reads them into its buffer. std::getline will return the first command, but subsequent calls to select() will block (since the OS pipe is now empty), trapping the remaining commands in the buffer until new data arrives. Additionally, if only a partial line arrives, std::getline will block the main SDL event loop until a newline is received.

Use non-blocking POSIX read() into a static buffer instead.

🛠️ Proposed fix to manually buffer lines
 static void automation_poll_stdin()
 {
   if (!automation_stdio) return;
 
 `#if` defined(_WIN32)
   return;
 `#else`
   timeval timeout = {0, 0};
   fd_set readfds;
   FD_ZERO(&readfds);
   FD_SET(STDIN_FILENO, &readfds);
 
   int ready = select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &timeout);
   if (ready > 0 && FD_ISSET(STDIN_FILENO, &readfds)) {
-    std::string line;
-    if (std::getline(std::cin, line)) {
-      automation_handle_command(line);
-    }
+    static std::string buffer;
+    char chunk[256];
+    ssize_t bytes = read(STDIN_FILENO, chunk, sizeof(chunk));
+    if (bytes > 0) {
+      buffer.append(chunk, bytes);
+      size_t pos;
+      while ((pos = buffer.find('\n')) != std::string::npos) {
+        std::string line = buffer.substr(0, pos);
+        buffer.erase(0, pos + 1);
+        if (!line.empty() && line.back() == '\r') line.pop_back();
+        automation_handle_command(line);
+      }
+    }
   }
 `#endif`
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static void automation_poll_stdin()
{
if (!automation_stdio) return;
#if defined(_WIN32)
return;
#else
timeval timeout = {0, 0};
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
int ready = select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &timeout);
if (ready > 0 && FD_ISSET(STDIN_FILENO, &readfds)) {
std::string line;
if (std::getline(std::cin, line)) {
automation_handle_command(line);
}
}
#endif
}
static void automation_poll_stdin()
{
if (!automation_stdio) return;
`#if` defined(_WIN32)
return;
`#else`
timeval timeout = {0, 0};
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
int ready = select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &timeout);
if (ready > 0 && FD_ISSET(STDIN_FILENO, &readfds)) {
static std::string buffer;
char chunk[256];
ssize_t bytes = read(STDIN_FILENO, chunk, sizeof(chunk));
if (bytes > 0) {
buffer.append(chunk, bytes);
size_t pos;
while ((pos = buffer.find('\n')) != std::string::npos) {
std::string line = buffer.substr(0, pos);
buffer.erase(0, pos + 1);
if (!line.empty() && line.back() == '\r') line.pop_back();
automation_handle_command(line);
}
}
}
`#endif`
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/targets/simu/sdl_simu.cpp` around lines 276 - 297, Replace the
select()/std::cin combination in automation_poll_stdin with non-blocking POSIX
read() calls into a static buffer. Accumulate partial input across polls,
extract and pass each complete newline-terminated command to
automation_handle_command, and ensure polling never blocks the SDL event loop or
loses commands already read from the pipe.

static bool handleKeyEvents(SDL_Event& event)
{
if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) {
Expand Down Expand Up @@ -263,6 +459,7 @@ static bool handleEvents()
}

redraw();
automation_poll_stdin();
return true;
}

Expand Down Expand Up @@ -697,6 +894,8 @@ int main(int argc, char* argv[])
return 0;
}

automation_stdio = args.isAutomationStdioEnabled();

int window_height = 600;
if (args.hasHeight()) {
window_height = args.getHeight();
Expand Down Expand Up @@ -746,6 +945,9 @@ int main(int argc, char* argv[])
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
if (automation_stdio) {
ImGui::GetIO().IniFilename = nullptr;
}

// Setup Dear ImGui style
ImGui::StyleColorsLight();
Expand Down
Loading