-
Notifications
You must be signed in to change notification settings - Fork 217
Conversation
pbruenn
commented
Nov 12, 2025
Hi @m-glz,
interesting idea, I really like it, since it seems we can finally inform about conenction loss. Whats still unclear to me is how does this work in practice? Please add a short example to your description, which we can later copy into one of the commit messages.
Some additional notes:
- Do we really need a new Notification type? On first look it seems we can use the normal one and we should even reuse the NotificationHeader, as a timestamp would be nice for these notifications, too. Overall the handling would be much simpler if we had only one type of notifications. And apart from a little overhead I don't see why our synthetic notification, shouldn't fit into the generic types.
- I don't understand the last commit. It is very intrusive. As I said maybe an example to test the connection lost usecase might help to understand this
- keep your commits clean. Don't add stuff to one commit which you immediately remove in the next one. Yes, sometimes this is necessary but I don't think thats the case for this feature.
georg-emg
commented
Nov 12, 2025
Hi, @pbruenn
Thank you for your prompt response. In answer to your questions:
- A new notication type is not really necessary. However, the existing notifications are identified by index group and index offset. Merging the notification types would require reserving special index group and index offset pairs for the synthetic notifications, which we didn't feel empowered to do. If Beckhoff is willing to reserve some addresses for these notifications, then the existing infrastructure can absolutely be used.
- As for the second commit, the issue we were facing was the following: The AMS connection uses the ring buffer to communicate with the notification dispatcher. Once the semaphore is triggered, the notification dispatcher will inspect the ring buffer to check whether there is a notification in it or not. Previously, the semaphore was only ever triggered once the ring buffer had been fully updated, so there was no issue. Now however, AmsConnection::ReceiveNotification might be interrupted by a socket read error. This will cause the semaphore to be triggered from AmsConnection::TryRecv. At this point, however, there may already be a partial notification in the ring buffer, which will confuse the notification dispatcher when it wakes up. Our solution was to make updates to the ring buffer atomic by introducing a transaction object. The transaction object operates on a copy of the ring buffer's write pointer, and only updates the real write pointer once the entire notification is in the buffer. That way, we can ensure that the notification dispatcher only ever sees complete notifications.
- As for the commits: When adding NOTIFY_NOTIFICATION_RCV, we needed access to the AMSAddr/Port associated with the notification, so we added code to NotificationDispatcher::Run to extract it from the ring buffer. When adding NOTIFY_CONNECTION_LOST, that no longer worked, because now the ring buffer was empty. So we replaced the mechanism we had implemented previously with a better and simpler one, that would work in both cases. If you prefer, we can rewrite the commit history to remove that intermediate step.
I will also try to prepare some code examples of use cases for the new notification types.
pbruenn
commented
Nov 12, 2025
Thanks for clarification. Yes, the examples are important for me to understand this and get a better idea of how we can cleanly implement this. Don't put to much effort into cleanup just yet. I first want to try your example to see in which direction we should move this.
georg-emg
commented
Nov 13, 2025
Here is a small sample for the use of the NOTIFY_CONNECTION_LOST notification. It's a little application that establishes a connection and logs ADSIOFFS_DEVDATA_ADSSTATE. It uses NOTIFY_CONNECTION_LOST to detect a connection loss and reestablished the connection automatically.
I will add a sample for NOTIFY_NOTIFICATION_RCV later.
// This is a sample application demonstarting the use of the proposed NOTIFY_CONNECTION_LOST synthetic notification // // This sample requires C++23 // // This application will connect to a TwinCAT PLC and output the PLC state. If the connection drops, the application // will attempt to reconnect once a until successful. // // To simplify the code, error handling has been largely omitted. // // Without the NOTIFY_CONNECTION_LOST the application would never detect the connection loss, because it does not // send any commands, and thus never gets any errors. With the NOTIFY_CONNECTION_LOST notification, the connection // loss is detected, and the connection reestablished. // // To test: // // - Start the application and connect to a TwinCAT PLC instance. The application will start logging the device state. // - Start and stop the PLC to verify that the device state is logged // - Put TwinCAT into config mode. This will cause TwinCAT to drop the connection. // - Put TwinCAT back into run mode. The application will reestablish the connection and resume logging the device state. // - Start and stop the PLC to verify that the device state is logged again // // Without the NOTIFY_CONNECTION_LOST notification, the connection will never be reestablished, and logging of the PLC state // will never resume even after TwinCAT has been put back into run mode. #include <AdsLib.h> #include <chrono> #include <cstddef> #include <cstdlib> #include <iostream> #include <semaphore> #include <span> #include <thread> using namespace std::literals; // A semaphore used to communicate with the main thread std::binary_semaphore connectionLostSemaphore { 0 }; // Callback for connection loss notifications void connectionLostCallback(const AmsAddr *amsAddress, std::uint32_t type, std::uint32_t userValue) { // Log the connection loss std::cout << "Connection loss detected." << std::endl; // Notify the main thread connectionLostSemaphore.release(); } // Callback for device state notifications void deviceStateCallback(const AmsAddr *amsAddress, const AdsNotificationHeader *header, std::uint32_t userValue) { // Sanity check if (header->cbSampleSize != sizeof(std::uint16_t)) { std::cout << "Received invalid device state with size " << header->cbSampleSize << std::endl; return; } // Get the value std::uint16_t state; std::memcpy(&state, header + 1, sizeof(state)); if constexpr (std::endian::native != std::endian::little) { state = std::byteswap(state); } // Log the state std::cout << "Device state: " << state << std::endl; } auto main(int argc, char *argv[]) -> int { // Decode the parameters if (argc < 4) { std::cerr << "Usage: <AMS Net-ID> <AMS Port> <IP address>" << std::endl; return EXIT_FAILURE; } AmsAddr amsAddress { .netId { argv[1] }, .port { std::uint16_t(std::atoi(argv[2])) } }; const char *ipAddress { argv[3] }; // Keep reconnecting forever for (;;) { ////////////////////////////////////////////////// // Establish the connection // Try to connect std::cout << "Initiating connection..." << std::endl; auto connectionError = AdsAddRoute(amsAddress.netId, ipAddress); // Check for error if (connectionError != ADSERR_NOERR) { std::cout << "Connection failed: 0x" << std::hex << connectionError << std::dec << std::endl; // Sleep for 1s and retry std::this_thread::sleep_for(1s); continue; } // Open a port auto port = AdsPortOpenEx(); ////////////////////////////////////////////////// // Add handlers and service the connection // Add a handler for the connection loss notification std::uint32_t connectionLossNotification; AdsAddSyntheticDeviceNotificationReqEx( port, &amsAddress, NOTIFY_CONNECTION_LOST, connectionLostCallback, 0, &connectionLossNotification); // Add a handler for the PLC state const AdsNotificationAttrib deviceStateNotificationAttrib { .cbLength = sizeof(std::uint16_t), .nTransMode = ADSTRANS_SERVERONCHA }; std::uint32_t deviceStateNotification; auto addNotificationError = AdsSyncAddDeviceNotificationReqEx(port, &amsAddress, ADSIGRP_DEVICE_DATA, ADSIOFFS_DEVDATA_ADSSTATE, &deviceStateNotificationAttrib, deviceStateCallback, 0, &deviceStateNotification); // The connection might have failed before the call to AdsAddSyntheticDeviceNotificationReqEx, so check for an error here as well if (addNotificationError == ADSERR_NOERR) { std::cout << "Connected." << std::endl; // Wait for the connection to terminate connectionLostSemaphore.acquire(); } else { std::cout << "AdsSyncAddDeviceNotificationReqEx failed: 0x" << std::hex << addNotificationError << std::dec << std::endl; } ////////////////////////////////////////////////// // Shut down the connection and clean up std::cout << "Shutting down connection." << std::endl; // Clean everything up before trying to re-add the route AdsSyncDelDeviceNotificationReqEx(port, &amsAddress, deviceStateNotification); AdsDelSyntheticDeviceNotificationReqEx(port, &amsAddress, connectionLossNotification); AdsPortCloseEx(port); AdsDelRoute(amsAddress.netId); // Clean up any dangling connection loss notifications void(connectionLostSemaphore.try_acquire()); // Sleep for 1s before reconnecting std::this_thread::sleep_for(1s); } }
georg-emg
commented
Nov 14, 2025
Ok, here is a sample for the use NOTIFY_NOTIFICATION_RCV. This application just logs all the notifications from a single AoE frame to standard out as a block.
// This is a sample application demonstarting the use of the proposed NOTIFY_NOTIFICATION_RCV synthetic notification // // This sample requires C++23 // // This application will connect to a TwinCAT PLC and register notifications for symbols specified on the command // line. It will then group change notifications into transactions, where each transaction contains all the changes // from a single DEVICE_NOTIFICATION AoE frame. // // To simplify the code, error handling has been largely omitted. // // Without the NOTIFY_NOTIFICATION_RCV, it would not be possible to group the change notifications together. This // could introduce a critical bottleneck in situations where a lot of PLC variables change at the same time. // // To test: // // - Start the application and connect to a TwinCAT PLC instance. // - Perform an action in the PLC that will change multiple of the watched symbols within the same cycle // - Verify that the changes are grouped into a single transaction. #include <AdsLib.h> #include <compare> #include <cstdlib> #include <iomanip> #include <iostream> #include <span> #include <string> #include <utility> #include <vector> // The symbol names, in the order they were specified on the command line std::vector<std::string> symbolNames; // Information about a symbol change struct ChangeInfo { std::size_t symbolIndex; std::vector<std::byte> data; }; // The list of changes since the last transaction std::vector<ChangeInfo> changeLog; // Callback for change notifications void symbolChangeCallback(const AmsAddr *amsAddress, const AdsNotificationHeader *header, std::uint32_t index) { // Sanity check if (std::cmp_greater_equal(index, symbolNames.size())) { std::cout << "Received invalid notification with index " << index << std::endl; return; } // Extract the data std::span data { reinterpret_cast<const std::byte *>(header + 1), std::size_t(header->cbSampleSize) }; // Add an entry to the log changeLog.push_back({ .symbolIndex { std::size_t(index) }, .data { std::from_range, data } }); } // Callback for notification received notifications void notificationReceivedCallback(const AmsAddr *amsAddress, std::uint32_t type, std::uint32_t userValue) { // This callback just outputs the list of changes to std::cout. A real-life application could do // any of the following action, e.g.: // // - Write the changes to a database using a single database transaction // - Send the changes to a REST interface using a single HTTP POST request // - Send the changes to a UI thread using a single event // - etc. etc. etc. // // Each of these actions benefit heavily from batching together multiple changes. When dealing with a // large number of symbols, having to process each change individually could introduce a critical // performance bottleneck. std::cout << "BEGIN NOTIFICATION FRAME\n["; for (bool isFirst = true; auto &&[symbolIndex, data] : changeLog) { if (!std::exchange(isFirst, false)) { std::cout << ","; } std::cout << "\n {\n \"symbol\": \"" << symbolNames[symbolIndex] << "\",\n \"data\": "; char prefix { '"' }; for (auto &&byte : data) { std::cout << std::exchange(prefix, ':') << std::hex << std::setfill('0') << std::setw(2) << static_cast<unsigned int>(byte) << std::dec << std::setfill(' '); } std::cout << "\"\n }"; } std::cout <<"\n]\nEND NOTIFICATION FRAME" << std::endl; changeLog.clear(); } auto main(int argc, char *argv[]) -> int { // Decode the parameters if (argc < 5) { std::cerr << "Usage: <AMS Net-ID> <AMS Port> <IP address> <symbol name>..." << std::endl; return EXIT_FAILURE; } AmsAddr amsAddress { .netId { argv[1] }, .port { std::uint16_t(std::atoi(argv[2])) } }; const char *ipAddress { argv[3] }; for (int index = 4; index < argc; ++index) { symbolNames.emplace_back(argv[index]); } std::cout << "Press Enter to exit.\nLog:" << std::endl; // Establish the connection AdsAddRoute(amsAddress.netId, ipAddress); // Open a port and add the notification received callback auto port = AdsPortOpenEx(); // Add a handler for the notification received notification std::uint32_t notificationReceivedNotification; AdsAddSyntheticDeviceNotificationReqEx( port, &amsAddress, NOTIFY_NOTIFICATION_RCV, notificationReceivedCallback, 0, ¬ificationReceivedNotification); // Remember all the notifications and handles, so we can release them struct SymbolInfo { std::uint32_t handle; std::uint32_t notification; }; std::vector<SymbolInfo> symbolInfos; // Add notifications for all the symbols for (std::size_t index = 0; index < symbolNames.size(); ++index) { SymbolInfo symbolInfo; // Get a handle auto &&symbolName { symbolNames[index] }; std::uint32_t bytesRead; auto lookupError = AdsSyncReadWriteReqEx2(port, &amsAddress, ADSIGRP_SYM_HNDBYNAME, 0, sizeof(symbolInfo.handle), &symbolInfo.handle, std::uint32_t(symbolName.size()), symbolName.data(), &bytesRead); if (lookupError != ADSERR_NOERR) { std::cerr << "Error looking up symbol \"" << symbolName << "\": 0x" << std::hex << lookupError << std::dec << std::endl; continue; } // Add a notification for it const AdsNotificationAttrib attrib { .cbLength = sizeof(std::uint16_t), .nTransMode = ADSTRANS_SERVERONCHA }; AdsSyncAddDeviceNotificationReqEx(port, &amsAddress, ADSIGRP_SYM_VALBYHND, symbolInfo.handle, &attrib, symbolChangeCallback, std::uint32_t(index), &symbolInfo.notification); symbolInfos.push_back(symbolInfo); } // Wait for a key press std::cin.ignore(); // Clean everything up before exiting for (auto &&[handle, notification] : symbolInfos) { AdsSyncDelDeviceNotificationReqEx(port, &amsAddress, notification); AdsSyncWriteReqEx(port, &amsAddress, ADSIGRP_SYM_RELEASEHND, 0, sizeof(handle), &handle); } AdsDelSyntheticDeviceNotificationReqEx(port, &amsAddress, notificationReceivedNotification); AdsPortCloseEx(port); AdsDelRoute(amsAddress.netId); return EXIT_SUCCESS; }
pbruenn
commented
Nov 21, 2025
Thanks a lot. Sorry for not responding more early. It was a short and busy week :-(.
I struggle with the second usecase RCV. If I understand that correctly: The PLC sends a large notification with many different symbols changed. When we receive the notification and processing it we create a synthetic notification for each symbol and then process each of the symbol changes individually. How is this faster then just splitting the processing in the normal notification handler? We only delay it by the time we need too complete the entire notification. Again, iam not an ADS expert but I have a feeling your problem could be mitigated by tuning AdsNotificationAttrib https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_adsdll2/117553803.html&id= But I might be wrong.
I still like the LOST notification very much. I will try to integrate this within the next few days.
georg-emg
commented
Nov 21, 2025
|
Hi Patrick, There is only one single synthetic notification per frame. TwinCAT will take all the variables that changed in one single PLC cycle and put them into a single Network packet with a single AoE header. So, If variables MAIN.var1, MAIN.var2 and MAIN.var3 change in the same cycle, only a single network packet is sent, that looks something like this (simplified):
This is nothing new, that is how TwinCAT works, and that is how the ADS protocol is designed. This is done, of course, because placing all the change notification together into a single packet greatly reduces network traffic. The existing library, however, will generate only the following notifications for that:
Now, if we want to do some batch processing on the notifications, there currently is no way for us to know that MAIN.var3 is the last notification, and that we can now go ahead and process the changes. For all we know, there could be a fourth change notification in the AoE packet. So all we can do is wait around for a bit to see if maybe the callback gets called a fourth time, and do our processing only after nothing has happened for a certain amount of time. Our changes merely add a final synthetic notification that tells the caller that notifications for all the samples in the packet have been sent, and that it can go ahead and process them now. So the notifications for the same AoE packet in the updated library would look like this:
Now, when we get It is important to understand that this changes nothing in the ADS protocol, or in the way TwinCAT or the notifications work. It is merely an extra callback in the client library to tell the caller that all the samples in the received frame have been processed.
From an implemetation standpoint, Batch processing of changes is very important for applications that need to log or forward changes. Without the possibility of handling changes as a batch, many applications are simply not possible. One of our use-cases, for example, is sending data to an InfluxDB time series database using their HTTP write_lp API. Using |
pbruenn
commented
Nov 26, 2025
Okay, I think I now understand your usecase. But I have more questions. Wouldn't it be more efficient to fill the InfluxDB directly from the PLC? Like its done here:
https://infosys.beckhoff.com/english.php?content=../content/1033/tf6420_tc3_database_server/8117583755.html&id=
Or even use the TwinCAT Analytics Storage Provider:
https://infosys.beckhoff.com/english.php?content=../content/1033/tf3520_tc3_analytics_storage_provider/5759734539.html&id=
Did you consider these TwinCAT functions?
georg-emg
commented
Nov 26, 2025
That was just an example usecase, demonstrating why this functionality is important, and how it would be applied. I'm not suggesting that we are stuck on this particular issue, and that we are in search of a specific solution for that specific problem. What we want to be able to do more generally, is get data from the PLC in an efficient way, and then process it in a number of different ways, only one of which is sending it to InfluxDB.
For us, and for anyone else who has to deal with large amounts of data, it is crucial to be able to eliminate data bottlenecks, and batch processing is the first and most important step in accomplishing that. That is why ADS itself sends change notifications in batches, and that is why the ADS protocol offers things like AdsNotificationAttrib that allow us to fine-tune the batch sizes and spacing. But we need that one little extra step to be able to take full advantage of the potential performance benefits that those features offer, and that is a notification of when the batch is complete. Otherwise, it is very difficult for us to do any batch processing inside our application, which makes it more difficult for us to meet our performance goals.
Overview
This pull request introduces synthetic notifications as a new notification channel in the ADS library.
Unlike standard ADS notifications that originate from target ADS devices, synthetic notifications are generated within the library to inform users about key internal events and states.
Implemented Synthetic Notifications
Two new synthetic notification types have been added:
NOTIFY_CONNECTION_LOST
This synthetic notification type is emitted when the connection to a target ADS device is lost.
Previously, client code could only detect a connection failure indirectly by sending a request and receiving an error response. For applications that rely mainly on ADS notifications to track symbol updates (without performing regular read/write operations), this made it impossible to detect connection loss at runtime.
With NOTIFY_CONNECTION_LOST, the library now notifies registered users whenever any socket operation for a given connection fails, allowing client code to react immediately to connection issues.
NOTIFY_NOTIFICATION_RCV
This synthetic notification type is emitted whenever any ADS notification is received from a target device.
It serves as a lightweight, data-free event that is triggered after the invocation of all ADS notifications contained within an AMS packet, enabling client code to receive a general "summary" notification when any subscribed ADS symbol is updated.
This can be useful in scenarios where multiple symbols are updated via ADS notifications, and a single trigger is needed to perform follow-up actions, such as refreshing a GUI, aggregating data, or committing a database transaction.