Beckhoff / ADS

Beckhoff protocol to communicate with TwinCAT devices.
MIT License
493 stars 195 forks source link

read write frequency #231

Open marvin-ad-martianum opened 2 weeks ago

marvin-ad-martianum commented 2 weeks ago

Im running a ros2 node reading and writing in parallel (optimized for minimal data) on four loops. However, the data transmission speed of the fastest loop is limited to 1000hz (also if the plc is running faster) and less if i am running everything at the speed i need (around 600hz for the fastest loop). Is there a way to speed up the beckhoff side? or change from tcp to udp? The general limitation seems to be around 800-1000 microseconds for one read or write process of a variable or list of variables.

pbruenn commented 2 weeks ago

You might be able to achieve a higher frequency with AdsSyncAddDeviceNotificationReqEx

When you set AdsNotificationAttrib.nCycleTime=0 and use ADSTRANS_SERVERCYCLE you should get the maximum amount of events.

A simple example for notifications is here: https://github.com/Beckhoff/ADS/blob/master/example/example.cpp#L30-L42

marvin-ad-martianum commented 1 week ago

Thank you for your response! I took your advice and had a try. Now please correct me or point me in the right direction.

Original Loop

This is how I had one of my loops: (some parameters are written, some are read. In the future, I will separate read and write to different arrays.)

// Initialize
AdsVariable<std::array<bool, machine_control_bool_len>> machine_control_bool{route, idx_group, idx_offset_BOOL};

// Pass to node as pointer

// -------- Start loop -----------------

std::array<bool, machine_control_bool_len> machine_control_b = static_cast<std::array<bool, machine_control_bool_len>>(machine_control_bool_);

// ---- Stay alive, ROS to Beckhoff
machine_control_b[0] = true;

// Do other stuff

// Write using the operator overloading of ADS template
machine_control_bool_ = machine_control_b; 

// ---- End loop ----------------------

New Approach with Callback

With the new approach you suggested, would I be callback-based? So Beckhoff would define the frequency through PLC timers, or how? I implemented this:

// Initialize

long nErr;
AmsAddr Addr;

// Set remote AmsNetId
Addr.netId = remoteNetId;

// Set remote AMS port (commonly 851 for the first PLC runtime system)
Addr.port = AMSPORT_R0_PLC_TC3;

// Index group and index offset
uint32_t indexGroup = idx_group;
uint32_t indexOffset = idx_offset_BOOL;

AdsNotificationAttrib NotificationAttrib;
NotificationAttrib.cbLength = sizeof(std::array<bool, machine_control_bool_len>);
NotificationAttrib.nTransMode = ADSTRANS_SERVERCYCLE;
NotificationAttrib.nMaxDelay = 1000; // 0.001 second
NotificationAttrib.nCycleTime = 0; // 100 ms

uint32_t hNotification;
uint32_t hUser = 0;

nErr = AdsSyncAddDeviceNotificationReqEx(AdsPort, &Addr, indexGroup, indexOffset, &NotificationAttrib, AdsNotificationCallback, hUser, &hNotification);

if (nErr) {
    std::cerr << "Error: AdsSyncAddDeviceNotificationReqEx failed with error code " << nErr << std::endl;
    return -1;
} else {                
    std::cerr << "Success on New callback method with: code " << nErr << std::endl;
}

void AdsNotificationCallback(const AmsAddr* pAddr, const AdsNotificationHeader* pNotification, uint32_t hUser) {
    // Suppress unused parameter warnings
    (void)pAddr;
    (void)hUser;

    const std::array<bool, machine_control_bool_len>& data = *reinterpret_cast<const std::array<bool, machine_control_bool_len>*>(pNotification + 1);
    std::cout << "First element: " << data[0] << std::endl;

    }

Questions and Considerations

pbruenn commented 1 week ago

@marvin-ad-martianum

EDIT: We seem to have one dispatcher thread per Notification, however to be able to receive the next one while processing another you should either process fast or relay the data to another thread

marvin-ad-martianum commented 5 days ago

Great. I have implemented this and it works amazing. I am in the nanosecond range for receiving data (much faster than expected). However, there is still one issue. Writing using the overload of the ads function such as before:

std::array<bool, machine_control_bool_len> machine_control_b = static_cast<std::array<bool, machine_control_bool_len>>(machine_controlbool);

// Do other stuff

// Write using the operator overloading of ADS template machine_controlbool = machine_control_b;

This operation delays the callbacks and creates gaps in the streamed data. Is there a more efficient way to writing data back to beckhoff? also with defining some sort of callback on the beckhoff side? This operation takes around 200 to 1200 microseconds which is extremely slow compared to the 10-30 nanoseconds i measure on the callback. Further the time it takes to write from cpp to ads i canot influence from the cpp side. beckhoff will let it rise to the large value of 1200 before it cycles down to 200. This operation seems to be blocking to some extent.

pbruenn commented 1 day ago

You can try to use a different "AmsPort" for sending than receiving. I don't know about the "TwinCAT receiving end" so I am sorry I cant help you with that.

Do you run TwinCAT and your ros2 node on the same CPU? Or is there a real network physical layer between them?

marvin-ad-martianum commented 1 day ago

Yea. okay. Do you know that using another port is nonblocking?

I was thinking of adding another remoteNetId but the port would be more convenient i guess.

    AmsAddr Addr;
    // Set remote AmsNetId
    Addr.netId = remoteNetId;
    // Set remote AMS port (commonly 851 for the first PLC runtime system)
    Addr.port = AMSPORT_R0_PLC_TC3;

I physically divide the system. I have the beckhoff plc on a their classical windows and a 32 core linux server to do the heavy lifting. The software is multi-threaded and the bottleneck the ads tcp i think (also udp seems impossible with the current state of beckhoff). The callback seems almost unreasonably fast while writing is slow at 1ms.

pbruenn commented 1 day ago

Okay with (virtually) two separate hosts maybe EtherCAT Automation Protocol (EAP) is a better solution. Did you considere that: https://infosys.beckhoff.com/content/1033/eap/1521519371.html?id=795366871337330973

marvin-ad-martianum commented 1 day ago

Yes, Two considerations, one we are planning to switch beckhoff to linux for shipping later, which they claim to "release in the future". Whatever that means. But its important to have this option to move to one "pc" unit.

I have seen the advantage of Ethercat but no route that would allow me to use the current hardware C6030-0070 without a lot of extra work and without the guarantee it will greatly improve the performance. If there is an efficient way we will do it, if there is not i will live with the limitation. I saw a few things but do you know some reliable library?

Is there a way in this library to limit or define the write time? switch to udp? or i guess changing the port could solve the issue? Given i am 90% at where i want to be i am inclined to stay with this solution here.

pbruenn commented 1 day ago

Yeah, I totally understand your reasoning.
I looked into the code again and the problem is we only have a single TCP connection. I don't know how good both sides can handle send/recv in parallel. So that might be one of the limits. UDP would be the obvious next try. But the problem is the TwinCAT cannot receive ADS on UDP (As far as I know).

I have zero experience with EAP myself, so I can't tell you how much effort it is to port your current solution to EAP. And I can't tell about any libraries either. I simply don't know. What I know is the EtherCAT Technology Group has some excellent support guys. Not sure if you have to be ETG member (free of charge) to ask them for support. If you get EAP working I think you should make it work on localhost too, with virtual network interfaces or UDP.

Another hack you can try is socat. You can try to run socat as a UDP tunnel between Linux and Windows and have TCP only on localhost. I can imagine this might speed up your latency issue, but getting the ADS setting correct for this might be pretty hard. You might need a fake IP address on the windows side to force TwinCAT really send tcp out to your socat instance on localhost.