Open hplatou opened 5 years ago
I completely agree with @hplatou. After looking at DCPLib I went straight into we-can-do-this-way-better mode (which is definitely not the same as NIH syndrome, no sir, thank you very much) and started getting some ideas of how this can be done. I'll post them here soon so we can discuss.
Some thoughts:
cse::event_loop
was used for this, of course, since that's exactly what I wrote it for. ;) But we haven't discussed this properly yet, so opinions may differ.Edit: Suggest going for UDP before TCP, for simplicity.
Regarding the transport layer, I wrote some PoC code that could be used for this when I was working on event_loop
, but now I can't find it. But it went something like this:
class socket :
public socket_event_handler,
public timer_event_handler
{
socket(cse::event_loop& eventLoop)
{
socket_ = open_socket_in_nonblocking_mode();
ioEvent_ = eventLoop.add_socket(socket_);
timeoutEvent_ = eventLoop.add_timer();
}
gsl::span<std::byte> receive(gsl::span<std::byte> buffer, std::chrono::nanoseconds timeout)
{
while(true) {
const auto n = receive_nonblocking(buffer); // e.g. using an OS function
if (n > 0) {
return buffer.subspan(0, n);
}
ioEvent_->enable(socket_event_type::read, false, *this);
timeoutEvent_->enable(timeout, false, *this);
// now wait for condition variable, yielding to other fibers in the meantime
}
}
private:
void handle_socket_event(socket_event* event, socket_event_type* type)
{
// reset timeout timer
// signal condition variable so that receive() resumes
}
void handle_timer_event(timer_event* event)
{
// cancel i/o event
// signal condition variable so that receive() can resume and throw timeout exception
}
native_socket socket_;
socket_event* ioEvent_;
timer_event* timeoutEvent_;
}
The example above was just off the top of my head, there are probably several issues with it. The point was to give a feel for what I had in mind. Among other things it's missing a send()
function, but the implementation would be very similar to receive()
.
The nice thing about it is that when you write code that uses the socket
class, the code looks linear while it's really asynchronous and fiberised. :-)
As mentioned in the meeting, I think we should strive to use cse::async_slave
as the interface to slaves regardless of protocol.
Regarding the serialisation/deserialisation mechanism:
There are a bunch of different message (aka. PDU) types in DCP, and depending on how many of them we actually need in "our" DCP subset, writing and maintaining the code for each of them by hand may become a pain. We should consider code generation instead. We could specify the PDU structure in a simple text format (YAML used as example here):
pdus:
- name: stc_do_step
class: stc
type_id: 0x07
fields:
- name: steps
type: uint32
- name: inf_log
class: inf
type_id: 0x82
fields:
- name: log_category
type: uint8
- name: log_max_num
type: uint8
Then, a simple script could take this as input, apply it to some C++ code templates, and produce custom code such as this:
#pragma pack(1)
class stc_do_step
{
private:
// Common PDU fields
std::uint8 type_id_ = 0x07;
std::uint16 pdu_seq_id_;
std::uint8 receiver_;
// Common STC fields
std::uint8 state_id_;
// PDU-specific fields
std::uint32 steps_;
public:
std::uint32 get_steps() { return steps_; }
void set_steps(std::uint32 value) { steps_ = value; }
// ...
gsl::span<std::byte> serialize()
{
// With some care to handle endianness, and given the `pragma pack`
// up top, this might even work:
return gsl::make_span(reinterpret_cast<std::byte*>(this), sizeof *this);
}
};
This might be overkill for a first PoC, but it would be really cool to do this if/when we decide to go beyond that stage.
I couldn't help myself, and even started (over)thinking of how to handle endianness and similar encoding issues. ;)
template<T>
class serialized_integer
{
public:
serialized_integer& operator=(T value)
{
// Encode `value` with the correct endianness in `value_`.
}
T operator T() const
{
// Decode `value_` to native endianness and return it.
}
private:
std::byte value_[sizeof(T)];
};
#pragma pack(1)
class stc_do_step
{
private:
// Common PDU fields
serialized_integer<std::uint8> type_id_ = 0x07;
serialized_integer<std::uint16> pdu_seq_id_;
serialized_integer<std::uint8> receiver_;
// ...
I'l give it a try!
Opening this issue to start discussing a PoC implementation of DCP.
After reading the code in the DCPLib repository and discussing with @kyllingstad I don't think we should use it. The code quality seems to be less than adequate.
My suggestion would be to implement a PoC in cse-core. One solution would be to implement a new cse_slave being a DCP master that will be instantiated for every DCP Slave, but this is up for discussion.