/* see protocol.md for the protocol specification */ #ifndef __PROTOCOL_HPP #define __PROTOCOL_HPP // TODO: resolve assert #define assert(expr) #include #include #include #include "crc.hpp" // Note that this option cannot be used to debug UART because it prints on UART //#define DEBUG_PROTOCOL #ifdef DEBUG_PROTOCOL #define LOG_PROTO(...) do { printf(__VA_ARGS__); osDelay(10); } while (0) #else #define LOG_PROTO(...) ((void) 0) #endif constexpr uint8_t SYNC_BYTE = 0xAA; constexpr uint8_t CRC8_INIT = 0x42; constexpr uint16_t CRC16_INIT = 0x1337; constexpr uint16_t PROTOCOL_VERSION = 1; // This value must not be larger than USB_TX_DATA_SIZE defined in usbd_cdc_if.h //Oskar: What's the error? What values work? Does 63 work? Ideally we figure out how to get 64 to work, but if not let's find something better than 32. constexpr uint16_t TX_BUF_SIZE = 32; // does not work with 64 for some reason constexpr uint16_t RX_BUF_SIZE = 128; // larger values than 128 have currently no effect because of protocol limitations // Maximum time we allocate for processing and responding to a request constexpr uint32_t PROTOCOL_SERVER_TIMEOUT_MS = 10; template inline size_t write_le(T value, uint8_t* buffer); template inline size_t read_le(T* value, const uint8_t* buffer); template<> inline size_t write_le(bool value, uint8_t* buffer) { buffer[0] = value ? 1 : 0; return 1; } template<> inline size_t write_le(uint8_t value, uint8_t* buffer) { buffer[0] = value; return 1; } template<> inline size_t write_le(uint16_t value, uint8_t* buffer) { buffer[0] = (value >> 0) & 0xff; buffer[1] = (value >> 8) & 0xff; return 2; } template<> inline size_t write_le(uint32_t value, uint8_t* buffer) { buffer[0] = (value >> 0) & 0xff; buffer[1] = (value >> 8) & 0xff; buffer[2] = (value >> 16) & 0xff; buffer[3] = (value >> 24) & 0xff; return 4; } template<> inline size_t write_le(int32_t value, uint8_t* buffer) { buffer[0] = (value >> 0) & 0xff; buffer[1] = (value >> 8) & 0xff; buffer[2] = (value >> 16) & 0xff; buffer[3] = (value >> 24) & 0xff; return 4; } template<> inline size_t write_le(float value, uint8_t* buffer) { static_assert(CHAR_BIT * sizeof(float) == 32, "32 bit floating point expected"); static_assert(std::numeric_limits::is_iec559, "IEEE 754 floating point expected"); const uint32_t * value_as_uint32 = reinterpret_cast(&value); return write_le(*value_as_uint32, buffer); } template<> inline size_t read_le(bool* value, const uint8_t* buffer) { *value = buffer[0]; return 1; } template<> inline size_t read_le(uint8_t* value, const uint8_t* buffer) { *value = buffer[0]; return 1; } template<> inline size_t read_le(uint16_t* value, const uint8_t* buffer) { *value = (static_cast(buffer[0]) << 0) | (static_cast(buffer[1]) << 8); return 2; } template<> inline size_t read_le(int32_t* value, const uint8_t* buffer) { *value = (static_cast(buffer[0]) << 0) | (static_cast(buffer[1]) << 8) | (static_cast(buffer[2]) << 16) | (static_cast(buffer[3]) << 24); return 4; } template<> inline size_t read_le(uint32_t* value, const uint8_t* buffer) { *value = (static_cast(buffer[0]) << 0) | (static_cast(buffer[1]) << 8) | (static_cast(buffer[2]) << 16) | (static_cast(buffer[3]) << 24); return 4; } template<> inline size_t read_le(float* value, const uint8_t* buffer) { static_assert(CHAR_BIT * sizeof(float) == 32, "32 bit floating point expected"); static_assert(std::numeric_limits::is_iec559, "IEEE 754 floating point expected"); return read_le(reinterpret_cast(value), buffer); } // @brief Reads a value of type T from the buffer. // @param buffer Pointer to the buffer to be read. The pointer is updated by the number of bytes that were read. // @param length The number of available bytes in buffer. This value is updated to subtract the bytes that were read. template static inline T read_le(const uint8_t** buffer, size_t* length) { T result; size_t cnt = read_le(&result, *buffer); *buffer += cnt; *length -= cnt; return result; } class PacketSink { public: // @brief Processes a packet. // The blocking behavior shall depend on the thread-local deadline_ms variable. // @return: 0 on success, otherwise a non-zero error code // TODO: define what happens when the packet is larger than what the implementation can handle. virtual int process_packet(const uint8_t* buffer, size_t length) = 0; }; class StreamSink { public: // @brief Processes a chunk of bytes that is part of a continuous stream. // The blocking behavior shall depend on the thread-local deadline_ms variable. // @return: 0 on success, otherwise a non-zero error code virtual int process_bytes(const uint8_t* buffer, size_t length) = 0; // @brief Returns the number of bytes that can still be written to the stream. // Shall return SIZE_MAX if the stream has unlimited lenght. virtual size_t get_free_space() = 0; }; class StreamToPacketConverter : public StreamSink { public: StreamToPacketConverter(PacketSink& output) : output_(output) { }; int process_bytes(const uint8_t *buffer, size_t length); size_t get_free_space() { return SIZE_MAX; } private: uint8_t header_buffer_[3]; size_t header_index_ = 0; uint8_t packet_buffer_[RX_BUF_SIZE]; size_t packet_index_ = 0; size_t packet_length_ = 0; PacketSink& output_; }; class PacketToStreamConverter : public PacketSink { public: PacketToStreamConverter(StreamSink& output) : output_(output) { }; int process_packet(const uint8_t *buffer, size_t length); private: StreamSink& output_; }; // Implements the StreamSink interface by writing into a fixed size // memory buffer. class MemoryStreamSink : public StreamSink { public: MemoryStreamSink(uint8_t *buffer, size_t length) : buffer_(buffer), buffer_length_(length) {} // Returns 0 on success and -1 if the buffer could not accept everything because it became full int process_bytes(const uint8_t* buffer, size_t length) { int status = 0; if (length > buffer_length_) { length = buffer_length_; status = -1; } memcpy(buffer_, buffer, length); buffer_ += length; buffer_length_ -= length; return status; } size_t get_free_space() { return buffer_length_; } private: uint8_t * buffer_; size_t buffer_length_; }; // Implements the StreamSink interface by discarding the first couple of bytes // and then forwarding the rest to another stream. class NullStreamSink : public StreamSink { public: NullStreamSink(size_t skip, StreamSink& follow_up_stream) : skip_(skip), follow_up_stream_(follow_up_stream) {} // Returns 0 on success and -1 if the buffer could not accept everything because it became full int process_bytes(const uint8_t* buffer, size_t length) { if (skip_ < length) { buffer += skip_; length -= skip_; skip_ = 0; return follow_up_stream_.process_bytes(buffer, length); } else { skip_ -= length; return 0; } } size_t get_free_space() { return skip_ + follow_up_stream_.get_free_space(); } private: size_t skip_; StreamSink& follow_up_stream_; }; // Implements the StreamSink interface by calculating the CRC16 checksum // on the data that is sent to it. class CRC16Calculator : public StreamSink { public: CRC16Calculator(uint16_t crc16_init) : crc16_(crc16_init) {} int process_bytes(const uint8_t* buffer, size_t length) { crc16_ = calc_crc16(crc16_, buffer, length); return 0; } size_t get_free_space() { return SIZE_MAX; } uint16_t get_crc16() { return crc16_; } private: uint16_t crc16_; }; // @brief Endpoint request handler // // When passed a valid endpoint context, implementing functions shall handle an // endpoint read/write request by reading the provided input data and filling in // output data. The exact semantics of this function depends on the corresponding // endpoint's specification. // // @param input: pointer to the input data // @param input_length: number of available input bytes // @param output: The stream where to write the output to. Can be null. // The handler shall abort as soon as the stream returns // a non-zero error code on write. typedef std::function EndpointHandler; template void default_readwrite_endpoint_handler(const T* value, const uint8_t* input, size_t input_length, StreamSink* output) { // If the old value was requested, call the corresponding little endian serialization function if (output) { // TODO: make buffer size dependent on the type uint8_t buffer[sizeof(T)]; size_t cnt = write_le(*value, buffer); if (cnt <= output->get_free_space()) output->process_bytes(buffer, cnt); } } template void default_readwrite_endpoint_handler(T* value, const uint8_t* input, size_t input_length, StreamSink* output) { // Read the endpoint value into output default_readwrite_endpoint_handler(const_cast(value), input, input_length, output); // If a new value was passed, call the corresponding little endian deserialization function uint8_t buffer[sizeof(T)] = { 0 }; // TODO: make buffer size dependent on the type if (input_length >= sizeof(buffer)) read_le(value, input); } template static inline const char* get_default_json_modifier(); template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"float\",\"access\":\"r\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"float\",\"access\":\"rw\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"int32\",\"access\":\"r\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"int32\",\"access\":\"rw\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"uint32\",\"access\":\"r\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"uint32\",\"access\":\"rw\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"uint16\",\"access\":\"r\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"uint16\",\"access\":\"rw\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"uint8\",\"access\":\"r\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"uint8\",\"access\":\"rw\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"bool\",\"access\":\"r\""; } template<> inline constexpr const char* get_default_json_modifier() { return "\"type\":\"bool\",\"access\":\"rw\""; } class Endpoint { public: //const char* const name_; virtual void handle(const uint8_t* input, size_t input_length, StreamSink* output) = 0; }; class EndpointProvider { public: virtual size_t get_endpoint_count() = 0; virtual void write_json(size_t id, StreamSink* output) = 0; virtual void register_endpoints(Endpoint** list, size_t id, size_t length) = 0; }; static inline int write_string(const char* str, StreamSink* output) { return output->process_bytes(reinterpret_cast(str), strlen(str)); } /* @brief Handles the communication protocol on one channel. * * When instantiated with a list of endpoints and an output packet sink, * objects of this class will handle packets passed into process_packet, * pass the relevant data to the corresponding endpoints and dispatch response * packets on the output. */ class BidirectionalPacketBasedChannel : public PacketSink { public: BidirectionalPacketBasedChannel(PacketSink& output) : output_(output) { } int process_packet(const uint8_t* buffer, size_t length); private: PacketSink& output_; uint8_t tx_buf_[TX_BUF_SIZE]; }; template struct MemberList; template<> struct MemberList<> { public: static constexpr size_t endpoint_count = 0; static constexpr bool is_empty = true; void write_json(size_t id, StreamSink* output) { // no action } void register_endpoints(Endpoint** list, size_t id, size_t length) { // no action } std::tuple<> get_names_as_tuple() const { return std::tuple<>(); } }; template struct MemberList { public: static constexpr size_t endpoint_count = TMember::endpoint_count + MemberList::endpoint_count; static constexpr bool is_empty = false; MemberList(TMember&& this_member, TMembers&&... subsequent_members) : this_member_(std::forward(this_member)), subsequent_members_(std::forward(subsequent_members)...) {} MemberList(TMember&& this_member, MemberList&& subsequent_members) : this_member_(std::forward(this_member)), subsequent_members_(std::forward>(subsequent_members)) {} // @brief Move constructor /* MemberList(MemberList&& other) : this_member_(std::move(other.this_member_)), subsequent_members_(std::move(other.subsequent_members_)) {}*/ void write_json(size_t id, StreamSink* output) /*final*/ { this_member_.write_json(id, output); if (!MemberList::is_empty) write_string(",", output); subsequent_members_.write_json(id + TMember::endpoint_count, output); } void register_endpoints(Endpoint** list, size_t id, size_t length) /*final*/ { this_member_.register_endpoints(list, id, length); subsequent_members_.register_endpoints(list, id + TMember::endpoint_count, length); } TMember this_member_; MemberList subsequent_members_; }; template MemberList make_protocol_member_list(TMembers&&... member_list) { return MemberList(std::forward(member_list)...); } template class ProtocolObject { public: ProtocolObject(const char * name, TMembers&&... member_list) : name_(name), member_list_(std::forward(member_list)...) {} static constexpr size_t endpoint_count = MemberList::endpoint_count; void write_json(size_t id, StreamSink* output) { write_string("{\"name\":\"", output); write_string(name_, output); write_string("\",\"type\":\"object\",\"members\":[", output); member_list_.write_json(id, output), write_string("]}", output); } void register_endpoints(Endpoint** list, size_t id, size_t length) { member_list_.register_endpoints(list, id, length); } const char * name_; MemberList member_list_; }; template ProtocolObject make_protocol_object(const char * name, TMembers&&... member_list) { return ProtocolObject(name, std::forward(member_list)...); } template class ProtocolProperty : Endpoint { public: static constexpr const char * json_modifier = get_default_json_modifier(); static constexpr size_t endpoint_count = 1; ProtocolProperty(const char * name, TProperty* property) : name_(name), property_(property) {} // ProtocolProperty(const ProtocolProperty&) = delete; // @brief Move constructor ProtocolProperty(ProtocolProperty&& other) : Endpoint(std::move(other)), name_(std::move(other.name_)), property_(other.property_) {} //constexpr ProtocolProperty& operator=(const ProtocolProperty& other) = delete; /*constexpr ProtocolProperty& operator=(const ProtocolProperty& other) { //Endpoint(std::move(other)), //name_(std::move(other.name_)), //property_(other.property_) name_ = other.name_; property_ = other.property_; return *this; }*/ /*ProtocolProperty& operator=(ProtocolProperty&& other) : name_(other.name_), property_(other.property_) {} ProtocolProperty& operator=(const ProtocolProperty& other) : name_(other.name_), property_(other.property_) {}*/ void write_json(size_t id, StreamSink* output) { // write name write_string("{\"name\":\"", output); LOG_PROTO("json: this at %x, name at %x is s\r\n", (uintptr_t)this, (uintptr_t)name_); //LOG_PROTO("json\r\n"); write_string(name_, output); // write endpoint ID write_string("\",\"id\":", output); char id_buf[10]; snprintf(id_buf, sizeof(id_buf), "%u", id); // TODO: get rid of printf write_string(id_buf, output); // write additional JSON data if (json_modifier && json_modifier[0]) { write_string(",", output); write_string(json_modifier, output); } write_string("}", output); } void register_endpoints(Endpoint** list, size_t id, size_t length) { if (id < length) list[id] = this; } void handle(const uint8_t* input, size_t input_length, StreamSink* output) { default_readwrite_endpoint_handler(property_, input, input_length, output); } /*void handle(const uint8_t* input, size_t input_length, StreamSink* output) { handle(input, input_length, output); }*/ const char * name_; TProperty* property_; }; // Non-const non-enum types template::value>> ProtocolProperty make_protocol_property(const char * name, TProperty* property) { return ProtocolProperty(name, property); }; // Const non-enum types template::value>> ProtocolProperty make_protocol_ro_property(const char * name, const TProperty* property) { return ProtocolProperty(name, property); }; // Non-const enum types template::value>> ProtocolProperty> make_protocol_property(const char * name, TProperty* property) { return ProtocolProperty>(name, reinterpret_cast*>(property)); }; // Const enum types template::value>> ProtocolProperty> make_protocol_ro_property(const char * name, const TProperty* property) { return ProtocolProperty>(name, reinterpret_cast*>(property)); }; template class FunctionTraits { public: template> static TRet invoke(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std::tuple packed_args, TUnpackedArgs ... args) { return invoke(obj, func_ptr, packed_args, args..., std::get(packed_args)); } template static TRet invoke(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std::tuple packed_args, TArgs ... args) { return (obj.*func_ptr)(args...); } }; /* @brief Invoke a class member function with a variable number of arguments that are supplied as a tuple Example usage: class MyClass { public: int MyFunction(int a, int b) { return 0; } }; MyClass my_object; std::tuple my_args(3, 4); // arguments are supplied as a tuple int result = invoke_function_with_tuple(my_object, &MyClass::MyFunction, my_args); */ template TRet invoke_function_with_tuple(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std::tuple packed_args) { return FunctionTraits::template invoke<0>(obj, func_ptr, packed_args); } template struct PropertyListFactory; template<> struct PropertyListFactory<> { template static MemberList<> make_property_list(std::array names, std::tuple& values) { return MemberList<>(); } }; template struct PropertyListFactory { template static MemberList, ProtocolProperty...> make_property_list(std::array names, std::tuple& values) { return MemberList, ProtocolProperty...>( make_protocol_property(std::get(names), &std::get(values)), PropertyListFactory::template make_property_list(names, values) ); } }; template class ProtocolFunction : Endpoint { public: static constexpr size_t endpoint_count = 1 + MemberList...>::endpoint_count; template ProtocolFunction(const char * name, TObj& obj, TRet(TObj::*func_ptr)(TArgs...), TNames ... names) : name_(name), all_arg_names_{names...}, obj_(obj), func_ptr_(func_ptr), input_properties_(PropertyListFactory::template make_property_list<0>(all_arg_names_, in_args_)) { LOG_PROTO("my tuple is at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); } ProtocolFunction(const ProtocolFunction& other) : name_(other.name_), all_arg_names_(other.all_arg_names_), obj_(other.obj_), func_ptr_(other.func_ptr_), input_properties_(PropertyListFactory::template make_property_list<0>( all_arg_names_, in_args_)) { LOG_PROTO("COPIED! my tuple is at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); } void write_json(size_t id, StreamSink* output) { // write name write_string("{\"name\":\"", output); write_string(name_, output); // write endpoint ID write_string("\",\"id\":", output); char id_buf[10]; snprintf(id_buf, sizeof(id_buf), "%u", id); // TODO: get rid of printf write_string(id_buf, output); // write arguments write_string(",\"type\":\"function\",\"arguments\":[", output); input_properties_.write_json(id + 1, output), write_string("]}", output); } void register_endpoints(Endpoint** list, size_t id, size_t length) { if (id < length) list[id] = this; input_properties_.register_endpoints(list, id + 1, length); } void handle(const uint8_t* input, size_t input_length, StreamSink* output) { (void) input; (void) input_length; (void) output; LOG_PROTO("tuple still at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); LOG_PROTO("invoke function using %d and %.3f\r\n", std::get<0>(in_args_), std::get<1>(in_args_)); invoke_function_with_tuple(obj_, func_ptr_, in_args_); } const char * name_; std::array all_arg_names_; // TODO: remove TObj& obj_; TRet(TObj::*func_ptr_)(TArgs...); std::tuple in_args_; MemberList...> input_properties_; }; template> ProtocolFunction make_protocol_function(const char * name, TObj& obj, TRet(TObj::*func_ptr)(TArgs...), TNames ... names) { return ProtocolFunction(name, obj, func_ptr, names...); } template class EndpointProvider_from_MemberList : public EndpointProvider { public: EndpointProvider_from_MemberList(T& member_list) : member_list_(member_list) {} size_t get_endpoint_count() final { return T::endpoint_count; } void write_json(size_t id, StreamSink* output) final { return member_list_.write_json(id, output); } void register_endpoints(Endpoint** list, size_t id, size_t length) final { return member_list_.register_endpoints(list, id, length); } T& member_list_; }; void set_application_endpoints(EndpointProvider* endpoints); // defined in communication.cpp extern Endpoint* endpoints_[]; extern size_t n_endpoints_; extern const size_t max_endpoints_; #endif