From efbf3cc89296a78011f6c1aa85ccef925ebed2ce Mon Sep 17 00:00:00 2001 From: Niellune Date: Fri, 13 Mar 2026 04:05:24 +0200 Subject: [PATCH] Message queue for sending --- settings.txt | 14 +-- src/application.cpp | 12 +-- src/connector.cpp | 39 ++++--- src/connector.h | 12 ++- src/helper/iaudio_sender.h | 12 --- src/helper/isender.h | 15 +++ src/protocol.cpp | 204 +++++-------------------------------- src/protocol.h | 20 +--- src/recorder.cpp | 5 +- src/recorder.h | 28 +---- src/settings.h | 5 +- src/struct/atomic_queue.h | 8 ++ src/struct/audio_chunk.h | 31 ++++++ src/struct/command.h | 189 ++++++++++++++++++++++++++++++++++ 14 files changed, 326 insertions(+), 268 deletions(-) delete mode 100644 src/helper/iaudio_sender.h create mode 100644 src/helper/isender.h create mode 100644 src/struct/audio_chunk.h create mode 100644 src/struct/command.h diff --git a/settings.txt b/settings.txt index c420b94..9e5b818 100644 --- a/settings.txt +++ b/settings.txt @@ -109,6 +109,13 @@ # Corrects aspect of UI #aspect-correction = 1 +# Force SDL renderer backend, leave empty for automatic +# Examples: opengles2, opengl, software +#renderer-driver = + +# Use alternative video rendering method. May reduce CPU load and increase smoothness. +#alternative-rendering = false + # Select faster method of scaling image to window size (nearest) or better quality (linear) #fast-render-scale = false @@ -139,13 +146,6 @@ # pipewire #audio-driver = -# Force SDL renderer backend, leave empty for automatic -# Examples: opengles2, opengl, software -#renderer-driver = - -# Use alternative video rendering method. May reduce CPU load and increase smoothness. -#alternative-rendering = false - # Run script or app on phone connected and disconnected. # This script/app should be fast, otherwise it will block system. # If you need to start application in background use scripts with fork diff --git a/src/application.cpp b/src/application.cpp index d63146a..2689841 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -270,7 +270,7 @@ bool Application::processFrameEvents(Protocol &protocol, Renderer &renderer) int key = processKey(e.key.keysym); if (key > 0) { - protocol.sendKey(key); + protocol.send(Command::Control(key)); result = true; } break; @@ -279,7 +279,7 @@ bool Application::processFrameEvents(Protocol &protocol, Renderer &renderer) { if (e.key.keysym.sym == Settings::keyEnter) { - protocol.sendKey(Settings::keyEnterUp.key); + protocol.send(Command::Control(Settings::keyEnterUp.key)); result = true; } break; @@ -290,11 +290,11 @@ bool Application::processFrameEvents(Protocol &protocol, Renderer &renderer) if (_state.frameRendered && (downX >= 0 || upX >= 0 || motionX >= 0)) { if (downX >= 0) - protocol.sendClick(renderer.xScale * downX / _width, renderer.yScale * downY / _height, true); + protocol.send(Command::Click(renderer.xScale * downX / _width, renderer.yScale * downY / _height, true)); if (motionX >= 0) - protocol.sendMove(renderer.xScale * motionX / _width, renderer.yScale * motionY / _height); + protocol.send(Command::Move(renderer.xScale * motionX / _width, renderer.yScale * motionY / _height)); if (upX >= 0) - protocol.sendClick(renderer.xScale * upX / _width, renderer.yScale * upY / _height, false); + protocol.send(Command::Click(renderer.xScale * upX / _width, renderer.yScale * upY / _height, false)); } return result; @@ -358,7 +358,7 @@ void Application::loop() { if (latestFrameid == requestFrameId) { - protocol.requestKeyframe(); + protocol.send(Command::Control(BTN_SCREEN_REFRESH)); _state.requestFrame = 1; requestFrameId = latestFrameid; } diff --git a/src/connector.cpp b/src/connector.cpp index 59bf080..5118d9d 100644 --- a/src/connector.cpp +++ b/src/connector.cpp @@ -30,6 +30,7 @@ Connector::Connector(uint16_t videoPadding) Connector::~Connector() { _active = false; + _queue.notify(); if (_write_thread.joinable()) _write_thread.join(); @@ -69,6 +70,7 @@ void Connector::stop() return; _active = false; + _queue.notify(); state(PROTOCOL_STATUS_INITIALISING); @@ -181,7 +183,15 @@ bool Connector::linkFail(int status, const char *msg) return true; } -int Connector::send(int cmd, bool encrypt, uint8_t *data, uint32_t size) +bool Connector::send(std::unique_ptr packet) +{ + if (!_connected || !packet) + return false; + + return _queue.pushDiscard(std::move(packet)); +} + +int Connector::write(int cmd, bool encrypt, uint8_t *data, uint32_t size) { if (!_connected) return 0; @@ -380,9 +390,6 @@ void Connector::readLoop() void Connector::writeLoop() { - std::mutex mtx; - std::condition_variable cv; - // Set thread name setThreadName("protocol-writer"); state(PROTOCOL_STATUS_LINKING); @@ -400,10 +407,18 @@ void Connector::writeLoop() state(PROTOCOL_STATUS_ONLINE); while (_connected && _active) { - send(170); - std::unique_lock lock(mtx); - cv.wait_for(lock, std::chrono::seconds(2), [&]() - { return !_active.load(); }); + std::unique_ptr message = _queue.pop(); + if (!message) + { + if (!_queue.waitFor(_active, 2000)) + break; + message = _queue.pop(); + } + + if (message) + write(message->command, message->encryption, message->data, message->length); + else + write(CMD_HEARTBEAT, false, nullptr, 0); } if (_active) @@ -412,11 +427,11 @@ void Connector::writeLoop() onDevice(false); } + _queue.clear(); + if (_read_thread.joinable()) _read_thread.join(); } - std::unique_lock lock(mtx); - cv.wait_for(lock, std::chrono::seconds(1), [&]() - { return !_active.load(); }); + _queue.waitFor(_active, 1000); } -} \ No newline at end of file +} diff --git a/src/connector.h b/src/connector.h index 05d3af1..fb555b2 100644 --- a/src/connector.h +++ b/src/connector.h @@ -8,7 +8,10 @@ #include #include +#include "helper/isender.h" #include "aes_cipher.h" +#include "struct/atomic_queue.h" +#include "struct/command.h" #define READ_TIMEOUT 3000 #define ENCRYPTION_BASE "SkBRDy3gmrw1ieH0" @@ -29,7 +32,7 @@ struct Header }; #pragma pack(pop) -class Connector +class Connector : public ISender { public: @@ -38,10 +41,9 @@ public: void start(); void stop(); + bool send(std::unique_ptr packet) override; protected: - - int send(int cmd, bool encrypt = true, uint8_t *data = nullptr, uint32_t size = 0); virtual void onData(uint32_t cmd, uint32_t length, uint8_t *data) = 0; virtual void onStatus(u_int8_t status) = 0; virtual void onDevice(bool connected) = 0; @@ -66,12 +68,13 @@ private: void state(u_int8_t state); bool nextState(u_int8_t state); bool linkFail(int status, const char *msg); + int write(int cmd, bool encrypt, uint8_t *data, uint32_t size); libusb_context *_context = nullptr; libusb_device_handle *_device = nullptr; uint8_t _endpoint_in; uint8_t _endpoint_out; - bool _connected; + std::atomic _connected = false; std::atomic _ecnrypt = false; uint8_t _state; @@ -83,6 +86,7 @@ private: std::thread _write_thread; std::mutex _write_mutex; std::atomic _active = false; + AtomicQueue _queue{256}; u_int16_t _videoPadding; }; diff --git a/src/helper/iaudio_sender.h b/src/helper/iaudio_sender.h deleted file mode 100644 index 63770b5..0000000 --- a/src/helper/iaudio_sender.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SRC_HELPER_IAUDIO_SENDER -#define SRC_HELPER_IAUDIO_SENDER - -#include - -class IAudioSender { -public: - virtual ~IAudioSender() = default; - virtual void sendAudio(uint8_t* data, uint32_t length) = 0; -}; - -#endif /* SRC_HELPER_IAUDIO_SENDER */ diff --git a/src/helper/isender.h b/src/helper/isender.h new file mode 100644 index 0000000..1a8a5fb --- /dev/null +++ b/src/helper/isender.h @@ -0,0 +1,15 @@ +#ifndef SRC_HELPER_ISENDER +#define SRC_HELPER_ISENDER + +#include + +#include "struct/command.h" + +class ISender +{ +public: + virtual ~ISender() = default; + virtual bool send(std::unique_ptr packet) = 0; +}; + +#endif /* SRC_HELPER_ISENDER */ diff --git a/src/protocol.cpp b/src/protocol.cpp index bdb93ba..2adb6fa 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -15,7 +15,6 @@ Protocol::Protocol(uint16_t width, uint16_t height, uint16_t fps, uint16_t paddi _width(width), _height(height), _fps(fps), - _keySent(false), _phoneConnected(false) { } @@ -37,20 +36,6 @@ void Protocol::stop() Connector::stop(); } -void Protocol::sendInit(int width, int height, int fps) -{ - uint8_t buf[28]; - write_uint32_le(&buf[0], width); - write_uint32_le(&buf[4], height); - write_uint32_le(&buf[8], fps); - write_uint32_le(&buf[12], 5); - write_uint32_le(&buf[16], 49152); - write_uint32_le(&buf[20], 2); - write_uint32_le(&buf[24], 2); - - send(CMD_OPEN, true, buf, 28); -} - void Protocol::sendConfig() { int syncTime = std::time(nullptr); @@ -91,171 +76,25 @@ void Protocol::sendConfig() std::cout << "[Protocol] Request android image " << width << "x" << height << std::endl; - char buffer[512]; - snprintf(buffer, sizeof(buffer), - "{\"syncTime\":%d,\"mediaDelay\":%d,\"drivePosition\":%d," - "\"androidAutoSizeW\":%d,\"androidAutoSizeH\":%d,\"HiCarConnectMode\":0," - "\"GNSSCapability\":7,\"DashboardInfo\":1,\"UseBTPhone\":0}", - syncTime, Settings::mediaDelay.value, drivePosition, width, height); + send(Command::String( + CMD_JSON_CONTROL, + "{\"syncTime\":%d,\"mediaDelay\":%d,\"drivePosition\":%d," + "\"androidAutoSizeW\":%d,\"androidAutoSizeH\":%d,\"HiCarConnectMode\":0," + "\"GNSSCapability\":7,\"DashboardInfo\":1,\"UseBTPhone\":0}", + syncTime, Settings::mediaDelay.value, drivePosition, width, height)); - sendString(CMD_JSON_CONTROL, buffer); + send(Command::String(CMD_DAYNIGHT, "{\"DayNightMode\":%d}", nightMode)); - snprintf(buffer, sizeof(buffer), "{\"DayNightMode\":%d}", nightMode); - sendString(CMD_DAYNIGHT, buffer); + send(Command::File("/tmp/night_mode", nightMode)); + send(Command::File("/tmp/charge_mode", Settings::weakCharge ? 0 : 2)); // Weak charge 0, other 2 + send(Command::File("/etc/box_name", "CarPlay")); + send(Command::File("/tmp/hand_drive_mode", drivePosition)); - sendFile("/tmp/night_mode", nightMode); - sendFile("/tmp/charge_mode", Settings::weakCharge ? 0 : 2); // Weak charge 0, other 2 - sendFile("/etc/box_name", "CarPlay"); - sendFile("/tmp/hand_drive_mode", drivePosition); - - sendInt(CMD_CONTROL, mic); - sendInt(CMD_CONTROL, Settings::wifi5 ? 25 : 24); - sendInt(CMD_CONTROL, Settings::bluetoothAudio ? 22 : 23); + send(Command::Control(mic)); + send(Command::Control(Settings::wifi5 ? 25 : 24)); + send(Command::Control(Settings::bluetoothAudio ? 22 : 23)); if (Settings::autoconnect) - sendInt(CMD_CONTROL, 1002); -} - -bool Protocol::checkKey() -{ - bool result = _keySent; - _keySent = false; - return result; -} - -void Protocol::sendKey(int key) -{ - sendInt(CMD_CONTROL, key, false); - _keySent = true; -} - -void Protocol::requestKeyframe() -{ - sendInt(CMD_CONTROL, BTN_SCREEN_REFRESH, false); -} - -void Protocol::sendFile(const char *filename, const char *value) -{ - uint32_t len = strlen(value); - if (len > 16) - { - throw std::invalid_argument("String too long (max 16 bytes)"); - } - - // note: we send only the ASCII bytes, no trailing '\0' - sendFile(filename, - reinterpret_cast(value), - static_cast(len)); -} - -// overload for a single 32‑bit integer -void Protocol::sendFile(const char *filename, int value) -{ - uint8_t buf[4]; - write_uint32_le(buf, value); - sendFile(filename, buf, 4); -} - -void Protocol::sendClick(float x, float y, bool down) -{ - uint8_t buf[16]; - write_uint32_le(buf, down ? 14 : 16); - write_uint32_le(buf + 4, int(10000 * x)); - write_uint32_le(buf + 8, int(10000 * y)); - write_uint32_le(buf + 12, 0); - send(CMD_TOUCH, false, buf, 16); -} - -void Protocol::sendMultiTouch(const Multitouch &touches) -{ - int count = touches.size(); - if (count == 0) - return; - - uint8_t buf[MUTLITOUCH_MAX_TOUCH * sizeof(Multitouch::Touch)]; - uint8_t *p = buf; - - for (int i = 0; i < count; ++i) - { - const Multitouch::Touch &t = touches[i]; - write_float_le(p + 0, t.x); - write_float_le(p + 4, t.y); - write_uint32_le(p + 8, static_cast(t.state)); - write_uint32_le(p + 12, static_cast(t.id)); - p += 16; - } - - send(CMD_MULTI_TOUCH, false, buf, 16 * count); -} - -void Protocol::sendMove(float dx, float dy) -{ - uint8_t buf[16]; - write_uint32_le(buf, 15); - write_uint32_le(buf + 4, int(10000 * dx)); - write_uint32_le(buf + 8, int(10000 * dy)); - write_uint32_le(buf + 12, 0); - send(CMD_TOUCH, false, buf, 16); -} - -void Protocol::sendAudio(uint8_t *data, uint32_t length) -{ - write_uint32_le(data, 5); - write_uint32_le(data + 4, 0); - write_uint32_le(data + 8, 3); - send(CMD_AUDIO_DATA, false, data, length); -} - -void Protocol::sendFile(const char *filename, const uint8_t *data, uint32_t length) -{ - // filename is assumed null‑terminated, so strlen + 1 to include the '\0' - uint32_t fn_len = strlen(filename) + 1; - - // Total buffer size: 4 (fn_len) + fn_len + 4 (content_len) + content_len - uint32_t total = 4 + fn_len + 4 + length; - std::vector result(total); - uint8_t *buf = result.data(); - - // 1) filename length (LE) - write_uint32_le(buf, fn_len); - buf += 4; - - // 2) filename bytes (including the '\0') - std::memcpy(buf, filename, fn_len); - buf += fn_len; - - // 3) content length (LE) - write_uint32_le(buf, length); - buf += 4; - - // 4) content bytes - std::memcpy(buf, data, length); - - send(CMD_SEND_FILE, true, result.data(), total); -} - -void Protocol::sendInt(uint32_t cmd, uint32_t value, bool encryption) -{ - uint8_t buf[4]; - write_uint32_le(buf, value); - send(cmd, encryption, buf, 4); -} - -void Protocol::sendString(uint32_t cmd, char *str, bool encryption) -{ - uint32_t total = strlen(str); - send(cmd, true, (uint8_t *)str, total); -} - -void Protocol::sendEncryption() -{ - if (!_cipher) - { - std::cout << "[Protocol] Can't enable encryption: cypher is not initalised" << std::endl; - return; - } - uint8_t buf[4]; - write_uint32_le(buf, _cipher->Seed()); - send(CMD_ENCRYPTION, false, buf, 4); + send(Command::Control(1002)); } void Protocol::onStatus(uint8_t status) @@ -269,11 +108,16 @@ void Protocol::onDevice(bool connected) if (connected) { if (Settings::encryption) - sendEncryption(); + { + if (_cipher) + send(Command::Encryption(_cipher->Seed())); + else + std::cout << "[Protocol] Can't enable encryption: cypher is not initalised" << std::endl; + } if (Settings::dpi > 0) - sendFile("/tmp/screen_dpi", Settings::dpi); - sendFile("/etc/android_work_mode", 1); - sendInit(_width, _height, _fps); + send(Command::File("/tmp/screen_dpi", Settings::dpi)); + send(Command::File("/etc/android_work_mode", 1)); + send(Command::Init(_width, _height, _fps)); sendConfig(); } else diff --git a/src/protocol.h b/src/protocol.h index a8d488c..45b8c4c 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -4,12 +4,11 @@ #include "struct/atomic_queue.h" #include "struct/message.h" #include "struct/multitouch.h" -#include "helper/iaudio_sender.h" #include "settings.h" #include "connector.h" #include "recorder.h" -class Protocol : private Connector, public IAudioSender +class Protocol : public Connector { public: @@ -22,27 +21,11 @@ public: void start(uint32_t evtStatus, uint32_t evtPhone); void stop(); - - bool checkKey(); - void sendKey(int key); - void requestKeyframe(); - void sendFile(const char *filename, const uint8_t *data, uint32_t length); - void sendFile(const char *filename, const char *value); - void sendFile(const char *filename, int value); - void sendClick(float x, float y, bool down); - void sendMultiTouch(const Multitouch &touches); - void sendMove(float dx, float dy); - void sendAudio(uint8_t *data, uint32_t length) override; - AtomicQueue videoData; AtomicQueue audioStreamMain; AtomicQueue audioStreamAux; private: - void sendInt(uint32_t cmd, uint32_t value, bool encryption = true); - void sendString(uint32_t cmd, char *str, bool encryption = true); - void sendEncryption(); - void sendInit(int width, int height, int fps); void sendConfig(); void onStatus(uint8_t status) override; @@ -57,7 +40,6 @@ private: uint16_t _width; uint16_t _height; uint16_t _fps; - bool _keySent; bool _phoneConnected; uint32_t _evtStatusId = (uint32_t)-1; diff --git a/src/recorder.cpp b/src/recorder.cpp index 9c5ed6f..bf405d6 100644 --- a/src/recorder.cpp +++ b/src/recorder.cpp @@ -6,6 +6,7 @@ #include "helper/protocol_const.h" #include "helper/functions.h" #include "settings.h" +#include "struct/command.h" Recorder::Recorder(uint16_t buffSize) @@ -20,7 +21,7 @@ Recorder::~Recorder() _thread.join(); } -void Recorder::start(IAudioSender *sender) +void Recorder::start(ISender *sender) { if (_active) return; @@ -78,7 +79,7 @@ void Recorder::runner() { std::unique_ptr buffer = _data.pop(); if (buffer && _sender) - _sender->sendAudio(buffer.get()->data, buffer.get()->size); + _sender->send(Command::Audio(std::move(buffer))); else if (_active) std::this_thread::sleep_for(std::chrono::milliseconds(5)); diff --git a/src/recorder.h b/src/recorder.h index 1dab9f1..4ed5d2d 100644 --- a/src/recorder.h +++ b/src/recorder.h @@ -6,44 +6,24 @@ #include -#include "helper/iaudio_sender.h" +#include "helper/isender.h" +#include "struct/audio_chunk.h" #include "struct/atomic_queue.h" -class AudioChunk -{ -public: - AudioChunk(uint16_t size) - : data(new uint8_t[size]), size(size) - { - } - - ~AudioChunk() - { - delete[] data; - } - - // Deleted copy constructor/assignment - AudioChunk(const AudioChunk &) = delete; - AudioChunk &operator=(const AudioChunk &) = delete; - - uint8_t *data; - size_t size; -}; - class Recorder { public: Recorder(uint16_t buffSize); ~Recorder(); - void start(IAudioSender *sender); + void start(ISender *sender); void stop(); private: static void AudioCallback(void *userdata, Uint8 *stream, int len); void runner(); - IAudioSender *_sender; + ISender *_sender; std::atomic _active; std::thread _thread; AtomicQueue _data; diff --git a/src/settings.h b/src/settings.h index 988227f..34fc377 100644 --- a/src/settings.h +++ b/src/settings.h @@ -41,8 +41,9 @@ public: static inline Setting hwDecode{"hw-decode", true}; static inline Setting forceRedraw{"force-redraw", 0}; static inline Setting aspectCorrection{"aspect-correction", 1}; + static inline Setting renderDriver{"renderer-driver", ""}; + static inline Setting alternativeRendering{"alternative-rendering", false}; static inline Setting fastScale{"fast-render-scale", false}; - static inline Setting alternativeRendering{"alternative-rendering", false}; static inline Setting videoQueue{"video-buffer-size", 32}; static inline Setting audioQueue{"audio-buffer-size", 16}; static inline Setting audioDelay{"audio-buffer-wait", 2}; @@ -50,7 +51,7 @@ public: static inline Setting audioFade{"audio-fade", 0.3}; static inline Setting audioBuffer{"audio-buffer-samples", 2048}; static inline Setting audioDriver{"audio-driver", ""}; - static inline Setting renderDriver{"renderer-driver", "auto"}; + static inline Setting onConnect{"on-connect-script", ""}; static inline Setting onDisconnect{"on-disconnect-script", ""}; diff --git a/src/struct/atomic_queue.h b/src/struct/atomic_queue.h index 1b79df2..2b42b8b 100644 --- a/src/struct/atomic_queue.h +++ b/src/struct/atomic_queue.h @@ -82,6 +82,14 @@ public: return waitFlag.load(); } + bool waitFor(atomic &waitFlag, uint32_t timeoutMs, uint8_t count = 0) + { + unique_lock lock(_mtx); + _lock.wait_for(lock, std::chrono::milliseconds(timeoutMs), [&] + { return _count > count || !waitFlag.load(); }); + return waitFlag.load(); + } + void clear() { _data = std::make_unique[]>(_size); diff --git a/src/struct/audio_chunk.h b/src/struct/audio_chunk.h new file mode 100644 index 0000000..f8107bb --- /dev/null +++ b/src/struct/audio_chunk.h @@ -0,0 +1,31 @@ +#ifndef SRC_STRUCT_AUDIO_CHUNK +#define SRC_STRUCT_AUDIO_CHUNK + +#include +#include +#include + +class AudioChunk +{ +public: + AudioChunk(uint16_t size) + : data(nullptr), size(size) + { + if (size > 0) + data = static_cast(malloc(size)); + } + + ~AudioChunk() + { + free(data); + } + + // Deleted copy constructor/assignment + AudioChunk(const AudioChunk &) = delete; + AudioChunk &operator=(const AudioChunk &) = delete; + + uint8_t *data; + uint16_t size; +}; + +#endif /* SRC_STRUCT_AUDIO_CHUNK */ \ No newline at end of file diff --git a/src/struct/command.h b/src/struct/command.h new file mode 100644 index 0000000..347d8f5 --- /dev/null +++ b/src/struct/command.h @@ -0,0 +1,189 @@ +#ifndef SRC_STRUCT_COMMAND +#define SRC_STRUCT_COMMAND + +#include +#include +#include +#include +#include + +#include "helper/functions.h" +#include "helper/protocol_const.h" +#include "struct/audio_chunk.h" +#include "struct/multitouch.h" + +class Command +{ +public: + Command(const Command &) = delete; + Command &operator=(const Command &) = delete; + + Command(int cmd, bool encrypt = true, uint32_t size = 0) + : command(cmd), encryption(encrypt), length(size), data(nullptr) + { + if (size > 0) + data = static_cast(malloc(size)); + } + + Command(int cmd, uint32_t value, bool encrypt = true) + : Command(cmd, encrypt, 4) + { + write_uint32_le(data, value); + } + + Command(int cmd, bool encrypt, uint8_t *buffer, uint32_t size) + : command(cmd), encryption(encrypt), length(size), data(buffer) + { + } + + ~Command() + { + if (data) + { + free(data); + data = nullptr; + } + } + + static std::unique_ptr Init(int width, int height, int fps) + { + std::unique_ptr result(new Command(CMD_OPEN, true, 28)); + write_uint32_le(result->data + 0, width); + write_uint32_le(result->data + 4, height); + write_uint32_le(result->data + 8, fps); + write_uint32_le(result->data + 12, 5); + write_uint32_le(result->data + 16, 49152); + write_uint32_le(result->data + 20, 2); + write_uint32_le(result->data + 24, 2); + return result; + } + + static std::unique_ptr File(const char *filename, const uint8_t *data, uint32_t length) + { + // filename is assumed null‑terminated, so strlen + 1 to include the '\0' + uint32_t fn_len = strlen(filename) + 1; + + // Total buffer size: 4 (fn_len) + fn_len + 4 (content_len) + content_len + std::unique_ptr result(new Command(CMD_SEND_FILE, true, 4 + fn_len + 4 + length)); + uint8_t *buf = result->data; + + // 1) filename length (LE) + write_uint32_le(buf, fn_len); + buf += 4; + + // 2) filename bytes (including the '\0') + std::memcpy(buf, filename, fn_len); + buf += fn_len; + + // 3) content length (LE) + write_uint32_le(buf, length); + buf += 4; + + // 4) content bytes + if (length > 0 && data) + std::memcpy(buf, data, length); + + return result; + } + + static std::unique_ptr File(const char *filename, const char *value) + { + uint32_t len = std::strlen(value); + if (len > 16) + throw std::invalid_argument("String too long (max 16 bytes)"); + // note: we send only the ASCII bytes, no trailing '\0' + return File(filename, reinterpret_cast(value), len); + } + + // overload for a single 32‑bit integer + static std::unique_ptr File(const char *filename, int value) + { + uint8_t buffer[4]; + write_uint32_le(buffer, value); + return File(filename, buffer, 4); + } + + static std::unique_ptr Control(uint32_t value, bool encrypt = false) + { + return std::unique_ptr(new Command(CMD_CONTROL, value, encrypt)); + } + + static std::unique_ptr Encryption(uint32_t seed) + { + return std::unique_ptr(new Command(CMD_ENCRYPTION, seed, false)); + } + + static std::unique_ptr String(uint32_t cmd, const char *str, bool encrypt = true) + { + uint32_t length = std::strlen(str); + std::unique_ptr result(new Command(cmd, encrypt, length)); + if (length > 0) + std::memcpy(result->data, str, length); + return result; + } + + template + static std::unique_ptr String(uint32_t cmd, const char *format, Args... args) + { + char buffer[512]; + std::snprintf(buffer, sizeof(buffer), format, args...); + return String(cmd, buffer); + } + + static std::unique_ptr Touch(uint32_t action, float x, float y) + { + std::unique_ptr result(new Command(CMD_TOUCH, false, 16)); + write_uint32_le(result->data, action); + write_uint32_le(result->data + 4, int(10000 * x)); + write_uint32_le(result->data + 8, int(10000 * y)); + write_uint32_le(result->data + 12, 0); + return result; + } + + static std::unique_ptr Click(float x, float y, bool down) + { + return Touch(down ? 14 : 16, x, y); + } + + static std::unique_ptr Move(float x, float y) + { + return Touch(15, x, y); + } + + static std::unique_ptr Audio(std::unique_ptr chunk) + { + std::unique_ptr result(new Command(CMD_AUDIO_DATA, false, chunk->data, chunk->size)); + chunk->data = nullptr; + write_uint32_le(result->data, 5); + write_uint32_le(result->data + 4, 0); + write_uint32_le(result->data + 8, 3); + return result; + } + + static std::unique_ptr MultiTouch(const Multitouch &touches) + { + int count = touches.size(); + if (count == 0) + return nullptr; + + std::unique_ptr result(new Command(CMD_MULTI_TOUCH, false, sizeof(Multitouch::Touch) * count)); + uint8_t *buf = result->data; + for (int i = 0; i < count; ++i) + { + const Multitouch::Touch &t = touches[i]; + write_float_le(buf + 0, t.x); + write_float_le(buf + 4, t.y); + write_uint32_le(buf + 8, static_cast(t.state)); + write_uint32_le(buf + 12, static_cast(t.id)); + buf += 16; + } + return result; + } + + int command; + bool encryption; + uint32_t length; + uint8_t *data; +}; + +#endif /* SRC_STRUCT_COMMAND */