From 977677e3afca526186224dc4e031e6886a1ed033 Mon Sep 17 00:00:00 2001 From: Thomas Stoddard Date: Mon, 5 Jan 2026 15:40:14 +0000 Subject: [PATCH] Bindings: Add LiveData and LiveDataMessage support in Python bindings --- bindings/python/CMakeLists.txt | 2 + .../icsneopy/communication/livedata.cpp | 79 +++++++++++ .../communication/message/livedatamessage.cpp | 50 +++++++ bindings/python/icsneopy/device/device.cpp | 5 + bindings/python/icsneopy/icsneocpp.cpp | 4 + communication/livedata.cpp | 11 +- docs/icsneopy/examples.rst | 9 ++ examples/cpp/livedata/src/LiveDataExample.cpp | 45 ++++-- examples/python/livedata/livedata_example.py | 130 ++++++++++++++++++ include/icsneo/communication/livedata.h | 3 +- 10 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 bindings/python/icsneopy/communication/livedata.cpp create mode 100644 bindings/python/icsneopy/communication/message/livedatamessage.cpp create mode 100644 examples/python/livedata/livedata_example.py diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt index 93c93a9..1950278 100644 --- a/bindings/python/CMakeLists.txt +++ b/bindings/python/CMakeLists.txt @@ -22,6 +22,7 @@ pybind11_add_module(icsneopy icsneopy/device/devicetype.cpp icsneopy/communication/network.cpp icsneopy/communication/io.cpp + icsneopy/communication/livedata.cpp icsneopy/communication/message/message.cpp icsneopy/communication/message/canmessage.cpp icsneopy/communication/message/canerrormessage.cpp @@ -34,6 +35,7 @@ pybind11_add_module(icsneopy icsneopy/communication/message/spimessage.cpp icsneopy/communication/message/scriptstatusmessage.cpp icsneopy/communication/message/ethphymessage.cpp + icsneopy/communication/message/livedatamessage.cpp icsneopy/communication/message/callback/messagecallback.cpp icsneopy/communication/message/filter/messagefilter.cpp icsneopy/core/macseccfg.cpp diff --git a/bindings/python/icsneopy/communication/livedata.cpp b/bindings/python/icsneopy/communication/livedata.cpp new file mode 100644 index 0000000..7435714 --- /dev/null +++ b/bindings/python/icsneopy/communication/livedata.cpp @@ -0,0 +1,79 @@ +#include +#include +#include +#include + +#include "icsneo/communication/livedata.h" + +namespace icsneo { + +void init_livedata(pybind11::module_& m) { + // LiveDataValue struct + pybind11::classh(m, "LiveDataValue") + .def(pybind11::init<>()) + .def_readwrite("value", &LiveDataValue::value); + + // LiveDataArgument struct + pybind11::classh(m, "LiveDataArgument") + .def(pybind11::init<>()) + .def_readwrite("object_type", &LiveDataArgument::objectType) + .def_readwrite("object_index", &LiveDataArgument::objectIndex) + .def_readwrite("signal_index", &LiveDataArgument::signalIndex) + .def_readwrite("value_type", &LiveDataArgument::valueType); + + // LiveDataCommand enum + pybind11::native_enum(m, "LiveDataCommand", "enum.IntEnum") + .value("STATUS", LiveDataCommand::STATUS) + .value("SUBSCRIBE", LiveDataCommand::SUBSCRIBE) + .value("UNSUBSCRIBE", LiveDataCommand::UNSUBSCRIBE) + .value("RESPONSE", LiveDataCommand::RESPONSE) + .value("CLEAR_ALL", LiveDataCommand::CLEAR_ALL) + .value("SET_VALUE", LiveDataCommand::SET_VALUE) + .finalize(); + + // LiveDataStatus enum + pybind11::native_enum(m, "LiveDataStatus", "enum.IntEnum") + .value("SUCCESS", LiveDataStatus::SUCCESS) + .value("ERR_UNKNOWN_COMMAND", LiveDataStatus::ERR_UNKNOWN_COMMAND) + .value("ERR_HANDLE", LiveDataStatus::ERR_HANDLE) + .value("ERR_DUPLICATE", LiveDataStatus::ERR_DUPLICATE) + .value("ERR_FULL", LiveDataStatus::ERR_FULL) + .finalize(); + + // LiveDataObjectType enum + pybind11::enum_(m, "LiveDataObjectType") + .value("MISC", LiveDataObjectType::MISC) + .value("SNA", LiveDataObjectType::SNA) + .export_values(); + + // LiveDataValueType enum + pybind11::native_enum(m, "LiveDataValueType", "enum.IntEnum") + .value("GPS_LATITUDE", LiveDataValueType::GPS_LATITUDE) + .value("GPS_LONGITUDE", LiveDataValueType::GPS_LONGITUDE) + .value("GPS_ALTITUDE", LiveDataValueType::GPS_ALTITUDE) + .value("GPS_SPEED", LiveDataValueType::GPS_SPEED) + .value("GPS_VALID", LiveDataValueType::GPS_VALID) + .value("GPS_ENABLE", LiveDataValueType::GPS_ENABLE) + .value("MANUAL_TRIGGER", LiveDataValueType::MANUAL_TRIGGER) + .value("TIME_SINCE_MSG", LiveDataValueType::TIME_SINCE_MSG) + .value("GPS_ACCURACY", LiveDataValueType::GPS_ACCURACY) + .value("GPS_BEARING", LiveDataValueType::GPS_BEARING) + .value("GPS_TIME", LiveDataValueType::GPS_TIME) + .value("GPS_TIME_VALID", LiveDataValueType::GPS_TIME_VALID) + .value("DAQ_ENABLE", LiveDataValueType::DAQ_ENABLE) + .finalize(); + + // LiveDataUtil namespace functions + m.def("get_new_handle", &LiveDataUtil::getNewHandle, + "Generate a new unique LiveData handle"); + + m.def("livedata_value_to_double", &LiveDataUtil::liveDataValueToDouble, + pybind11::arg("val"), + "Convert LiveDataValue to double (32.32 fixed-point to floating-point)"); + + m.def("livedata_double_to_value", &LiveDataUtil::liveDataDoubleToValue, + pybind11::arg("d"), + "Convert double to LiveDataValue (32.32 fixed-point format). Returns LiveDataValue or None on failure."); +} + +} // namespace icsneo diff --git a/bindings/python/icsneopy/communication/message/livedatamessage.cpp b/bindings/python/icsneopy/communication/message/livedatamessage.cpp new file mode 100644 index 0000000..24a0fc9 --- /dev/null +++ b/bindings/python/icsneopy/communication/message/livedatamessage.cpp @@ -0,0 +1,50 @@ +#include +#include +#include +#include + +#include "icsneo/communication/message/livedatamessage.h" + +namespace icsneo { + +void init_livedatamessage(pybind11::module_& m) { + // LiveDataMessage base class + pybind11::classh(m, "LiveDataMessage") + .def(pybind11::init<>()) + .def_readwrite("handle", &LiveDataMessage::handle) + .def_readwrite("cmd", &LiveDataMessage::cmd); + + // LiveDataCommandMessage (for subscribe/unsubscribe) + pybind11::classh(m, "LiveDataCommandMessage") + .def(pybind11::init<>()) + .def_readwrite("update_period", &LiveDataCommandMessage::updatePeriod) + .def_readwrite("expiration_time", &LiveDataCommandMessage::expirationTime) + .def_readwrite("args", &LiveDataCommandMessage::args) + .def("append_signal_arg", &LiveDataCommandMessage::appendSignalArg, + pybind11::arg("value_type"), + "Append a signal argument to the command message"); + + // LiveDataValueMessage (received values) + pybind11::classh(m, "LiveDataValueMessage") + .def(pybind11::init<>()) + .def_readwrite("num_args", &LiveDataValueMessage::numArgs) + .def_readwrite("values", &LiveDataValueMessage::values); + + // LiveDataStatusMessage (status responses) + pybind11::classh(m, "LiveDataStatusMessage") + .def(pybind11::init<>()) + .def_readwrite("requested_command", &LiveDataStatusMessage::requestedCommand) + .def_readwrite("status", &LiveDataStatusMessage::status); + + // LiveDataSetValueMessage (for setting values) + pybind11::classh(m, "LiveDataSetValueMessage") + .def(pybind11::init<>()) + .def_readwrite("args", &LiveDataSetValueMessage::args) + .def_readwrite("values", &LiveDataSetValueMessage::values) + .def("append_set_value", &LiveDataSetValueMessage::appendSetValue, + pybind11::arg("value_type"), + pybind11::arg("value"), + "Append a value to set in the message"); +} + +} // namespace icsneo diff --git a/bindings/python/icsneopy/device/device.cpp b/bindings/python/icsneopy/device/device.cpp index 472f3b4..4454a47 100644 --- a/bindings/python/icsneopy/device/device.cpp +++ b/bindings/python/icsneopy/device/device.cpp @@ -52,6 +52,11 @@ void init_device(pybind11::module_& m) { .def("start_script", &Device::startScript, pybind11::call_guard()) .def("stop_script", &Device::stopScript, pybind11::call_guard()) .def("supports_tc10", &Device::supportsTC10) + .def("supports_live_data", &Device::supportsLiveData) + .def("subscribe_live_data", &Device::subscribeLiveData, pybind11::arg("message"), pybind11::call_guard()) + .def("unsubscribe_live_data", &Device::unsubscribeLiveData, pybind11::arg("handle"), pybind11::call_guard()) + .def("clear_all_live_data", &Device::clearAllLiveData, pybind11::call_guard()) + .def("set_value_live_data", &Device::setValueLiveData, pybind11::arg("message"), pybind11::call_guard()) .def("transmit", pybind11::overload_cast>(&Device::transmit), pybind11::call_guard()) .def("upload_coremini", [](Device& device, std::string& path, Disk::MemoryType memType) { std::ifstream ifs(path, std::ios::binary); return device.uploadCoremini(ifs, memType); }, pybind11::call_guard()) .def("write_macsec_config", &Device::writeMACsecConfig, pybind11::call_guard()) diff --git a/bindings/python/icsneopy/icsneocpp.cpp b/bindings/python/icsneopy/icsneocpp.cpp index 329f884..01cb7b6 100644 --- a/bindings/python/icsneopy/icsneocpp.cpp +++ b/bindings/python/icsneopy/icsneocpp.cpp @@ -35,6 +35,8 @@ void init_version(pybind11::module_&); void init_flexray(pybind11::module_& m); void init_idevicesettings(pybind11::module_&); void init_ethphymessage(pybind11::module_&); +void init_livedata(pybind11::module_&); +void init_livedatamessage(pybind11::module_&); PYBIND11_MODULE(icsneopy, m) { pybind11::options options; @@ -48,6 +50,7 @@ PYBIND11_MODULE(icsneopy, m) { init_devicetype(m); init_network(m); init_io(m); + init_livedata(m); init_message(m); init_canmessage(m); init_canerrormessage(m); @@ -60,6 +63,7 @@ PYBIND11_MODULE(icsneopy, m) { init_macsecconfig(m); init_scriptstatusmessage(m); init_spimessage(m); + init_livedatamessage(m); init_messagefilter(m); init_messagecallback(m); init_diskdriver(m); diff --git a/communication/livedata.cpp b/communication/livedata.cpp index a54804d..af46f97 100644 --- a/communication/livedata.cpp +++ b/communication/livedata.cpp @@ -19,7 +19,8 @@ double liveDataValueToDouble(const LiveDataValue& val) { return val.value * liveDataFixedPointToDouble; } -bool liveDataDoubleToValue(const double& dFloat, LiveDataValue& value) { +std::optional liveDataDoubleToValue(const double& dFloat) { + LiveDataValue value; union { struct { @@ -56,23 +57,23 @@ bool liveDataDoubleToValue(const double& dFloat, LiveDataValue& value) { value.value = CminiFixedPt.ValueLarge; if(dFloat == (double)0.0) - return true; + return value; //check if double can be stored as 32.32 // 0x1 0000 0000 0000 0000 * CM_FIXED_POINT_TO_DOUBLEVALUE = 0x1 0000 0000 if(dFloat > INT32_MAX_DOUBLE || dFloat < INT32_MIN_DOUBLE) { EventManager::GetInstance().add(APIEvent::Type::FixedPointOverflow, APIEvent::Severity::Error); - return false; + return std::nullopt; } // Use absolute value for minimum fixed point check double absFloat = (dFloat < 0.0) ? -dFloat : dFloat; if(absFloat < MIN_FIXED_POINT_DOUBLE) { EventManager::GetInstance().add(APIEvent::Type::FixedPointPrecision, APIEvent::Severity::Error); - return false; + return std::nullopt; } - return true; + return value; } } // namespace LiveDataUtil diff --git a/docs/icsneopy/examples.rst b/docs/icsneopy/examples.rst index 5cb5a7c..9551e5b 100644 --- a/docs/icsneopy/examples.rst +++ b/docs/icsneopy/examples.rst @@ -27,6 +27,15 @@ Complete CAN Example :language: python +LiveData Subscription and Monitoring +===================================== + +:download:`Download example <../../examples/python/livedata/livedata_example.py>` + +.. literalinclude:: ../../examples/python/livedata/livedata_example.py + :language: python + + Transmit Ethernet frames on Ethernet 01 ======================================== diff --git a/examples/cpp/livedata/src/LiveDataExample.cpp b/examples/cpp/livedata/src/LiveDataExample.cpp index 9819f7d..7b6ee32 100644 --- a/examples/cpp/livedata/src/LiveDataExample.cpp +++ b/examples/cpp/livedata/src/LiveDataExample.cpp @@ -29,7 +29,7 @@ int main() { } std::cout << "OK" << std::endl; - // Create a subscription message for the GPS signals + // Create a subscription message for the GPS signals and TIME_SINCE_MSG std::cout << "\tSending a live data subscribe command... "; auto msg = std::make_shared(); msg->appendSignalArg(icsneo::LiveDataValueType::GPS_LATITUDE); @@ -37,6 +37,7 @@ int main() { msg->appendSignalArg(icsneo::LiveDataValueType::GPS_ACCURACY); msg->appendSignalArg(icsneo::LiveDataValueType::DAQ_ENABLE); msg->appendSignalArg(icsneo::LiveDataValueType::MANUAL_TRIGGER); + msg->appendSignalArg(icsneo::LiveDataValueType::TIME_SINCE_MSG); msg->cmd = icsneo::LiveDataCommand::SUBSCRIBE; msg->handle = icsneo::LiveDataUtil::getNewHandle(); msg->updatePeriod = std::chrono::milliseconds(100); @@ -44,6 +45,9 @@ int main() { // Transmit the subscription message ret = device->subscribeLiveData(msg); std::cout << (ret ? "OK" : "FAIL") << std::endl; + if (!ret) { + std::cout << "\t\tError: " << icsneo::GetLastError() << std::endl; + } // Register a handler that uses the data after it arrives every ~100ms std::cout << "\tStreaming messages for 3 seconds... " << std::endl << std::endl; @@ -53,19 +57,21 @@ int main() { switch(ldMsg->cmd) { case icsneo::LiveDataCommand::STATUS: { auto msg2 = std::dynamic_pointer_cast(message); - std::cout << "[Handle] " << ldMsg->handle << std::endl; - std::cout << "[Requested Command] " << msg2->requestedCommand << std::endl; - std::cout << "[Status] " << msg2->status << std::endl << std::endl; + std::cout << "[STATUS Message]" << std::endl; + std::cout << " Handle: " << ldMsg->handle << std::endl; + std::cout << " Requested Command: " << msg2->requestedCommand << std::endl; + std::cout << " Status: " << msg2->status << std::endl << std::endl; break; } case icsneo::LiveDataCommand::RESPONSE: { auto valueMsg = std::dynamic_pointer_cast(message); if((valueMsg->handle == msg->handle) && (valueMsg->values.size() == msg->args.size())) { - std::cout << "[Handle] " << msg->handle << std::endl; - std::cout << "[Values] " << valueMsg->numArgs << std::endl; + std::cout << "[Response Message]" << std::endl; + std::cout << " Handle: " << msg->handle << std::endl; + std::cout << " Number of Values: " << valueMsg->numArgs << std::endl; for(uint32_t i = 0; i < valueMsg->numArgs; ++i) { - std::cout << "[" << msg->args[i]->valueType << "] "; + std::cout << " [" << msg->args[i]->valueType << "] "; auto scaledValue = icsneo::LiveDataUtil::liveDataValueToDouble(*valueMsg->values[i]); std::cout << scaledValue << std::endl; } @@ -86,22 +92,33 @@ int main() { setValMsg->cmd = icsneo::LiveDataCommand::SET_VALUE; setValMsg->handle = msg->handle; // Convert the value format - icsneo::LiveDataValue ldValueDAQEnable; - icsneo::LiveDataValue ldValueManTrig; - if (!icsneo::LiveDataUtil::liveDataDoubleToValue(val / 3, ldValueDAQEnable) || - !icsneo::LiveDataUtil::liveDataDoubleToValue(val, ldValueManTrig)) { + auto ldValueDAQEnable = icsneo::LiveDataUtil::liveDataDoubleToValue(val / 3); + auto ldValueManTrig = icsneo::LiveDataUtil::liveDataDoubleToValue(val); + auto ldValueTimeSinceMsg = icsneo::LiveDataUtil::liveDataDoubleToValue(val); + if (!ldValueDAQEnable || !ldValueManTrig || !ldValueTimeSinceMsg) { + std::cout << "\tError: Failed to convert values" << std::endl; break; } - setValMsg->appendSetValue(icsneo::LiveDataValueType::DAQ_ENABLE, ldValueDAQEnable); - setValMsg->appendSetValue(icsneo::LiveDataValueType::MANUAL_TRIGGER, ldValueManTrig); - device->setValueLiveData(setValMsg); + setValMsg->appendSetValue(icsneo::LiveDataValueType::DAQ_ENABLE, *ldValueDAQEnable); + setValMsg->appendSetValue(icsneo::LiveDataValueType::MANUAL_TRIGGER, *ldValueManTrig); + setValMsg->appendSetValue(icsneo::LiveDataValueType::TIME_SINCE_MSG, *ldValueTimeSinceMsg); + + std::cout << "\tSetting values: DAQ_ENABLE=" << (val / 3) + << ", MANUAL_TRIGGER=" << val + << ", TIME_SINCE_MSG=" << val << std::endl; + + if (!device->setValueLiveData(setValMsg)) { + std::cout << "\tError setting values: " << icsneo::GetLastError() << std::endl; + } ++val; // Run handler for three seconds to observe the signal data std::this_thread::sleep_for(std::chrono::seconds(3)); } // Unsubscribe from the GPS signals and run handler for one more second // Unsubscription only requires a valid in-use handle, in this case from our previous subscription + std::cout << "\tUnsubscribing... "; ret = device->unsubscribeLiveData(msg->handle); + std::cout << (ret ? "OK" : "FAIL") << std::endl; // The handler should no longer print values std::this_thread::sleep_for(std::chrono::seconds(1)); device->removeMessageCallback(handler); diff --git a/examples/python/livedata/livedata_example.py b/examples/python/livedata/livedata_example.py new file mode 100644 index 0000000..4c43378 --- /dev/null +++ b/examples/python/livedata/livedata_example.py @@ -0,0 +1,130 @@ +""" +LiveData subscription and monitoring example using icsneopy library. + +""" + +import icsneopy +import time +from datetime import timedelta + + +def livedata_example(): + """Subscribe to and monitor LiveData signals.""" + devices = icsneopy.find_all_devices() + if not devices: + raise RuntimeError("No devices found") + + device = devices[0] + print(f"Using device: {device}") + + try: + if not device.open(): + raise RuntimeError("Failed to open device") + + if not device.go_online(): + raise RuntimeError("Failed to go online") + + device.enable_message_polling() + + # Create subscription message + msg = icsneopy.LiveDataCommandMessage() + msg.handle = icsneopy.get_new_handle() + msg.cmd = icsneopy.LiveDataCommand.SUBSCRIBE + msg.update_period = timedelta(milliseconds=500) + msg.expiration_time = timedelta(milliseconds=0) + + # Subscribe to various LiveData signals + msg.append_signal_arg(icsneopy.LiveDataValueType.GPS_LATITUDE) + msg.append_signal_arg(icsneopy.LiveDataValueType.GPS_LONGITUDE) + msg.append_signal_arg(icsneopy.LiveDataValueType.GPS_ACCURACY) + msg.append_signal_arg(icsneopy.LiveDataValueType.DAQ_ENABLE) + msg.append_signal_arg(icsneopy.LiveDataValueType.MANUAL_TRIGGER) + msg.append_signal_arg(icsneopy.LiveDataValueType.TIME_SINCE_MSG) + + print("\nSubscribing to LiveData signals...") + if not device.subscribe_live_data(msg): + raise RuntimeError(f"Subscription failed: {icsneopy.get_last_error()}") + + print("Subscription successful") + print("\nMonitoring LiveData for 5 seconds...") + + response_count = 0 + start_time = time.time() + + while time.time() - start_time < 5: + result = device.get_messages() + messages = result[0] if isinstance(result, tuple) else result + + for m in messages: + if isinstance(m, icsneopy.LiveDataStatusMessage): + if m.handle == msg.handle: + print(f"\n[Status] Command: {m.requested_command}, Status: {m.status}") + + elif isinstance(m, icsneopy.LiveDataValueMessage): + if m.handle == msg.handle: + response_count += 1 + print(f"\n[Response #{response_count}]") + signal_names = ["GPS_LAT", "GPS_LON", "GPS_ACC", + "DAQ_EN", "MAN_TRIG", "TIME_SINCE"] + for idx, val in enumerate(m.values): + value = icsneopy.livedata_value_to_double(val) + name = signal_names[idx] if idx < len(signal_names) else f"Signal_{idx}" + print(f" {name:12s}: {value:10.2f}") + + time.sleep(0.1) + + print(f"\nReceived {response_count} response messages") + + # Demonstrate setting values + print("\nSetting custom values...") + set_msg = icsneopy.LiveDataSetValueMessage() + set_msg.handle = icsneopy.get_new_handle() + set_msg.cmd = icsneopy.LiveDataCommand.SET_VALUE + + # Set DAQ_ENABLE + value = icsneopy.livedata_double_to_value(1.0) + if value: + set_msg.append_set_value(icsneopy.LiveDataValueType.DAQ_ENABLE, value) + + # Set MANUAL_TRIGGER + value = icsneopy.livedata_double_to_value(1.0) + if value: + set_msg.append_set_value(icsneopy.LiveDataValueType.MANUAL_TRIGGER, value) + + if device.set_value_live_data(set_msg): + print("Values set successfully") + time.sleep(0.5) + + # Check the results + result = device.get_messages() + messages = result[0] if isinstance(result, tuple) else result + for m in messages: + if isinstance(m, icsneopy.LiveDataStatusMessage): + if m.handle == set_msg.handle: + print(f" Set status: {m.status}") + + # Keep device awake by resetting TIME_SINCE_MSG + print("\nResetting TIME_SINCE_MSG to keep device awake...") + reset_msg = icsneopy.LiveDataSetValueMessage() + reset_msg.handle = icsneopy.get_new_handle() + reset_msg.cmd = icsneopy.LiveDataCommand.SET_VALUE + + value = icsneopy.livedata_double_to_value(0.0) + if value: + reset_msg.append_set_value(icsneopy.LiveDataValueType.TIME_SINCE_MSG, value) + if device.set_value_live_data(reset_msg): + print("TIME_SINCE_MSG reset to 0") + + # Unsubscribe + print("\nUnsubscribing...") + if device.unsubscribe_live_data(msg.handle): + print("Unsubscribed successfully") + + finally: + device.close() + print("\nDevice closed") + + +if __name__ == "__main__": + livedata_example() + diff --git a/include/icsneo/communication/livedata.h b/include/icsneo/communication/livedata.h index 801dbdb..f9c813d 100644 --- a/include/icsneo/communication/livedata.h +++ b/include/icsneo/communication/livedata.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "icsneo/communication/command.h" #include "icsneo/api/eventmanager.h" @@ -157,7 +158,7 @@ namespace LiveDataUtil LiveDataHandle getNewHandle(); double liveDataValueToDouble(const LiveDataValue& val); -bool liveDataDoubleToValue(const double& dFloat, LiveDataValue& value); +std::optional liveDataDoubleToValue(const double& dFloat); static constexpr uint32_t LiveDataVersion = 1; } // namespace LiveDataUtil