open-simulation-platform / libcosim

OSP C++ co-simulation library
https://open-simulation-platform.github.io/libcosim
Mozilla Public License 2.0
55 stars 10 forks source link

PoC for DCP #192

Open hplatou opened 5 years ago

hplatou commented 5 years ago

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.

kyllingstad commented 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.

kyllingstad commented 5 years ago

Some thoughts:

Edit: Suggest going for UDP before TCP, for simplicity.

kyllingstad commented 5 years ago

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_;
}
kyllingstad commented 5 years ago

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. :-)

kyllingstad commented 5 years ago

As mentioned in the meeting, I think we should strive to use cse::async_slave as the interface to slaves regardless of protocol.

kyllingstad commented 5 years ago

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.

kyllingstad commented 5 years ago

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_;
    // ...
hplatou commented 5 years ago

I'l give it a try!