From 6f4d23868cd9e74c5cbc730e0d714cc885606f4c Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Tue, 6 Jan 2026 19:50:45 +0800 Subject: [PATCH] write shit --- BasaltPresenter/Presenter/CMakeLists.txt | 2 + BasaltPresenter/Presenter/command_client.cpp | 78 +++++++++++++ BasaltPresenter/Presenter/command_client.hpp | 38 +++++++ BasaltTrainer/command_server.py | 113 +++++++++++++++++++ BasaltTrainer/pipe_operator.py | 50 +------- Document/PROTOCOL.md | 19 +++- 6 files changed, 252 insertions(+), 48 deletions(-) create mode 100644 BasaltPresenter/Presenter/command_client.cpp create mode 100644 BasaltPresenter/Presenter/command_client.hpp create mode 100644 BasaltTrainer/command_server.py diff --git a/BasaltPresenter/Presenter/CMakeLists.txt b/BasaltPresenter/Presenter/CMakeLists.txt index 6ad7e8d..604a0c3 100644 --- a/BasaltPresenter/Presenter/CMakeLists.txt +++ b/BasaltPresenter/Presenter/CMakeLists.txt @@ -3,12 +3,14 @@ target_sources(BasaltPresenter PRIVATE main.cpp dll_loader.cpp + command_client.cpp ) target_sources(BasaltPresenter PUBLIC FILE_SET HEADERS FILES dll_loader.hpp + command_client.hpp ) target_include_directories(BasaltPresenter PUBLIC diff --git a/BasaltPresenter/Presenter/command_client.cpp b/BasaltPresenter/Presenter/command_client.cpp new file mode 100644 index 0000000..83a48c7 --- /dev/null +++ b/BasaltPresenter/Presenter/command_client.cpp @@ -0,0 +1,78 @@ +#include "command_client.hpp" +#include + +namespace Basalt::Presenter { + + CommandClient::CommandClient(const std::basic_string_view pipe_name) + : m_PipeOperator(pipe_name) { + } + + CommandClient::~CommandClient() { + } + + void CommandClient::WaitHandshake(std::uint8_t pixel_kind, std::uint32_t width, std::uint32_t height) { + if (m_Handshaked) { + throw std::runtime_error("Handshake already completed"); + } + + // Wait for handshake request from Trainer (code 0x61) + std::uint8_t received_code; + m_PipeOperator.Read(&received_code, sizeof(received_code)); + + if (received_code != HANDSHAKE_CODE_REQUEST) { + throw std::runtime_error("Expected handshake code 0x61, got 0x" + + std::to_string(static_cast(received_code))); + } + + // Send handshake response (code 0x62) back to Trainer + std::uint8_t handshake_response = HANDSHAKE_CODE_RESPONSE; + m_PipeOperator.Write(&handshake_response, sizeof(handshake_response)); + + // Send data properties after handshake + m_PipeOperator.Write(&pixel_kind, sizeof(pixel_kind)); + m_PipeOperator.Write(&width, sizeof(width)); + m_PipeOperator.Write(&height, sizeof(height)); + + m_Handshaked = true; + } + + bool CommandClient::Tick(bool actively_stop) { + if (!m_Handshaked) { + throw std::runtime_error("Handshake must be completed before calling Tick"); + } + + // If actively_stop is true, send actively stop code to Trainer + if (actively_stop) { + std::uint8_t stop_code = ACTIVELY_STOP_CODE; + m_PipeOperator.Write(&stop_code, sizeof(stop_code)); + } + + // Send data ready code to Trainer + std::uint8_t data_ready_code = DATA_READY_CODE; + m_PipeOperator.Write(&data_ready_code, sizeof(data_ready_code)); + + // Wait for response from Trainer + std::uint8_t received_code; + m_PipeOperator.Read(&received_code, sizeof(received_code)); + + // Handle the received code + if (received_code == DATA_RECEIVED_CODE) { + // Normal response, continue processing + return false; // Not stopping + } else if (received_code == STOP_CODE) { + // Trainer wants to stop + return true; // Should stop + } else if (received_code == HANDSHAKE_CODE_REQUEST || received_code == HANDSHAKE_CODE_RESPONSE) { + // Unexpected handshake code during Tick + throw std::runtime_error("Unexpected handshake code 0x" + + std::to_string(static_cast(received_code)) + + " received during Tick operation"); + } else { + // Unknown code + throw std::runtime_error("Unknown code 0x" + + std::to_string(static_cast(received_code)) + + " received during Tick operation"); + } + } + +} // namespace Basalt::Presenter \ No newline at end of file diff --git a/BasaltPresenter/Presenter/command_client.hpp b/BasaltPresenter/Presenter/command_client.hpp new file mode 100644 index 0000000..2530ffe --- /dev/null +++ b/BasaltPresenter/Presenter/command_client.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace Basalt::Presenter { + + class CommandClient { + public: + // Protocol codes + static constexpr std::uint8_t HANDSHAKE_CODE_REQUEST = 0x61; // Trainer -> Presenter + static constexpr std::uint8_t HANDSHAKE_CODE_RESPONSE = 0x62; // Presenter -> Trainer + static constexpr std::uint8_t DATA_READY_CODE = 0x01; // Presenter -> Trainer + static constexpr std::uint8_t DATA_RECEIVED_CODE = 0x02; // Trainer -> Presenter + static constexpr std::uint8_t ACTIVELY_STOP_CODE = 0x71; // Both directions + static constexpr std::uint8_t STOP_CODE = 0x71; // Both directions (same code) + + // Pixel kind values + static constexpr std::uint8_t PIXEL_GRAY_FLOAT32 = 0x01; // Grayscale represented by one float32 + static constexpr std::uint8_t PIXEL_GRAY_U8 = 0x02; // Grayscale represented by one u8 + static constexpr std::uint8_t PIXEL_RGB_FLOAT32 = 0x03; // RGB represented by three float32 + static constexpr std::uint8_t PIXEL_RGB_U8 = 0x04; // RGB represented by three u8 + + CommandClient(const std::basic_string_view pipe_name = BSTEXT("ed0e3f1f-d214-4880-9562-640bce15e72e")); + ~CommandClient(); + + // Wait for handshake from Trainer, send response with data properties + void WaitHandshake(std::uint8_t pixel_kind, std::uint32_t width, std::uint32_t height); + + // Tick function called every frame to send data ready and wait for response + bool Tick(bool actively_stop = false); + + private: + Basalt::Shared::PipeOperator m_PipeOperator; + bool m_Handshaked = false; // Track handshake state + }; + +} // namespace Basalt::Presenter \ No newline at end of file diff --git a/BasaltTrainer/command_server.py b/BasaltTrainer/command_server.py new file mode 100644 index 0000000..5045bcb --- /dev/null +++ b/BasaltTrainer/command_server.py @@ -0,0 +1,113 @@ +import struct +from enum import IntEnum, Enum, auto +from dataclasses import dataclass +from typing import Tuple, Optional +from pipe_operator import PipeOperator + +PIPE_NAME: str = "ed0e3f1f-d214-4880-9562-640bce15e72e" + +class ProtocolCode(IntEnum): + HANDSHAKE_REQUEST = 0x61 # Trainer -> Presenter + HANDSHAKE_RESPONSE = 0x62 # Presenter -> Trainer + DATA_READY = 0x01 # Presenter -> Trainer + DATA_RECEIVED = 0x02 # Trainer -> Presenter + REQUEST_STOP = 0x71 # Presenter -> Trainer (request stop) + STOP = 0x71 # Trainer -> Presenter (confirm stop) + +class PixelKind(IntEnum): + GRAY_FLOAT32 = 0x01 # Grayscale represented by one float32 + GRAY_U8 = 0x02 # Grayscale represented by one u8 + RGB_FLOAT32 = 0x03 # RGB represented by three float32 + RGB_U8 = 0x04 # RGB represented by three u8 + +@dataclass +class ImageProperties: + pixel_kind: PixelKind + width: int + height: int + +class ServerStatus(Enum): + Ready = auto() + Running = auto() + Stop = auto() + + +CODE_PACKER: struct.Struct = struct.Struct("=B") +HANDSHAKE_RESPONSE_PACKER: struct.Struct = struct.Struct("=BII") + +class CommandServer: + """ + Command server implementation for the Trainer side according to the protocol. + """ + + pipe_operator: PipeOperator + statue: ServerStatus + + def __init__(self): + self.pipe_operator = PipeOperator(PIPE_NAME) + self.handshaked = ServerStatus.Ready + + def __del__(self): + """Cleanup resources when object is destroyed.""" + self.pipe_operator.close() + + def wait_handshake(self) -> ImageProperties: + """ + Wait for handshake from Presenter, send request first and wait for response with data properties. + Returns a tuple of (pixel_kind, width, height) from the Presenter. + """ + if self.statue != ServerStatus.Ready: + raise RuntimeError("unexpected server status") + + # Send handshake request to Presenter (code 0x61) + self.pipe_operator.write(CODE_PACKER.pack(ProtocolCode.HANDSHAKE_REQUEST)) + + # Wait for handshake response from Presenter (code 0x62) + code_bytes = self.pipe_operator.read(CODE_PACKER.size) + (code, ) = CODE_PACKER.unpack(code_bytes) + if ProtocolCode(code) != ProtocolCode.HANDSHAKE_RESPONSE: + raise RuntimeError("expect handshake response code, but got another") + + # Read data properties from Presenter (pixel_kind, width, height) + handshake_response_payload = self.pipe_operator.read(HANDSHAKE_RESPONSE_PACKER.size) + (raw_pixel_kind, width, height) = HANDSHAKE_RESPONSE_PACKER.unpack(handshake_response_payload) + + # Set status and return + self.statue = ServerStatus.Running + return ImageProperties(PixelKind(raw_pixel_kind), width, height) + + def tick(self, request_stop: bool) -> bool: + """ + Tick function called every frame to wait for data ready from Presenter and send response. + Returns True if a stop code was received (meaning the process should stop), False otherwise. + """ + if self.statue != ServerStatus.Running: + raise RuntimeError("unexpected server status") + + # If there is stop requested, we post it first and return + if request_stop: + self.pipe_operator.write(CODE_PACKER.pack(ProtocolCode.STOP)) + self.statue = ServerStatus.Stop + return True + + # Wait for code from Presenter + code_bytes = self.pipe_operator.read(CODE_PACKER.size) + (code, ) = CODE_PACKER.unpack(code_bytes) + + # Analyse code + match ProtocolCode(code): + case ProtocolCode.DATA_READY: + # Receive data + print('Data received') + # Send data received symbol + self.pipe_operator.write(CODE_PACKER.pack(ProtocolCode.DATA_RECEIVED)) + case ProtocolCode.REQUEST_STOP: + # Presenter requested stop. + # Agree with it, send code and return. + self.pipe_operator.write(CODE_PACKER.pack(ProtocolCode.STOP)) + self.statue = ServerStatus.Stop + return True + case _: + raise RuntimeError("unexpected protocol code when running") + + return False diff --git a/BasaltTrainer/pipe_operator.py b/BasaltTrainer/pipe_operator.py index 11dfcd1..1cc9deb 100644 --- a/BasaltTrainer/pipe_operator.py +++ b/BasaltTrainer/pipe_operator.py @@ -51,6 +51,10 @@ class PipeOperator: else: # POSIX implementation self.pipe_name = f"/tmp/{name}" + # If there is an existing FIFO file, remove it + if os.path.exists(self.pipe_name): + os.unlink(self.pipe_name) + # Create pipe try: os.mkfifo(self.pipe_name) except OSError as e: @@ -155,49 +159,3 @@ class PipeOperator: except OSError as e: raise RuntimeError(f"Failed to write to named pipe: {e}") - -# class PipeServer: -# """ -# A convenience class for creating a pipe server that can handle connections in a separate thread. -# """ - -# def __init__(self, name: str): -# self._name = name -# self._pipe_operator: Optional[PipeOperator] = None -# self._server_thread: Optional[threading.Thread] = None -# self._stop_event = threading.Event() - -# def start_server(self, handler_func): -# """ -# Start the pipe server in a separate thread. - -# :param handler_func: Function to handle the connected pipe (takes PipeOperator as parameter) -# """ -# self._server_thread = threading.Thread(target=self._server_worker, -# args=(handler_func, )) -# self._server_thread.start() - -# def _server_worker(self, handler_func): -# """Internal method to handle server operations.""" -# try: -# # Create pipe server -# pipe_op = PipeOperator(self._name, is_server=True) -# self._pipe_operator = pipe_op - -# # Call the handler function with the connected pipe -# handler_func(pipe_op) -# except Exception as e: -# print(f"Error in pipe server: {e}") -# finally: -# if self._pipe_operator: -# self._pipe_operator.close() - -# def stop_server(self): -# """Stop the pipe server.""" -# self._stop_event.set() -# if self._server_thread: -# self._server_thread.join() - -# def get_pipe(self) -> Optional[PipeOperator]: -# """Get the connected pipe operator (only valid after client connects).""" -# return self._pipe_operator diff --git a/Document/PROTOCOL.md b/Document/PROTOCOL.md index b427529..c5f9a9c 100644 --- a/Document/PROTOCOL.md +++ b/Document/PROTOCOL.md @@ -2,11 +2,18 @@ This document introduce the protocol used between Basalt Presenter and Basalt Trainer. +If there is no specific description, the endian of data is **native endian**, and the alignment also is native. +Because all data are transmitted in local machine, so we use native layout. + ## Command Protocol Before introducing command protocol, it would be better to the priniciple, that Presenter is the slave application, and Trainer is the master application. +Command protocol is tramsmitted by system named pipe. +The name of pipe is `\\.\pipe\ed0e3f1f-d214-4880-9562-640bce15e72e` on Windows +or `/tmp/ed0e3f1f-d214-4880-9562-640bce15e72e` on POSIX. + |Code|Direction|Comment| |:---|:---|:---| |`0x61`|Presenter<--Trainer|Handshake code (Are Presenter ready?)| @@ -36,8 +43,16 @@ For the all possible values of "pixel kind" in above table, there is also a tabl |Value|Comment| |:---|:---| -|`0x01`|Grayscale represneted by one float32| +|`0x01`|Grayscale represented by one float32| +|`0x02`|Grayscale represented by one u8| +|`0x03`|RGB represented by three float32| +|`0x04`|RGB represented by three u8| ## Data Protocol -TODO +### Pipe + +For this kind deliver, it is tramsmitted by system named pipe. +The name of pipe is `\\.\pipe\ebe2a39d-75de-4cf4-933f-c50284149d63` on Windows +or `/tmp/ebe2a39d-75de-4cf4-933f-c50284149d63` on POSIX. +