From aee6a7ab51a9f27debdd5b83c6666708a8be2f39 Mon Sep 17 00:00:00 2001 From: Martin Unkel Date: Tue, 3 Sep 2024 13:29:12 +0200 Subject: [PATCH] Release 2.0.0 (#26) * :sparkles: alpha 2.0 * :sparkles: add custom timer callback * :bug: fix read handler, add changelog draft, minor improvements * :art: make init more robust, improve performance, add selection_timeout parameter to server, remove deprecation * :bug: fix test scripts * :coffin: cleanup interfaces * :coffin: improve datetime compatibility, fix test * :coffin: improve test * :bug: fix read command and incoming message order * :bug: fix IC, CS command success * :art: use chrono for time information, update pybind11 to v2.13.4 * :label: undo utc_clock changes and return to system_clock for pybind11 compatibility * :bug: fix tls, fix tests, add connected_at and disconnected_at to Connection * :bug: fix clock sync, improve to string conversion, fix tests * :bug: fix report interval setter on point construction, fix clock in tests * :arrow_down: return to c++ standard 17 for manylinux compatibility * :arrow_up: update dependencies lib60870 to latest, pybind11 to v2.13.5, catch to v3.7.0 * :bug: fix manual reconnect deadlock, improve debug output, add tests * :art: improve tests * :bug: fix select detection in explain_bytes_dict * :zap: allow different parallel commands * update version info * :bug: fix fieldset16 causing to string crash, fix pybind using correct python version * :memo: update documentation and changelog, update docs builder dependencies * :art: c104.Byte32 can construct from bytes and converted to bytes, c104 numbers can be converted to int and float, fix __repr__ toString behavior * :bug: fix message __repr__ toString * :art: remove python 3.6 from build script; add build script for aarch64, armv7l; fix cmake version for arm * :memo: improve number and information documentation * :bug: fix numbers, fix tests, add lib60870 patch, improve docs * :ambulance: add lib60870 patch * :bug: fix build scripts * :memo: improve type hints in documentation, improve source distribution --- CHANGELOG.md | 140 +- CMakeLists.txt | 161 +- Doxyfile | 4 +- MANIFEST.in | 27 +- README.md | 43 +- bin/aarch64-build.sh | 27 + bin/armv7l-build.sh | 23 + bin/linux-build.sh | 1 - bin/win-build.bat | 3 +- depends/catch | 2 +- depends/lib60870 | 2 +- depends/lib60870.patch | 29 + depends/mbedtls | 2 +- depends/pybind11 | 2 +- docs/requirements.txt | 4 +- docs/source/changelog.rst | 265 +++ docs/source/conf.py | 4 +- .../core/enums/binarycounterquality.rst | 5 + .../source/core/enums/commandprocessstate.rst | 5 + .../core/enums/cs101causeofinitialization.rst | 5 + docs/source/core/enums/debug.rst | 7 - docs/source/core/enums/doublepointvalue.rst | 5 + .../{informationtype.rst => eventstate.rst} | 4 +- docs/source/core/enums/fieldset16.rst | 5 + docs/source/core/enums/iec608705typeid.rst | 5 + docs/source/core/enums/index.rst | 12 +- docs/source/core/enums/outputcircuits.rst | 5 + docs/source/core/enums/startevents.rst | 5 + docs/source/core/enums/stepcommandvalue.rst | 5 + docs/source/core/enums/tlsversion.rst | 5 + docs/source/core/index.rst | 1 + docs/source/core/numbers/byte32.rst | 6 + docs/source/core/numbers/index.rst | 15 + docs/source/core/numbers/limitedint16.rst | 6 + docs/source/core/numbers/limitedint7.rst | 6 + docs/source/core/numbers/limiteduint16.rst | 6 + docs/source/core/numbers/limiteduint5.rst | 6 + docs/source/core/numbers/limiteduint7.rst | 6 + docs/source/core/numbers/normalizedfloat.rst | 6 + docs/source/core/object/index.rst | 1 + docs/source/core/object/information.rst | 78 + docs/source/index.rst | 53 +- docs/source/install.rst | 9 + docs/source/objects.inv | Bin 11868 -> 23962 bytes docs/source/python/enum/coi.rst | 7 + docs/source/python/enum/commandmode.rst | 1 - docs/source/python/enum/connectionstate.rst | 1 - docs/source/python/enum/cot.rst | 1 - docs/source/python/enum/data.rst | 8 - docs/source/python/enum/double.rst | 1 - docs/source/python/enum/eventstate.rst | 1 - docs/source/python/enum/index.rst | 2 +- docs/source/python/enum/init.rst | 1 - docs/source/python/enum/qoc.rst | 1 - docs/source/python/enum/qoi.rst | 1 - docs/source/python/enum/responsestate.rst | 1 - docs/source/python/enum/step.rst | 1 - docs/source/python/enum/tlsversion.rst | 1 - docs/source/python/enum/type.rst | 1 - docs/source/python/enum/umc.rst | 1 - .../python/enumset/binarycounterquality.rst | 7 + docs/source/python/enumset/debug.rst | 1 - docs/source/python/enumset/index.rst | 4 + docs/source/python/enumset/outputcircuits.rst | 7 + docs/source/python/enumset/packedsingle.rst | 7 + docs/source/python/enumset/quality.rst | 1 - docs/source/python/enumset/startevents.rst | 7 + docs/source/python/functions.rst | 8 - docs/source/python/index.rst | 2 + docs/source/python/information/binarycmd.rst | 8 + .../python/information/binarycounterinfo.rst | 8 + docs/source/python/information/binaryinfo.rst | 8 + docs/source/python/information/doublecmd.rst | 8 + docs/source/python/information/doubleinfo.rst | 8 + docs/source/python/information/index.rst | 26 + .../source/python/information/information.rst | 15 + .../python/information/normalizedcmd.rst | 8 + .../python/information/normalizedinfo.rst | 8 + .../information/protectioncircuitinfo.rst | 8 + .../information/protectioneventinfo.rst | 8 + .../information/protectionstartinfo.rst | 8 + docs/source/python/information/scaledcmd.rst | 8 + docs/source/python/information/scaledinfo.rst | 8 + docs/source/python/information/shortcmd.rst | 8 + docs/source/python/information/shortinfo.rst | 8 + docs/source/python/information/singlecmd.rst | 8 + docs/source/python/information/singleinfo.rst | 8 + .../python/information/statusandchanged.rst | 8 + docs/source/python/information/stepcmd.rst | 8 + docs/source/python/information/stepinfo.rst | 8 + docs/source/python/number/byte32.rst | 8 + docs/source/python/number/index.rst | 13 + docs/source/python/number/int16.rst | 8 + docs/source/python/number/int7.rst | 8 + docs/source/python/number/normalizedfloat.rst | 8 + docs/source/python/number/uint16.rst | 8 + docs/source/python/number/uint5.rst | 8 + docs/source/python/number/uint7.rst | 8 + examples/dump_client.py | 118 ++ examples/simple_client.py | 14 +- examples/simple_server.py | 26 +- pyproject.toml | 4 +- setup.cfg | 5 +- setup.py | 2 +- src/Client.cpp | 213 +- src/Client.h | 82 +- src/Server.cpp | 853 ++++---- src/Server.h | 101 +- src/enums.cpp | 296 ++- src/enums.h | 115 +- src/main_client.cpp | 74 +- src/main_server.cpp | 127 +- src/module/Callback.h | 22 +- src/module/GilAwareMutex.h | 13 +- src/numbers.h | 302 +++ src/object/DataPoint.cpp | 1260 +++++++++--- src/object/DataPoint.h | 218 +- src/object/Information.cpp | 489 +++++ src/object/Information.h | 880 ++++++++ src/object/Station.cpp | 35 +- src/object/Station.h | 36 +- src/python.cpp | 1828 ++++++++++++----- src/remote/Connection.cpp | 476 +++-- src/remote/Connection.h | 58 +- src/remote/Helper.cpp | 2 +- src/remote/TransportSecurity.cpp | 15 +- src/remote/TransportSecurity.h | 8 + src/remote/message/IMessageInterface.h | 53 +- src/remote/message/IncomingMessage.cpp | 678 +++--- src/remote/message/IncomingMessage.h | 28 +- src/remote/message/OutgoingMessage.cpp | 9 +- src/remote/message/OutgoingMessage.h | 2 +- src/remote/message/PointCommand.cpp | 109 +- src/remote/message/PointCommand.h | 22 +- src/remote/message/PointMessage.cpp | 231 ++- src/remote/message/PointMessage.h | 11 +- src/types.cpp | 110 +- src/types.h | 59 +- tests/client.py | 42 +- tests/client_reconnect.py | 32 + tests/loop.py | 33 + tests/server.py | 85 +- tests/server_passive.py | 38 + tests/test.py | 94 +- tests/test_object_datapoint.cpp | 62 +- 145 files changed, 7958 insertions(+), 2759 deletions(-) create mode 100644 bin/aarch64-build.sh create mode 100644 bin/armv7l-build.sh create mode 100644 depends/lib60870.patch create mode 100644 docs/source/changelog.rst create mode 100644 docs/source/core/enums/binarycounterquality.rst create mode 100644 docs/source/core/enums/commandprocessstate.rst create mode 100644 docs/source/core/enums/cs101causeofinitialization.rst create mode 100644 docs/source/core/enums/doublepointvalue.rst rename docs/source/core/enums/{informationtype.rst => eventstate.rst} (66%) create mode 100644 docs/source/core/enums/fieldset16.rst create mode 100644 docs/source/core/enums/iec608705typeid.rst create mode 100644 docs/source/core/enums/outputcircuits.rst create mode 100644 docs/source/core/enums/startevents.rst create mode 100644 docs/source/core/enums/stepcommandvalue.rst create mode 100644 docs/source/core/enums/tlsversion.rst create mode 100644 docs/source/core/numbers/byte32.rst create mode 100644 docs/source/core/numbers/index.rst create mode 100644 docs/source/core/numbers/limitedint16.rst create mode 100644 docs/source/core/numbers/limitedint7.rst create mode 100644 docs/source/core/numbers/limiteduint16.rst create mode 100644 docs/source/core/numbers/limiteduint5.rst create mode 100644 docs/source/core/numbers/limiteduint7.rst create mode 100644 docs/source/core/numbers/normalizedfloat.rst create mode 100644 docs/source/core/object/information.rst create mode 100644 docs/source/install.rst create mode 100644 docs/source/python/enum/coi.rst delete mode 100644 docs/source/python/enum/data.rst create mode 100644 docs/source/python/enumset/binarycounterquality.rst create mode 100644 docs/source/python/enumset/outputcircuits.rst create mode 100644 docs/source/python/enumset/packedsingle.rst create mode 100644 docs/source/python/enumset/startevents.rst create mode 100644 docs/source/python/information/binarycmd.rst create mode 100644 docs/source/python/information/binarycounterinfo.rst create mode 100644 docs/source/python/information/binaryinfo.rst create mode 100644 docs/source/python/information/doublecmd.rst create mode 100644 docs/source/python/information/doubleinfo.rst create mode 100644 docs/source/python/information/index.rst create mode 100644 docs/source/python/information/information.rst create mode 100644 docs/source/python/information/normalizedcmd.rst create mode 100644 docs/source/python/information/normalizedinfo.rst create mode 100644 docs/source/python/information/protectioncircuitinfo.rst create mode 100644 docs/source/python/information/protectioneventinfo.rst create mode 100644 docs/source/python/information/protectionstartinfo.rst create mode 100644 docs/source/python/information/scaledcmd.rst create mode 100644 docs/source/python/information/scaledinfo.rst create mode 100644 docs/source/python/information/shortcmd.rst create mode 100644 docs/source/python/information/shortinfo.rst create mode 100644 docs/source/python/information/singlecmd.rst create mode 100644 docs/source/python/information/singleinfo.rst create mode 100644 docs/source/python/information/statusandchanged.rst create mode 100644 docs/source/python/information/stepcmd.rst create mode 100644 docs/source/python/information/stepinfo.rst create mode 100644 docs/source/python/number/byte32.rst create mode 100644 docs/source/python/number/index.rst create mode 100644 docs/source/python/number/int16.rst create mode 100644 docs/source/python/number/int7.rst create mode 100644 docs/source/python/number/normalizedfloat.rst create mode 100644 docs/source/python/number/uint16.rst create mode 100644 docs/source/python/number/uint5.rst create mode 100644 docs/source/python/number/uint7.rst create mode 100644 examples/dump_client.py create mode 100644 src/numbers.h create mode 100644 src/object/Information.cpp create mode 100644 src/object/Information.h create mode 100644 tests/client_reconnect.py create mode 100644 tests/loop.py create mode 100644 tests/server_passive.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fee7d9c..eccb267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,143 @@ # Change log +## v2.0 +### Features +- Support for equipment protection points (*M_EP_TD_1*, *M_EP_TE_1*, *M_EP_TF_1*) and status with change detection (*M_PS_NA_1*) +- Command mode select and execute with automatic selection timeout +- Added point timer callback for extended event driven transmission scenarios +- Added Option *MUTED* to *c104.Init*, to open a connection in muted state +- Extended datetime.datetime support +- Performance and stability improvements +- Improved string representation for all classes +- Improved type safety +### Breaking Changes +- Dropped python 3.6 support, since pybind11 does not suppor +#### c104.Point +The concept of a points value is not enough to support all properties of all protocol messages. therefore the value was replaced by individual information objects. Every point type has a specific information type that stores a specific value type but also other properties. +This also ensures type safety, because there is no automatic cast from a Python number to a required value class. +- Added property **point.info** \ + This container class carry's all protocol message specific properties of a point. + ```python + single_point.info = c104.SingleInfo(True) + double_point.info = c104.DoubleInfo(c104.Double.ON) + step_point.info = c104.StepInfo(c104.Int5(13)) + binary_point.info = c104.BinaryInfo(c104.Byte32(12)) + normalized_point.info = c104.NormalizedInfo(c104.NormalizedFloat(-0.734)) + scaled_point.info = c104.ScaledInfo(c104.Int16(-24533)) + short_point.info = c104.ShortInfo(12.34) + counter_point.info = c104.BinaryCounterInfo(345678) + pe_event_point.info = c104.ProtectionEventInfo(c104.EventState.ON) + pe_start_point.info = c104.ProtectionStartInfo(c104.StartEvents.PhaseL1 | c104.StartEvents.PhaseL2) + pe_circuit_point.info = c104.ProtectionCircuitInfo(c104.OutputCircuits.PhaseL1) + pe_changed_point.info = c104.StatusAndChanged(c104.PackedSingle.I0) + ``` +- Changed signature of **point.value** `float` **->** `Union[None,bool,c104.Double,c104.Step,c104.Int7,c104.Int16,int,c104.Byte32,c104.NormalizedFloat,float,c104.EventState,c104.StartEvents,c104.OutputCircuits,c104.PackedSingle]` \ + The `point.value` property is a shortcut to `point.info.value` for convenience. \ + Example: `single_point.value = False` +- Removed property **point.value_uint32** +- Removed property **point.value_int32** +- Removed property **point.value_float** +- Changed signature of **point.quality** `c104.Quality` **->** `Union[None,c104.Quality,c104.BinaryCounterQuality]` + The `point.quality` property is a shortcut to `point.info.quality` and returns point specific types. For points without quality information this will be None. Calling `point.quality.is_good()` can therefore result in an error, if `point.quality` is **None**. +- Removed **point.set(...)** method \ + Set a new info object `point.info = ...` instead, to update all properties like time and quality than just the value \ + Example: `cl_double_command.set(value=c104.Double.ON, timestamp_ms=1711111111111) -> cl_double_command.info = c104.DoubleCmd(state=c104.Double.ON, qualifier=c104.Qoc.LONG_PULSE, recorded_at=datetime.datetime.fromtimestamp(1711111111.111))` +- Changed **point.report_ms** setter validation + The `report_ms` property must be a positive integer and a **multiple of the tick_rate_ms** of the corresponding server or client +- Removed property **point.updated_at_ms**: `int`, use `point.recorded_at` instead +- Removed property **point.received_at_ms**: `int`, use `point.processed_at` instead +- Removed property **point.sent_at_ms**: `int`, use `point.processed_at` instead +- Removed property **point.reported_at_ms**: `int`, use `point.processed_at` instead +- Added read-only property **point.recorded_at**: `Optional[datetime.datetime]` + The timestamp send with the info via protocol. At the sender side this value will be set on info creation time and updated on info.value assigning. This timestamp will not be updated on point transmission. The property can be None, if the protocol message type does not contain a timestamp. +- Added read-only property **point.processed_at**: `datetime.datetime` + This timestamp stands for the last sending or receiving timestamp of this info. +- Added read-only property **point.selected_by**: `Optional[int]` + If select this will be the originator address, otherwise None +- Changed signature of method **point.transmit**(cause: c104.Cot = c104.Cot.UNKNOWN_COT, qualifier: c104.Qoc = c104.Qoc.NONE) -> point.transmit(cause: c104.Cot) + The qualifier is now part of the info object of command points and can be set via a new info assignment. The cause qualifier does not have a default value anymore so that this argument is obligatory now. +- Changed signature of **point.related_io_address** to accept None as value: `int` **->** `Optional[int]` + This is necessary to accept a value of 0 as a valid io_address. +- Changed signature of **point.on_receive(...)** callback signature from `(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState` to `(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState` \ + The argument `previous_state: dict` was replaced by argument `previous_info: c104.Information`. Since all relevant is accessible via the info object, a dict is not required anymore. Instead, the previous info object will be provided. +- Added callback **point.on_timer(...)** \ + Callback signature function: `(point: c104.Point) -> None` \ + Register callback signature: `point.on_timer(callable=on_timer, interval_ms=1000)` \ + The `timer_ms` property must be a positive integer and a **multiple of the tick_rate_ms** of the corresponding server or client +- Added read-only property **point.interval_ms**: `int` \ + This property defines the interval between two on_timer callback executions. \ + This property can only be changed via the `point.on_timer(...)` method + +#### c104.Station +- Changed signature of method **station.add_point(...)** \ + Parameter `io_address` accepts a value of `0`. \ + Parameter `related_io_address` accepts a value of `0` as valid IOA and a value of `None` as not set + +##### c104.IncomingMessage +- Added read-only property info: Union[...] +- Removed property command_qualifier, use message.info.qualifier instead +- Removed property connection_string +- Removed property value +- Removed property quality + +#### c104.Client +- Changed signature of **constructor** + Reduced default value of argument **command_timeout_ms** from `1000ms` to `100ms`. \ + Reduced default value of argument **tick_rate_ms** from `1000ms` to `100ms`. \ + The minimum tick rate is `50ms`. +- Added read-only property **client.tick_rate_ms**: `int` + +#### Connection +- Added read-only property **connection.connected_at**: `Optional[datetime.datetime]` +- Added read-only property **connection.disconnected_at**: `Optional[datetime.datetime]` +- Add c104.Init.MUTED to connect to a server without activating the message transmission. +- Removed c104.ConnectionState values: OPEN_AWAIT_UNMUTE, OPEN_AWAIT_INTERROGATION, OPEN_AWAIT_CLOCK_SYNC + The connection will change from CLOSED_AWAIT_OPEN to OPEN_MUTED, will then execute the init commands, if any and change the state afterwards to OPEN if init != c104.Init.MUTED. The intermediary states are not required anymore. +- Instead of using to wait for a connection establishment: + while not connection.is_connected: + time.sleep(1) + wait for state open so that not only connection is established but also init commands are finished + while connection.state != c104.ConnectionState.OPEN: + time.sleep(1) + +#### c104.Server +- Changed signature of **constructor** \ + Add argument **select_timeout_ms** to constructor with default value `100ms` \ + Reduced default value of **tick_rate_ms** from `1000ms` to `100ms`. \ + The minimum tick rate is 50ms. +- Added read-only property **client.tick_rate_ms**: `int` + +#### c104 Enums, Numbers and Helper Classes +- Renamed enum property **c104.Qoc.CONTINUOUS** to **c104.Qoc.PERSISTENT** \ + This corresponds to the standard description for Qualifier of command. + +#### c104 Global functions +- Removed deprecated function **c104.add_server(...)**, use `c104.Server()` constructor instead +- Removed deprecated function **c104.remove_server(...)**, remove last reference to server instance instead +- Removed deprecated function **c104.add_client(...)**, use `c104.Client()` constructor instead +- Removed deprecated function **c104.remove_client(...)**, remove last reference to client instance instead + +### Bugfixes +- Accept **io_address=0** for points +- Read property **IncomingMessage.raw** caused SIGABRT +- **Server.active_connection_count** counts also inactive open connections +- fix select detection in **c104.explain_bytes_dict(...)** +- **point.transmit(...)** throws an exception if the same point is in an active transmission +- auto set environment variable **PYTHONUNBUFFERED** to avoid delayed print output from Python callbacks + +### Dependencies +- Update catch2 to 3.7.0 +- Update pybind11 to 2.13.5 +- Update lib60870-C to latest (>2.3.3) +- Update mbedtls to 2.28.8 + +- Todo +- Remove outdated documentation +- Removed message.requires_confirmation ? +- dp_related_ioa call .value() in ctor point , use setter +- Common address 0? +- Timer_ms multiple int of tick rate? + ## v1.18 - Add support for Qualifier of Command for single, double and regulating step commands - Fix transmit updated_at timestamp for time aware point @@ -124,5 +262,5 @@ __New coding convention:__ Caller passes self-reference as first argument to cal - Server.on_receive_raw: Server reference as additional argument in the first place - Server.on_send_raw: Server reference as additional argument in the first place - Server.on_connect: Server reference as additional argument in the first place -- Server.on_clock_sync: Server reference as additional argument in the first placele +- Server.on_clock_sync: Server reference as additional argument in the first place - Server.on_unexpected_message: Server reference as additional argument in the first place diff --git a/CMakeLists.txt b/CMakeLists.txt index f6fa913..ab6854b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,19 +1,20 @@ -cmake_minimum_required(VERSION 3.12) -project(c104) +cmake_minimum_required(VERSION 3.15) +project(c104 LANGUAGES CXX) # ############################################################################## # COMPILE FLAGS # ############################################################################## set(CMAKE_CXX_STANDARD 17) -set(CMAKE_VERBOSE_MAKEFILE on) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) message(STATUS "Processor: ${CMAKE_SYSTEM_PROCESSOR}") message(STATUS "ModulePath: ${CMAKE_MODULE_PATH}") if(DEFINED ENV{TOOLCHAIN}) - message(ERROR "TOOLCHAIN: $ENV{TOOLCHAIN}") + message(STATUS "TOOLCHAIN: $ENV{TOOLCHAIN}") endif() # ############################################################################## @@ -21,12 +22,13 @@ endif() # ############################################################################## message(STATUS "Add Pybind11") +set(Python_FIND_STRATEGY "LOCATION") +set(Python_FIND_REGISTRY "LAST") +set(PYBIND11_FINDPYTHON ON) add_subdirectory(depends/pybind11) -list(APPEND c104_PRIVATE_LIBRARIES pybind11::module pybind11::windows_extras) -list(APPEND c104_tests_PRIVATE_LIBRARIES pybind11::embed - pybind11::windows_extras) -if(NOT DEFINED CMAKE_INTERPROCEDURAL_OPTIMIZATION) - list(APPEND c104_PRIVATE_LIBRARIES pybind11::lto) +list(APPEND c104_tests_PRIVATE_LIBRARIES pybind11::embed) +if(MSVC) + list(APPEND c104_tests_PRIVATE_LIBRARIES pybind11::windows_extras) endif() # ############################################################################## @@ -53,17 +55,71 @@ endif() # ############################################################################## # lib60870-C 2.3.2 # ############################################################################## +message(STATUS "Patch lib60870") + +# test apply the patch +execute_process( + COMMAND git apply --check --ignore-whitespace ../lib60870.patch + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/depends/lib60870 + RESULT_VARIABLE GIT_APPLY_CHECK_RESULT + ERROR_VARIABLE GIT_APPLY_CHECK_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE) + +# If the patch can be applied cleanly (i.e., it has not been applied yet), apply +# the patch +if(GIT_APPLY_CHECK_RESULT EQUAL "0") + execute_process( + COMMAND git apply --ignore-whitespace ../lib60870.patch + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/depends/lib60870 + RESULT_VARIABLE GIT_APPLY_RESULT + ERROR_VARIABLE GIT_APPLY_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE) + + # Check if the patch was applied successfully + if(NOT GIT_APPLY_RESULT EQUAL "0") + message(FATAL_ERROR "> Failed to apply patch: ${GIT_APPLY_ERROR}") + else() + message(STATUS "> Patch applied successfully.") + endif() +else() + message(STATUS "> Patch already applied or cannot be applied") +endif() message(STATUS "Add lib60870") -set(BUILD_COMMON ON) -set(BUILD_HAL ON) -set(WITH_MBEDTLS 1) -message(STATUS "Copy mbedtls") -file( - COPY depends/mbedtls/library depends/mbedtls/include - depends/mbedtls/CMakeLists.txt - DESTINATION - ${PROJECT_SOURCE_DIR}/depends/lib60870/lib60870-C/dependencies/mbedtls-2.28) +set(BUILD_COMMON + ON + CACHE BOOL "Build the platform abstraction layer (HAL)") +set(BUILD_HAL + ON + CACHE BOOL + "Build common code (shared with other libraries - e.g. libiec61850)") +set(BUILD_EXAMPLES + OFF + CACHE BOOL "Build the examples") +set(BUILD_TESTS + OFF + CACHE BOOL "Build the tests") +if(EXISTS "${PROJECT_SOURCE_DIR}/depends/mbedtls/library") + message(STATUS "> copy mbedtls") + file( + MAKE_DIRECTORY + "${PROJECT_SOURCE_DIR}/depends/lib60870/lib60870-C/dependencies/mbedtls-2.28" + ) + file( + COPY depends/mbedtls/library depends/mbedtls/include + depends/mbedtls/CMakeLists.txt + DESTINATION + ${PROJECT_SOURCE_DIR}/depends/lib60870/lib60870-C/dependencies/mbedtls-2.28 + ) +else() + if(EXISTS + "${PROJECT_SOURCE_DIR}/depends/lib60870/lib60870-C/dependencies/mbedtls-2.28/library" + ) + message(STATUS " > Use existing copy of mbedtls") + else() + message(FATAL_ERROR "> mbedtls not found") + endif() +endif() add_subdirectory(depends/lib60870/lib60870-C) list(APPEND c104_PRIVATE_LIBRARIES lib60870) list(APPEND c104_tests_PRIVATE_LIBRARIES lib60870) @@ -78,6 +134,7 @@ include_directories( depends/lib60870/lib60870-C/src/common/inc) set(c104_SOURCES + src/numbers.h src/enums.h src/enums.cpp src/types.cpp @@ -86,6 +143,7 @@ set(c104_SOURCES src/module/ScopedGilAcquire.h src/module/ScopedGilRelease.h src/module/GilAwareMutex.h + src/object/Information.h src/object/DataPoint.h src/object/Station.h src/remote/Helper.h @@ -107,60 +165,59 @@ set(c104_SOURCES src/Client.h src/Server.cpp src/Server.h + src/object/Information.cpp src/object/DataPoint.cpp src/object/Station.cpp src/python.cpp) -add_library(c104 MODULE ${c104_SOURCES}) +pybind11_add_module(c104 MODULE OPT_SIZE ${c104_SOURCES}) if(c104_PRIVATE_LIBRARIES) target_link_libraries(c104 PRIVATE ${c104_PRIVATE_LIBRARIES}) endif() -pybind11_extension(c104) -if(NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug|RelWithDebInfo) - pybind11_strip(c104) -endif() - -set_target_properties( - c104 - PROPERTIES CXX_VISIBILITY_PRESET "hidden" - CUDA_VISIBILITY_PRESET "hidden" - PREFIX "${PYTHON_MODULE_PREFIX}" - SUFFIX "${PYTHON_MODULE_EXTENSION}") - -target_compile_definitions(c104 PRIVATE VERSION_INFO=${C104_VERSION_INFO}) +target_compile_definitions(c104 PRIVATE VERSION_INFO=\"${C104_VERSION_INFO}\") # ############################################################################## # Tests with Catch2 # ############################################################################## -set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF) +if(EXISTS "${PROJECT_SOURCE_DIR}/depends/catch/src" + AND EXISTS "${PROJECT_SOURCE_DIR}/tests/main.cpp") + message(STATUS "Add catch2 and tests") -add_subdirectory(depends/catch) -list(APPEND c104_tests_PRIVATE_LIBRARIES Catch2::Catch2) + add_executable(c104_tests ${c104_SOURCES} tests/test_object_datapoint.cpp + tests/test_object_station.cpp tests/main.cpp) -add_executable(c104_tests ${c104_SOURCES} tests/test_object_datapoint.cpp - tests/test_object_station.cpp tests/main.cpp) + if(TARGET c104_tests) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF) -if(c104_tests_PRIVATE_LIBRARIES) - target_link_libraries(c104_tests PRIVATE ${c104_tests_PRIVATE_LIBRARIES}) -endif() + add_subdirectory(depends/catch) + list(APPEND c104_tests_PRIVATE_LIBRARIES Catch2::Catch2) -include(CTest) -include(Catch) -catch_discover_tests(c104_tests) + if(c104_tests_PRIVATE_LIBRARIES) + target_link_libraries(c104_tests PRIVATE ${c104_tests_PRIVATE_LIBRARIES}) + endif() -add_executable(c104_test_client ${c104_SOURCES} src/main_client.cpp) + include(CTest) + include(Catch) + catch_discover_tests(c104_tests) -if(c104_tests_PRIVATE_LIBRARIES) - target_link_libraries(c104_test_client - PRIVATE ${c104_tests_PRIVATE_LIBRARIES}) -endif() + endif() -add_executable(c104_test_server ${c104_SOURCES} src/main_server.cpp) + add_executable(c104_test_client ${c104_SOURCES} src/main_client.cpp) -if(c104_tests_PRIVATE_LIBRARIES) - target_link_libraries(c104_test_server - PRIVATE ${c104_tests_PRIVATE_LIBRARIES}) + if(c104_tests_PRIVATE_LIBRARIES) + target_link_libraries(c104_test_client + PRIVATE ${c104_tests_PRIVATE_LIBRARIES}) + endif() + + add_executable(c104_test_server ${c104_SOURCES} src/main_server.cpp) + + if(c104_tests_PRIVATE_LIBRARIES) + target_link_libraries(c104_test_server + PRIVATE ${c104_tests_PRIVATE_LIBRARIES}) + endif() +else() + message(STATUS "Skip catch2 and tests") endif() diff --git a/Doxyfile b/Doxyfile index 1cd33ac..62efdba 100644 --- a/Doxyfile +++ b/Doxyfile @@ -918,7 +918,9 @@ WARN_LOGFILE = # Note: If this tag is empty the current directory is searched. INPUT = "../README.md" \ - "src" + "src" \ + "depends/lib60870/lib60870-C/src/inc/api" \ + "depends/lib60870/lib60870-C/src/hal/inc" # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/MANIFEST.in b/MANIFEST.in index 2f1dc22..31482b5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,24 @@ -graft depends +graft depends/lib60870/lib60870-C/config +graft depends/lib60870/lib60870-C/src +include depends/lib60870/lib60870-C/CMakeLists.txt +include depends/lib60870/COPYING +include depends/lib60870.patch + +graft depends/mbedtls/library +graft depends/mbedtls/include +include depends/mbedtls/CMakeLists.txt +include depends/mbedtls/LICENSE + +graft depends/pybind11/include +graft depends/pybind11/pybind11 +graft depends/pybind11/tools +include depends/pybind11/CMakeLists.txt +include depends/pybind11/LICENSE + graft src -include tests/*.cpp +exclude src/main_client.cpp +exclude src/main_server.cpp + global-exclude **/.* -prune **/.* -prune **/docs -include CHANGELOG.md CMakeLists.txt LICENSE README.md pyproject.toml setup.cfg setup.py +prune **/tests +include CMakeLists.txt LICENSE README.md pyproject.toml setup.cfg setup.py diff --git a/README.md b/README.md index 790b23e..f39e523 100644 --- a/README.md +++ b/README.md @@ -104,13 +104,13 @@ The library is used as testing framework for test-automation. ### Operating systems -* Debian/Ubuntu (x64): YES >= 20.04 -* Raspbian (arm32v7): YES +* Manylinux (x86_64): YES +* Manylinux (aarch64): YES +* Raspbian (armv7l): YES * Windows (x64): YES -* Raspbian (aarch64): Not yet tested ### Python versions -* python >= 3.6, < 3.13 +* python >= 3.7, < 3.13 ## Installation Please adjust the version number to the latest version or use a specific version according to your needs. @@ -125,6 +125,8 @@ python3 -m pip install c104 python3 -m pip install c104@git+https://github.com/fraunhofer-fit-dien/iec104-python.git ``` +You need the build requirements, listed under "How to build". + ## Documentation Read more about the **Classes** and their **Properties** in our [read the docs documentation](https://iec104-python.readthedocs.io/python/index.html). @@ -136,12 +138,6 @@ Read more about the **Classes** and their **Properties** in our [read the docs d 1. Add feature requests and report bugs using GitHub's issues 1. Create pull requests - -### How to build for multiple python versions (linux with docker) - -1. Build wheels via docker - ```bash - /bin/bash ./bin/linux-build.sh ``` ### How to build (linux) @@ -152,11 +148,24 @@ Read more about the **Classes** and their **Properties** in our [read the docs d python3 -m pip install --upgrade pip ``` +1. Clone repository + ```bash + git clone --depth=1 --branch=main https://github.com/Fraunhofer-FIT-DIEN/iec104-python.git + cd iec104-python + git submodule update --init + ``` + 1. Build wheel ```bash python3 -m pip wheel . ``` +### How to build for multiple python versions (linux with docker) + +1. Build wheels via docker (linux) + ```bash + /bin/bash ./bin/linux-build.sh + ### How to analyze performance (linux) 1. Install dependencies @@ -181,19 +190,27 @@ Read more about the **Classes** and their **Properties** in our [read the docs d 1. Install dependencies - [Python 3](https://www.python.org/downloads/windows/) - - [Buildtools für Visual Studio 201*x*](https://visualstudio.microsoft.com/de/downloads/) (Scroll down » All Downloads » Tools for Visual Studio 201*x*) + - [Buildtools für Visual Studio 2022](https://visualstudio.microsoft.com/de/downloads/) (Scroll down » All Downloads » Tools for Visual Studio 2022) -1. Build wheel +1. Option 1: Build as wheel ```bash python3 -m pip wheel . ``` +1. Option 2: Build pyd via Powershell + ```powershell + cmake -DCMAKE_BUILD_TYPE=Release -G "Visual Studio 17 2022" -B cmake-build-release -A x64 -DPython_EXECUTABLE=C:\PATH_TO_PYTHON\python.exe + &"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" /m /p:Platform=x64 /p:Configuration=Release c104.sln /t:Rebuild + ``` + Set a valid PATH_TO_PYTHON, if you have multiple python versions. \ + Set a valid path to MSBuild.exe unless msbuild is already in path. + ### Generate documentation 1. Build c104 module 1. Install dependencies - - `python3 -m pip install --upgrade sphinx breathe sphinx-autodoc-typehints` + - `python3 -m pip install -r ./docs/requirements.txt` - doxygen - graphviz diff --git a/bin/aarch64-build.sh b/bin/aarch64-build.sh new file mode 100644 index 0000000..ebcda9e --- /dev/null +++ b/bin/aarch64-build.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +DIR=$(dirname "$(dirname "$(realpath "$0")")") + +#rm -rf /opt/c104/build/*linux-aarch64* ; \ + +CMD1=" +mkdir -p /opt/c104/dist ; \ +rm -rf /opt/c104/dist/*manylinux*.whl ; \ +/opt/python/cp39-cp39/bin/python3 -m pip wheel /opt/c104 ; \ +/opt/python/cp310-cp310/bin/python3 -m pip wheel /opt/c104 ; \ +/opt/python/cp311-cp311/bin/python3 -m pip wheel /opt/c104 ; \ +/opt/python/cp312-cp312/bin/python3 -m pip wheel /opt/c104 ; \ +auditwheel repair ./c104-*-linux_aarch64.whl ; \ +mv ./wheelhouse/* /opt/c104/dist/ +" + +docker run -it --rm -v "$DIR:/opt/c104" quay.io/pypa/manylinux_2_28_aarch64 /bin/bash -c "$CMD1" + +CMD2=" +/opt/python/cp37-cp37m/bin/python3 -m pip wheel /opt/c104 ; \ +/opt/python/cp38-cp38/bin/python3 -m pip wheel /opt/c104 ; \ +auditwheel repair ./c104-*-linux_aarch64.whl ; \ +mv ./wheelhouse/* /opt/c104/dist/ +" + +docker run -it --rm -v "$DIR:/opt/c104" quay.io/pypa/manylinux2014_aarch64 /bin/bash -c "$CMD2" diff --git a/bin/armv7l-build.sh b/bin/armv7l-build.sh new file mode 100644 index 0000000..765b56e --- /dev/null +++ b/bin/armv7l-build.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +DIR=$(dirname "$(dirname "$(realpath "$0")")") + +# use cmake from repository + +CMD1=" +apt-get update -qq >/dev/null ; \ +DEBIAN_FRONTEND=noninteractive apt-get install -y -qq cmake ; \ +sed -i \"s@ \\\"cmake@# \\\"cmake@\" /opt/c104/pyproject.toml ; \ +mkdir -p /opt/c104/dist ; \ +rm -rf /opt/c104/build/* ; \ +python3 -m pip wheel /opt/c104 ; \ +mv ./c104-*.whl /opt/c104/dist/ ; \ +sed -i \"s@# \\\"cmake@ \\\"cmake@\" /opt/c104/pyproject.toml +" + +docker run -it --rm -v "$DIR:/opt/c104" python:3.12-bullseye /bin/bash -c "$CMD1" +docker run -it --rm -v "$DIR:/opt/c104" python:3.11-bullseye /bin/bash -c "$CMD1" +docker run -it --rm -v "$DIR:/opt/c104" python:3.10-bullseye /bin/bash -c "$CMD1" +docker run -it --rm -v "$DIR:/opt/c104" python:3.9-bullseye /bin/bash -c "$CMD1" +docker run -it --rm -v "$DIR:/opt/c104" python:3.8-bullseye /bin/bash -c "$CMD1" +docker run -it --rm -v "$DIR:/opt/c104" python:3.7-bullseye /bin/bash -c "$CMD1" diff --git a/bin/linux-build.sh b/bin/linux-build.sh index 20a2028..62dbb81 100755 --- a/bin/linux-build.sh +++ b/bin/linux-build.sh @@ -17,7 +17,6 @@ mv ./wheelhouse/* /opt/c104/dist/ docker run -it --rm -v "$DIR:/opt/c104" quay.io/pypa/manylinux_2_28_x86_64 /bin/bash -c "$CMD1" CMD2=" -/opt/python/cp36-cp36m/bin/python3 -m pip wheel /opt/c104 ; \ /opt/python/cp37-cp37m/bin/python3 -m pip wheel /opt/c104 ; \ /opt/python/cp38-cp38/bin/python3 -m pip wheel /opt/c104 ; \ auditwheel repair ./c104-*-linux_x86_64.whl ; \ diff --git a/bin/win-build.bat b/bin/win-build.bat index 0de2ddb..56d71e7 100644 --- a/bin/win-build.bat +++ b/bin/win-build.bat @@ -9,6 +9,7 @@ py -3.10 -m pip wheel . -w dist py -3.9 -m pip wheel . -w dist py -3.8 -m pip wheel . -w dist py -3.7 -m pip wheel . -w dist -py -3.6 -m pip wheel . -w dist +rmdir /S /Q c104.egg-info +py -m pip install build py -m build . --sdist diff --git a/depends/catch b/depends/catch index 3f0283d..31588bb 160000 --- a/depends/catch +++ b/depends/catch @@ -1 +1 @@ -Subproject commit 3f0283de7a9c43200033da996ff9093be3ac84dc +Subproject commit 31588bb4f56b638dd5afc28d3ebff9b9dcefb88d diff --git a/depends/lib60870 b/depends/lib60870 index f07a361..3f9b0cf 160000 --- a/depends/lib60870 +++ b/depends/lib60870 @@ -1 +1 @@ -Subproject commit f07a3611ddc43660e127127ef55ba451e068ef16 +Subproject commit 3f9b0cf6d9ebc074db5d55792ca070ca85f5b573 diff --git a/depends/lib60870.patch b/depends/lib60870.patch new file mode 100644 index 0000000..c190fe1 --- /dev/null +++ b/depends/lib60870.patch @@ -0,0 +1,29 @@ +diff --git a/lib60870-C/src/hal/tls/mbedtls/mbedtls_config.h b/lib60870-C/src/hal/tls/mbedtls/mbedtls_config.h +index 4cb7722..e211d58 100644 +--- a/lib60870-C/src/hal/tls/mbedtls/mbedtls_config.h ++++ b/lib60870-C/src/hal/tls/mbedtls/mbedtls_config.h +@@ -7,7 +7,6 @@ + #define MBEDTLS_HAVE_TIME_DATE + #define MBEDTLS_NO_UDBL_DIVISION + #define MBEDTLS_PLATFORM_C +-#define MBEDTLS_DEBUG_C + + /* mbed TLS feature support */ + #define MBEDTLS_CIPHER_MODE_CBC +diff --git a/lib60870-C/src/iec60870/cs104/cs104_connection.c b/lib60870-C/src/iec60870/cs104/cs104_connection.c +index 856cd7f..4bb917d 100644 +--- a/lib60870-C/src/iec60870/cs104/cs104_connection.c ++++ b/lib60870-C/src/iec60870/cs104/cs104_connection.c +@@ -1021,8 +1021,10 @@ handleConnection(void* parameter) + } + + #if (CONFIG_CS104_SUPPORT_TLS == 1) +- if (self->tlsSocket) +- TLSSocket_close(self->tlsSocket); ++ if (self->tlsSocket) { ++ TLSSocket_close(self->tlsSocket); ++ self->tlsSocket = NULL; ++ } + #endif + + Socket_destroy(self->socket); diff --git a/depends/mbedtls b/depends/mbedtls index 981743d..5a764e5 160000 --- a/depends/mbedtls +++ b/depends/mbedtls @@ -1 +1 @@ -Subproject commit 981743de6fcdbe672e482b6fd724d31d0a0d2476 +Subproject commit 5a764e5555c64337ed17444410269ff21cb617b1 diff --git a/depends/pybind11 b/depends/pybind11 index 5b0a6fc..7c33cdc 160000 --- a/depends/pybind11 +++ b/depends/pybind11 @@ -1 +1 @@ -Subproject commit 5b0a6fc2017fcc176545afe3e09c9f9885283242 +Subproject commit 7c33cdc2d39c7b99a122579f53bc94c8eb3332ff diff --git a/docs/requirements.txt b/docs/requirements.txt index f905464..2932df1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ breathe==4.35.0 -cmake==3.26.3 -sphinx-autodoc-typehints==1.23.0 +cmake==3.30.2 +sphinx-autodoc-typehints==2.2.3 sphinx-rtd-theme==2.0.0 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 0000000..9b24522 --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,265 @@ +Change log +========== + +v2.0 +----- + +Features +^^^^^^^^^ + +- Add support for equipment protection points (*M_EP_TD_1*, *M_EP_TE_1*, *M_EP_TF_1*) and status with change detection (*M_PS_NA_1*) +- Add advanced property support for all messages +- Add point timer callback for extended event driven transmission scenarios +- Add option *c104.Init.MUTED*, to open a connection in muted state +- Add extended datetime.datetime support +- Add support for information object address **0** +- Improve command mode select and execute with automatic selection timeout +- Improve performance and stability +- Improve string representation for all classes +- Improve type safety + +Breaking Changes +^^^^^^^^^^^^^^^^^ + +- Dropped python 3.6 support, since pybind11 does not support +- c104.Point signature changes (see below) +- c104.Station signature changes (see below) +- c104.Client signature changes (see below) +- c104.Connection signature changes (see below) +- c104.Server signature changes (see below) +- c104.IncomingMessage signature changes (see below) +- Renamed enum property **c104.Qoc.CONTINUOUS** to **c104.Qoc.PERSISTENT**. \ + This corresponds to the standard description for Qualifier of command. +- Removed deprecated function **c104.add_server(...)**, use ``c104.Server()`` constructor instead +- Removed deprecated function **c104.remove_server(...)**, remove last reference to server instance instead +- Removed deprecated function **c104.add_client(...)**, use ``c104.Client()`` constructor instead +- Removed deprecated function **c104.remove_client(...)**, remove last reference to client instance instead + + +Changed signatures in c104.Point +"""""""""""""""""""""""""""""""" + +The concept of a points value is not enough to support all properties of all protocol messages. Therefore, the value was replaced by individual information objects. Every point type has a specific information type that stores a specific value type but also other properties. This also ensures type safety because there is no automatic cast from a Python number to a required value class. + +- Added property **point.info** + This container class carries all protocol message specific properties of a point. + + .. code-block:: python + + single_point.info = c104.SingleInfo(True) + double_point.info = c104.DoubleInfo(c104.Double.ON) + step_point.info = c104.StepInfo(c104.Int5(13)) + binary_point.info = c104.BinaryInfo(c104.Byte32(12)) + normalized_point.info = c104.NormalizedInfo(c104.NormalizedFloat(-0.734)) + scaled_point.info = c104.ScaledInfo(c104.Int16(-24533)) + short_point.info = c104.ShortInfo(12.34) + counter_point.info = c104.BinaryCounterInfo(345678) + pe_event_point.info = c104.ProtectionEventInfo(c104.EventState.ON) + pe_start_point.info = c104.ProtectionStartInfo(c104.StartEvents.PhaseL1 | c104.StartEvents.PhaseL2) + pe_circuit_point.info = c104.ProtectionCircuitInfo(c104.OutputCircuits.PhaseL1) + pe_changed_point.info = c104.StatusAndChanged(c104.PackedSingle.I0) + +- Changed signature of **point.value** ``float`` **->** ``typing.Union[None, bool, c104.Double, c104.Step, c104.Int7, c104.Int16, int, c104.Byte32, c104.NormalizedFloat, float, c104.EventState, c104.StartEvents, c104.OutputCircuits, c104.PackedSingle]`` + The *point.value* property is a shortcut to *point.info.value* for convenience. + Example: ``single_point.value = False`` + +- Removed property **point.value_uint32** +- Removed property **point.value_int32** +- Removed property **point.value_float** + +- Changed signature of **point.quality** ``c104.Quality`` **->** ``typing.Union[None, c104.Quality, c104.BinaryCounterQuality]`` + The *point.quality* property is a shortcut to *point.info.quality* and returns point-specific types. For points without quality information, this will be None. Calling ``point.quality.is_good()`` can therefore result in an error if ``point.quality`` is **None**. + +- Removed **point.set(...)** method + Set a new info object ``point.info = ...`` instead, to update all properties like time and quality than just the value + Example: ``cl_double_command.set(value=c104.Double.ON, timestamp_ms=1711111111111) -> cl_double_command.info = c104.DoubleCmd(state=c104.Double.ON, qualifier=c104.Qoc.LONG_PULSE, recorded_at=datetime.datetime.fromtimestamp(1711111111.111))`` + +- Changed **point.report_ms** setter validation + The *report_ms* property must be a positive integer and a **multiple of the tick_rate_ms** of the corresponding server or client + +- Removed property **point.updated_at_ms**: ``int``, use ``point.recorded_at`` instead +- Removed property **point.received_at_ms**: ``int``, use ``point.processed_at`` instead +- Removed property **point.sent_at_ms**: ``int``, use ``point.processed_at`` instead +- Removed property **point.reported_at_ms**: ``int``, use ``point.processed_at`` instead + +- Added read-only property **point.recorded_at**: ``typing.Optional[datetime.datetime]`` + The timestamp sent with the info via protocol. At the sender side, this value will be set on info creation time and updated on info.value assigning. This timestamp will not be updated on point transmission. The property can be None, if the protocol message type does not contain a timestamp. +- Added read-only property **point.processed_at**: ``datetime.datetime`` + This timestamp stands for the last sending or receiving timestamp of this info. +- Added read-only property **point.selected_by**: ``typing.Optional[int]`` + If select this will be the originator address, otherwise None +- Changed signature of method **point.transmit** (cause: c104.Cot = c104.Cot.UNKNOWN_COT, qualifier: c104.Qoc = c104.Qoc.NONE) -> point.transmit(cause: c104.Cot) + The qualifier is now part of the info object of command points and can be set via a new info assignment. The cause qualifier does not have a default value anymore so that this argument is obligatory now. +- Changed signature of **point.related_io_address** to accept None as value: ``int`` **->** ``typing.Optional[int]`` + This is necessary to accept a value of 0 as a valid io_address. +- Changed signature of **point.on_receive(...)** callback signature from ``(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState`` to ``(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState`` \ + The argument ``previous_state: dict`` was replaced by argument ``previous_info: c104.Information``. Since all relevant is accessible via the info object, a dict is not required anymore. Instead, the previous info object will be provided. +- Added callback **point.on_timer(...)** \ + Callback signature function: ``(point: c104.Point) -> None`` \ + Register callback signature: ``point.on_timer(callable=on_timer, interval_ms=1000)`` \ + The *timer_ms* property must be a positive integer and a **multiple of the tick_rate_ms** of the corresponding server or client +- Added read-only property **point.interval_ms**: ``int`` \ + This property defines the interval between two on_timer callback executions. \ + This property can only be changed via the ``point.on_timer(...)`` method + +Changed signatures in c104.Station +""""""""""""""""""""""""""""""""""" +- Changed signature of method **station.add_point(...)** \ + Parameter *io_address* accepts a value of ``0``. \ + Parameter *related_io_address* accepts a value of ``0`` as valid IOA and a value of ``None`` as not set + +Changed signatures in c104.IncomingMessage +""""""""""""""""""""""""""""""""""""""""""" +- Added read-only property info: Union[...] +- Removed property command_qualifier, use message.info.qualifier instead +- Removed property connection_string +- Removed property value +- Removed property quality + +Changed signatures in c104.Client +"""""""""""""""""""""""""""""""""" +- Changed signature of **constructor** + Reduced default value of argument **command_timeout_ms** from ``1000ms`` to ``100ms``. \ + Reduced default value of argument **tick_rate_ms** from ``1000ms`` to ``100ms``. \ + The minimum tick rate is ``50ms``. +- Added read-only property **client.tick_rate_ms**: ``int`` + +Changed signatures in c104.Connection +"""""""""""""""""""""""""""""""""""""" +- Added read-only property **connection.connected_at**: ``typing.Optional[datetime.datetime]`` +- Added read-only property **connection.disconnected_at**: ``typing.Optional[datetime.datetime]`` +- Add c104.Init.MUTED to connect to a server without activating the message transmission. +- Removed c104.ConnectionState values: OPEN_AWAIT_UNMUTE, OPEN_AWAIT_INTERROGATION, OPEN_AWAIT_CLOCK_SYNC + The connection will change from CLOSED_AWAIT_OPEN to OPEN_MUTED, will then execute the init commands, if any and change the state afterwards to OPEN if init != c104.Init.MUTED. The intermediary states are not required anymore. +- Instead of using to wait for a connection establishment: + while not connection.is_connected: + time.sleep(1) + wait for state open so that not only connection is established but also init commands are finished + while connection.state != c104.ConnectionState.OPEN: + time.sleep(1) + +Changed signatures in c104.Server +"""""""""""""""""""""""""""""""""" +- Changed signature of **constructor** \ + Add argument **select_timeout_ms** to constructor with default value ``100ms`` \ + Reduced default value of **tick_rate_ms** from ``1000ms`` to ``100ms``. \ + The minimum tick rate is 50ms. +- Added read-only property **client.tick_rate_ms**: ``int`` + +Bugfixes +^^^^^^^^^^ +- Read property **IncomingMessage.raw** caused SIGABRT +- **Server.active_connection_count** counts also inactive open connections +- fix select detection in **c104.explain_bytes_dict(...)** +- **point.transmit(...)** throws an exception if the same point is in an active transmission +- auto set environment variable **PYTHONUNBUFFERED** to avoid delayed print output from Python callbacks + +v1.18 +------- +- Add support for Qualifier of Command for single, double and regulating step commands +- Fix transmit updated_at timestamp for time aware point +- c104.Point.set method signature improved (non-breaking): + - Add keyword argument timestamp_ms to allow setting a points value in combination with an updated_at_ms timestamp + - Improve value argument to support instances of type c104.Double and c104.Step as setter for c104.Point.value does +- Improve GIL handling for methods station.add_point, server.stop and client.stop + +v1.17 +------- +- Fix (1.17.1): Fix select-and-execute for C_SE_NA +- Fix (1.17.1): Fix armv7 build + +- Add optional feature **Select-And-Execute** (also called Select-Before-Execute) + - Add enum c104.CommandMode + - Add properties point.command_mode, point.selected_by and incomingmessage.is_select_command + - on_receive callback argument previous_state contains key selected_by + - Add select field to explain_bytes and explain_bytes_dict + +- Fix free command response state key if command was never send +- Improve point transmission handling +- Improve documentation + +v1.16 +------- +- Add feature TLS (working versions: SSLv3.0, TLSv1.0, TLSv1.1, TLSv1.2; not working: TLSv1.3) +- Fix potential segmentation fault by using smart pointer with synchronized reference counter between cpp and python +- Improve CMake structure +- Improve reconnect behaviour +- Update lib60870-C to latest + +v1.15 +------- +- Fix (1.15.2): Fix deadlock between GIL and client-internal mutex. +- Add new Connection callback **on_state_change** (connection: c104.Connection, state: c104.ConnectionState) -> None +- Add new enum c104.ConnectionState (OPEN, CLOSED, ...) +- Allow COT 7,9,10 for command point transmit() from server side to support manual/lazy command responses +- Add new enum c104.ResponseState (FAILURE, SUCCESS, NONE) +- **BC signature of callback server.on_clock_sync changed** + - Return c104.ResponseState instead of bool +- **BC signature of callback point.on_receive changed** + - Return c104.ResponseState instead of bool + +v1.14 +------- +- Fix (1.14.2): Fix potential segmentation fault +- Fix (1.14.1): Add missing option c104.Init.NONE +- Add c104.Init enum to configure outgoing commands after START_DT, defaults to c104.Init.ALL which is equal to previous behaviour +- Clients timeout_ms parameter is used to configure maximum rtt for message in lib60870-C \ + (APCI Parameter t1: max(1, (int)round(timeout_ms/1000))) +- **BC callback signature validation** + - Allow functools.partial, functools.partialmethod and extra arguments in callbacks that have a default/bound value + - Ignore arguments with non-empty default value n callback signature validation + +v1.13 +------- +- Fix (1.13.6): try send clock sync only once after start_dt +- Fix (1.13.5): Silence debug output, update dependencies +- Fix (1.13.4): PointCommand encode REGULATION STEP COMMAND values, windows stack manipulation in server +- Fix (1.13.3): IncomingMessage decode DOUBLE POINT values 0.0, 1.0, 2.0, 3.0 +- Fix (1.13.3): IncomingMessage allows 0.0,1.0,2.0,3.0 values for DoubleCommands, message.value returns value instead of IOA +- Fix (1.13.2): Server sends multiple ASDU per TypeID in InterrogationResponse or Periodic transmission if IOs exceed single ASDU size +- **BC for on_clock_sync** \ + Callable must return a bool to provide act-con feedback to client +- **Respond to global CA messages** \ + Fix: Server confirms messages that are addressed at global ca from each local CA with its own address. + +v1.12 +------- +- **Replace BitSets by Enum flags** \ + Change usage of Debug and Quality attributes +- **Start periodic transmission instantly** after receiving START_DT, do not wait for a first interrogation command + +v1.11 +------- +- **Add python 3.6 support** +- **Add Windows support** +- **Migrated from boost::python to pybind11** \ + Drop all dependencies to boost libraried and replace bindings by header only template library pybind11. +- **Simplified build process via setuptools and cmake** \ + Integrate lib60870 into cmake to build everything in a single build process. +- **Improve callback handling** + - *Function:* A **reference** is stored internally with valid reference counter. + - *Method:* A **reference** to the bounded method is stored internally. + +v1.10 +------- +- **Add ARM support** +- **New DebugFlag: GIL** \ + Print debug information when GIL is acquired or released. +- **New coding convention for callbacks:** + - Callback function signature must match perfectly (variable names, order, return and type hints). + - *Lambda:* Usage of lambda function is **not possible** as type hinting information are not added to the function object itself, only to the namespace the object is stored in. + - *Function:* A **copy** (type.FunctionType) is stored internally using the same references as the original function to guarantee function existence. (^1.10.2) + - *Method:* A **reference** to the object is stored internally with the name of the method. (^1.10.2) + +v1.9 +------- +- **New coding convention:** Caller passes self-reference as first argument to callback functions. + - Client.on_new_station: Client reference as additional argument in the first place + - Client.on_new_point: Client reference as additional argument in the first place + - Connection.on_receive_raw: Connection reference as additional argument in the first place + - Connection.on_send_raw: Connection reference as additional argument in the first place + - Server.on_receive_raw: Server reference as additional argument in the first place + - Server.on_send_raw: Server reference as additional argument in the first place + - Server.on_connect: Server reference as additional argument in the first place + - Server.on_clock_sync: Server reference as additional argument in the first place + - Server.on_unexpected_message: Server reference as additional argument in the first place diff --git a/docs/source/conf.py b/docs/source/conf.py index efecd7a..47d2fe7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,8 +13,8 @@ copyright = "2020-2024, Fraunhofer Institute for Applied Information Technology FIT" author = "Martin Unkel " -release = "1.0" -version = "1.18.0" +release = "2.0" +version = "2.0.0" # -- General configuration --------------------------------------------------- diff --git a/docs/source/core/enums/binarycounterquality.rst b/docs/source/core/enums/binarycounterquality.rst new file mode 100644 index 0000000..8487b56 --- /dev/null +++ b/docs/source/core/enums/binarycounterquality.rst @@ -0,0 +1,5 @@ +BinaryCounterQuality +====================================================================== + +.. doxygenenum:: BinaryCounterQuality + :project: iec104-python diff --git a/docs/source/core/enums/commandprocessstate.rst b/docs/source/core/enums/commandprocessstate.rst new file mode 100644 index 0000000..a32a204 --- /dev/null +++ b/docs/source/core/enums/commandprocessstate.rst @@ -0,0 +1,5 @@ +CommandProcessState +====================================================================== + +.. doxygenenum:: CommandProcessState + :project: iec104-python diff --git a/docs/source/core/enums/cs101causeofinitialization.rst b/docs/source/core/enums/cs101causeofinitialization.rst new file mode 100644 index 0000000..42cda6b --- /dev/null +++ b/docs/source/core/enums/cs101causeofinitialization.rst @@ -0,0 +1,5 @@ +Cause of Initialization +====================================================================== + +.. doxygenenum:: CS101_CauseOfInitialization + :project: iec104-python diff --git a/docs/source/core/enums/debug.rst b/docs/source/core/enums/debug.rst index 5bbab96..c4ed5e9 100644 --- a/docs/source/core/enums/debug.rst +++ b/docs/source/core/enums/debug.rst @@ -3,10 +3,3 @@ Debug .. doxygenenum:: Debug :project: iec104-python - -Quality -InformationType -ConnectionState -ConnectionInit -CommandResponseState -CommandTransmissionMode diff --git a/docs/source/core/enums/doublepointvalue.rst b/docs/source/core/enums/doublepointvalue.rst new file mode 100644 index 0000000..31db682 --- /dev/null +++ b/docs/source/core/enums/doublepointvalue.rst @@ -0,0 +1,5 @@ +DoublePointValue +====================================================================== + +.. doxygenenum:: DoublePointValue + :project: iec104-python diff --git a/docs/source/core/enums/informationtype.rst b/docs/source/core/enums/eventstate.rst similarity index 66% rename from docs/source/core/enums/informationtype.rst rename to docs/source/core/enums/eventstate.rst index 9227e6f..b1b134a 100644 --- a/docs/source/core/enums/informationtype.rst +++ b/docs/source/core/enums/eventstate.rst @@ -1,5 +1,5 @@ -InformationType +EventState ====================================================================== -.. doxygenenum:: InformationType +.. doxygenenum:: EventState :project: iec104-python diff --git a/docs/source/core/enums/fieldset16.rst b/docs/source/core/enums/fieldset16.rst new file mode 100644 index 0000000..8010466 --- /dev/null +++ b/docs/source/core/enums/fieldset16.rst @@ -0,0 +1,5 @@ +FieldSet16 +====================================================================== + +.. doxygenenum:: FieldSet16 + :project: iec104-python diff --git a/docs/source/core/enums/iec608705typeid.rst b/docs/source/core/enums/iec608705typeid.rst new file mode 100644 index 0000000..d074907 --- /dev/null +++ b/docs/source/core/enums/iec608705typeid.rst @@ -0,0 +1,5 @@ +IEC60870-5 Type IDs +====================================================================== + +.. doxygenenum:: IEC60870_5_TypeID + :project: iec104-python diff --git a/docs/source/core/enums/index.rst b/docs/source/core/enums/index.rst index de37d64..5fa86c1 100644 --- a/docs/source/core/enums/index.rst +++ b/docs/source/core/enums/index.rst @@ -6,13 +6,23 @@ List .. toctree:: :maxdepth: 4 + iec608705typeid + cs101causeofinitialization cs101qualifierofcommand cs101qualifierofinterrogation unexpectedmessagecause debug + doublepointvalue + stepcommandvalue + fieldset16 + eventstate + startevents + outputcircuits quality - informationtype + binarycounterquality connectionstate connectioninit commandresponsestate + commandprocessstate commandtransmissionmode + tlsversion diff --git a/docs/source/core/enums/outputcircuits.rst b/docs/source/core/enums/outputcircuits.rst new file mode 100644 index 0000000..4cddf16 --- /dev/null +++ b/docs/source/core/enums/outputcircuits.rst @@ -0,0 +1,5 @@ +OutputCircuits +====================================================================== + +.. doxygenenum:: OutputCircuits + :project: iec104-python diff --git a/docs/source/core/enums/startevents.rst b/docs/source/core/enums/startevents.rst new file mode 100644 index 0000000..0365dc7 --- /dev/null +++ b/docs/source/core/enums/startevents.rst @@ -0,0 +1,5 @@ +StartEvents +====================================================================== + +.. doxygenenum:: StartEvents + :project: iec104-python diff --git a/docs/source/core/enums/stepcommandvalue.rst b/docs/source/core/enums/stepcommandvalue.rst new file mode 100644 index 0000000..376efe7 --- /dev/null +++ b/docs/source/core/enums/stepcommandvalue.rst @@ -0,0 +1,5 @@ +StepCommandValue +====================================================================== + +.. doxygenenum:: StepCommandValue + :project: iec104-python diff --git a/docs/source/core/enums/tlsversion.rst b/docs/source/core/enums/tlsversion.rst new file mode 100644 index 0000000..1676fb1 --- /dev/null +++ b/docs/source/core/enums/tlsversion.rst @@ -0,0 +1,5 @@ +TLSConfigVersion +====================================================================== + +.. doxygenenum:: TLSConfigVersion + :project: iec104-python diff --git a/docs/source/core/index.rst b/docs/source/core/index.rst index 9ea0e9e..296b167 100644 --- a/docs/source/core/index.rst +++ b/docs/source/core/index.rst @@ -10,4 +10,5 @@ C++ Core module/index remote/index enums/index + numbers/index types diff --git a/docs/source/core/numbers/byte32.rst b/docs/source/core/numbers/byte32.rst new file mode 100644 index 0000000..2d330a1 --- /dev/null +++ b/docs/source/core/numbers/byte32.rst @@ -0,0 +1,6 @@ +Byte32 +====================================================================== + +.. doxygenclass:: Byte32 + :project: iec104-python + :members: diff --git a/docs/source/core/numbers/index.rst b/docs/source/core/numbers/index.rst new file mode 100644 index 0000000..a820675 --- /dev/null +++ b/docs/source/core/numbers/index.rst @@ -0,0 +1,15 @@ +Numbers +======== + +List + +.. toctree:: + :maxdepth: 4 + + byte32 + limitedint7 + limitedint16 + limiteduint5 + limiteduint7 + limiteduint16 + normalizedfloat diff --git a/docs/source/core/numbers/limitedint16.rst b/docs/source/core/numbers/limitedint16.rst new file mode 100644 index 0000000..e449947 --- /dev/null +++ b/docs/source/core/numbers/limitedint16.rst @@ -0,0 +1,6 @@ +LimitedInt16 +====================================================================== + +.. doxygenclass:: LimitedInt16 + :project: iec104-python + :members: diff --git a/docs/source/core/numbers/limitedint7.rst b/docs/source/core/numbers/limitedint7.rst new file mode 100644 index 0000000..e73f271 --- /dev/null +++ b/docs/source/core/numbers/limitedint7.rst @@ -0,0 +1,6 @@ +LimitedInt7 +====================================================================== + +.. doxygenclass:: LimitedInt7 + :project: iec104-python + :members: diff --git a/docs/source/core/numbers/limiteduint16.rst b/docs/source/core/numbers/limiteduint16.rst new file mode 100644 index 0000000..1773125 --- /dev/null +++ b/docs/source/core/numbers/limiteduint16.rst @@ -0,0 +1,6 @@ +LimitedUInt16 +====================================================================== + +.. doxygenclass:: LimitedUInt16 + :project: iec104-python + :members: diff --git a/docs/source/core/numbers/limiteduint5.rst b/docs/source/core/numbers/limiteduint5.rst new file mode 100644 index 0000000..f793f84 --- /dev/null +++ b/docs/source/core/numbers/limiteduint5.rst @@ -0,0 +1,6 @@ +LimitedUInt5 +====================================================================== + +.. doxygenclass:: LimitedUInt5 + :project: iec104-python + :members: diff --git a/docs/source/core/numbers/limiteduint7.rst b/docs/source/core/numbers/limiteduint7.rst new file mode 100644 index 0000000..add04b4 --- /dev/null +++ b/docs/source/core/numbers/limiteduint7.rst @@ -0,0 +1,6 @@ +LimitedUInt7 +====================================================================== + +.. doxygenclass:: LimitedUInt7 + :project: iec104-python + :members: diff --git a/docs/source/core/numbers/normalizedfloat.rst b/docs/source/core/numbers/normalizedfloat.rst new file mode 100644 index 0000000..059b667 --- /dev/null +++ b/docs/source/core/numbers/normalizedfloat.rst @@ -0,0 +1,6 @@ +NormalizedFloat +====================================================================== + +.. doxygenclass:: NormalizedFloat + :project: iec104-python + :members: diff --git a/docs/source/core/object/index.rst b/docs/source/core/object/index.rst index ddfd81f..4fc5d17 100644 --- a/docs/source/core/object/index.rst +++ b/docs/source/core/object/index.rst @@ -6,5 +6,6 @@ Classes .. toctree:: :maxdepth: 4 + information datapoint station diff --git a/docs/source/core/object/information.rst b/docs/source/core/object/information.rst new file mode 100644 index 0000000..3a6a911 --- /dev/null +++ b/docs/source/core/object/information.rst @@ -0,0 +1,78 @@ +Information +====================================================================== + +.. doxygenclass:: Object::SingleInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::SingleCmd + :project: iec104-python + :members: + +.. doxygenclass:: Object::DoubleInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::DoubleCmd + :project: iec104-python + :members: + +.. doxygenclass:: Object::StepInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::StepCmd + :project: iec104-python + :members: + +.. doxygenclass:: Object::BinaryInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::BinaryCmd + :project: iec104-python + :members: + +.. doxygenclass:: Object::NormalizedInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::NormalizedCmd + :project: iec104-python + :members: + +.. doxygenclass:: Object::ScaledInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::ScaledCmd + :project: iec104-python + :members: + +.. doxygenclass:: Object::ShortInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::ShortCmd + :project: iec104-python + :members: + +.. doxygenclass:: Object::BinaryCounterInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::ProtectionEquipmentEventInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::ProtectionEquipmentStartEventsInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::ProtectionEquipmentOutputCircuitInfo + :project: iec104-python + :members: + +.. doxygenclass:: Object::StatusWithChangeDetection + :project: iec104-python + :members: diff --git a/docs/source/index.rst b/docs/source/index.rst index cc3ca1a..84eecb8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,58 @@ Welcome to iec104-python's documentation! ================================================ +Introduction +------------ + +This software provides an object-oriented high-level python module to simulate scada systems and remote terminal units communicating via 60870-5-104 protocol. + +The python module c104 combines the use of lib60870-C with state structures and python callback handlers. + +Examples +-------- + +Remote terminal unit +^^^^^^^^^^^^^^^^^^^^^^^^^^ + + .. code-block:: python + + import c104 + + # server and station preparation + server = c104.Server(ip="0.0.0.0", port=2404) + + # add local station and points + station = server.add_station(common_address=47) + measurement_point = station.add_point(io_address=11, type=c104.Type.M_ME_NC_1, report_ms=1000) + command_point = station.add_point(io_address=12, type=c104.Type.C_RC_TA_1) + + server.start() + + +Scada unit +^^^^^^^^^^^^ + + .. code-block:: python + + import c104 + + client = c104.Client(tick_rate_ms=1000, command_timeout_ms=5000) + + # add RTU with station and points + connection = client.add_connection(ip="127.0.0.1", port=2404, init=c104.Init.INTERROGATION) + station = connection.add_station(common_address=47) + measurement_point = station.add_point(io_address=11, type=c104.Type.M_ME_NC_1, report_ms=1000) + command_point = station.add_point(io_address=12, type=c104.Type.C_RC_TA_1) + + client.start() + +Visit https://github.com/Fraunhofer-FIT-DIEN/iec104-python/tree/main/examples for more examples. + .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Contents: - core/index + install + changelog python/index + core/index diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 0000000..b5e1cb8 --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,9 @@ +Installation +============= + +Install from pypi.org +^^^^^^^^^^^^^^^^^^^^^^ + + .. code-block:: bash + + python3 -m pip install c104 diff --git a/docs/source/objects.inv b/docs/source/objects.inv index 3486de327f1487d5427d20ba53550867e88952b9..d30d7fc988d398a8a566444031906dc7324e2ed1 100644 GIT binary patch literal 23962 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 delta 11847 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
fTH;(_Q3;xILs0`= zahib%N-(`hl-8ybr-5;)V45;PrBjIN(~OB2q51HyM=oW`L0F_yTV z^%yn84@UTOtQ~QkGEqPrk#;ZJo}>onP})xdhRuOH8??6?lrD^BYjuq$agfE` z#IZOir3@%BWGzqqA`Ls~c+nz&a!wnj6vSH8nWRjuju2`3iiB6aaY<1JGaTGo31cJPd{uPf;l%4#qN* zU<*PFf-rT`c@%M4`Xql2Ty%nYL@XVnhv*p$faji{bRI?KtqUMpDNUh!AR>~OTL!T* zq7l=Ra?w~?WWkrVn5Be>#KS!25epbP;0B$P3%E%X+%7DMO-FlVhytqNKvBT)wEhbi zLWPheDSlDx7=EeM|?bAT&>#i6w-%fmv`EF@(;jSK*Q}j{`#K zRR{+=MljP699QExWuUQo6|SjDi$u^&B^B~1%`*%-aDgmXd!@rM!ktVBGg%C28?2GR zOBpo_w&{Mx~c1CK9lpX{W2v z5n;(OSPjqmkjbU=X_KuY!Uqi|8=i1*2zv{Pl80Vo9=w0nafD0_=EXv62$6a^M~NxO z6ewNIfLJ7^moWsP9;jJRuh!9!^2md`2G&-26w^sk)Q3z*VALQ514%m!{;HrD2nYy9 z!52cV7nuR#Tu%(K4kAsgJmDm8#c9CGS!V4aGQC&nHV@F%gk$7D5>IQWV8>Ga2-`qy zR__hi2M&MU30O-isBlDUi6w;?#4^b+xc(}b(697#CyWPoM5jEp7W@$nv0yrl3?XVd z7~QjUDN?EyB#C4ZY&Ok;VQ|f)knqHNmM%b{+m!o2A#hVIcyBO0ZHO`&>tgI#x+b7* zDL5xs!te$YF4HRwBQUtmULe}9^t2tuq*T5V+|_@m3`Qb4PQZnQSdZAArCY4PDN6Za zH5eUZ1cR|3A$0Lx@>uf{xfAIrt5>986d;V$KqF!=rPDE;{PP%__R_zE@n+NCy5{QB z)y_fq@!ihez+8#Hg-17e3TN}l*(faTrpqqy`oI7C|IE&y_{&omFLs0emv>JSUn24O zw?BWczQ}LdUCJJtr!adqF04*{8U&-zN5O;kA*Bn_(0f{_$dH4c^EH`Sa(hFw9@xd^ zJmp-9R%4yQKMs#Pt!emxz?Cy~Y0CFwLqz3CU@29&z-eEZ4o<;TCJ=SvXyh^2=>;RB z7F-ji(+M(Z!J5JVLFtSn2ss8j09qgv3?zR{d(e3S@cs?DKlNno9KMQ8+ zzHVEp#b+zR+4r}pSF=s|3pbG@U&ncu%%t!{5&>3TImRWp5#>o|a@V*gJDE2F(;A|F z#EFa$^Fu%(F~qAR9e9qMdm3ATc^lKo<8*gJN-x3;PN^t-BrK*pmS{iN{{spp9&4{4 BYCHe{ diff --git a/docs/source/python/enum/coi.rst b/docs/source/python/enum/coi.rst new file mode 100644 index 0000000..abdc832 --- /dev/null +++ b/docs/source/python/enum/coi.rst @@ -0,0 +1,7 @@ +.. _c104.Coi: + +Coi (Cause of Initialization) +############################# + +.. autoclass:: c104.Coi + :members: diff --git a/docs/source/python/enum/commandmode.rst b/docs/source/python/enum/commandmode.rst index a2a877f..d805341 100644 --- a/docs/source/python/enum/commandmode.rst +++ b/docs/source/python/enum/commandmode.rst @@ -5,4 +5,3 @@ CommandMode .. autoclass:: c104.CommandMode :members: - :noindex: diff --git a/docs/source/python/enum/connectionstate.rst b/docs/source/python/enum/connectionstate.rst index ef04277..78f70c6 100644 --- a/docs/source/python/enum/connectionstate.rst +++ b/docs/source/python/enum/connectionstate.rst @@ -5,4 +5,3 @@ ConnectionState .. autoclass:: c104.ConnectionState :members: - :noindex: diff --git a/docs/source/python/enum/cot.rst b/docs/source/python/enum/cot.rst index b593a62..c493771 100644 --- a/docs/source/python/enum/cot.rst +++ b/docs/source/python/enum/cot.rst @@ -5,4 +5,3 @@ Cot (Cause of Transmission) .. autoclass:: c104.Cot :members: - :noindex: diff --git a/docs/source/python/enum/data.rst b/docs/source/python/enum/data.rst deleted file mode 100644 index 6ec4ddd..0000000 --- a/docs/source/python/enum/data.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _c104.Data: - -Data -#### - -.. autoclass:: c104.Data - :members: - :noindex: diff --git a/docs/source/python/enum/double.rst b/docs/source/python/enum/double.rst index 7879241..4590780 100644 --- a/docs/source/python/enum/double.rst +++ b/docs/source/python/enum/double.rst @@ -5,4 +5,3 @@ Double .. autoclass:: c104.Double :members: - :noindex: diff --git a/docs/source/python/enum/eventstate.rst b/docs/source/python/enum/eventstate.rst index 85f0a28..919a610 100644 --- a/docs/source/python/enum/eventstate.rst +++ b/docs/source/python/enum/eventstate.rst @@ -5,4 +5,3 @@ EventState .. autoclass:: c104.EventState :members: - :noindex: diff --git a/docs/source/python/enum/index.rst b/docs/source/python/enum/index.rst index 6a5e0ee..ac0a2fd 100644 --- a/docs/source/python/enum/index.rst +++ b/docs/source/python/enum/index.rst @@ -4,10 +4,10 @@ Enums .. toctree:: :maxdepth: 4 + coi commandmode connectionstate cot - data double eventstate init diff --git a/docs/source/python/enum/init.rst b/docs/source/python/enum/init.rst index 754f464..dd786ae 100644 --- a/docs/source/python/enum/init.rst +++ b/docs/source/python/enum/init.rst @@ -5,4 +5,3 @@ Init .. autoclass:: c104.Init :members: - :noindex: diff --git a/docs/source/python/enum/qoc.rst b/docs/source/python/enum/qoc.rst index c65e2c7..93e1281 100644 --- a/docs/source/python/enum/qoc.rst +++ b/docs/source/python/enum/qoc.rst @@ -5,4 +5,3 @@ Qoc (Qualifier of Command) .. autoclass:: c104.Qoc :members: - :noindex: diff --git a/docs/source/python/enum/qoi.rst b/docs/source/python/enum/qoi.rst index 1f8c6a6..94d9d2c 100644 --- a/docs/source/python/enum/qoi.rst +++ b/docs/source/python/enum/qoi.rst @@ -5,4 +5,3 @@ Qoi (Qualifier of interrogation) .. autoclass:: c104.Qoi :members: - :noindex: diff --git a/docs/source/python/enum/responsestate.rst b/docs/source/python/enum/responsestate.rst index 29e7d0b..9f02334 100644 --- a/docs/source/python/enum/responsestate.rst +++ b/docs/source/python/enum/responsestate.rst @@ -5,4 +5,3 @@ ResponseState .. autoclass:: c104.ResponseState :members: - :noindex: diff --git a/docs/source/python/enum/step.rst b/docs/source/python/enum/step.rst index b68c9a9..6e7cda5 100644 --- a/docs/source/python/enum/step.rst +++ b/docs/source/python/enum/step.rst @@ -5,4 +5,3 @@ Step .. autoclass:: c104.Step :members: - :noindex: diff --git a/docs/source/python/enum/tlsversion.rst b/docs/source/python/enum/tlsversion.rst index 09421c2..93c2949 100644 --- a/docs/source/python/enum/tlsversion.rst +++ b/docs/source/python/enum/tlsversion.rst @@ -5,4 +5,3 @@ TlsVersion .. autoclass:: c104.TlsVersion :members: - :noindex: diff --git a/docs/source/python/enum/type.rst b/docs/source/python/enum/type.rst index 5b7e34c..a73ca8d 100644 --- a/docs/source/python/enum/type.rst +++ b/docs/source/python/enum/type.rst @@ -5,7 +5,6 @@ Type .. autoclass:: c104.Type :members: - :noindex: Float, Short, NaN ----------------- diff --git a/docs/source/python/enum/umc.rst b/docs/source/python/enum/umc.rst index 7f729fb..4120cf4 100644 --- a/docs/source/python/enum/umc.rst +++ b/docs/source/python/enum/umc.rst @@ -5,4 +5,3 @@ Umc (Unexpected message cause) .. autoclass:: c104.Umc :members: - :noindex: diff --git a/docs/source/python/enumset/binarycounterquality.rst b/docs/source/python/enumset/binarycounterquality.rst new file mode 100644 index 0000000..0e39a3e --- /dev/null +++ b/docs/source/python/enumset/binarycounterquality.rst @@ -0,0 +1,7 @@ +.. _c104.BinaryCounterQuality: + +BinaryCounterQuality +#################### + +.. autoclass:: c104.BinaryCounterQuality + :members: diff --git a/docs/source/python/enumset/debug.rst b/docs/source/python/enumset/debug.rst index 50739d6..3641971 100644 --- a/docs/source/python/enumset/debug.rst +++ b/docs/source/python/enumset/debug.rst @@ -5,4 +5,3 @@ Debug .. autoclass:: c104.Debug :members: - :noindex: diff --git a/docs/source/python/enumset/index.rst b/docs/source/python/enumset/index.rst index 2312822..f1d1b62 100644 --- a/docs/source/python/enumset/index.rst +++ b/docs/source/python/enumset/index.rst @@ -4,5 +4,9 @@ EnumSets .. toctree:: :maxdepth: 4 + binarycounterquality debug + packedsingle + outputcircuits quality + startevents diff --git a/docs/source/python/enumset/outputcircuits.rst b/docs/source/python/enumset/outputcircuits.rst new file mode 100644 index 0000000..c5805fa --- /dev/null +++ b/docs/source/python/enumset/outputcircuits.rst @@ -0,0 +1,7 @@ +.. _c104.OutputCircuits: + +OutputCircuits +############## + +.. autoclass:: c104.OutputCircuits + :members: diff --git a/docs/source/python/enumset/packedsingle.rst b/docs/source/python/enumset/packedsingle.rst new file mode 100644 index 0000000..f24f956 --- /dev/null +++ b/docs/source/python/enumset/packedsingle.rst @@ -0,0 +1,7 @@ +.. _c104.PackedSingle: + +PackedSingle +############ + +.. autoclass:: c104.PackedSingle + :members: diff --git a/docs/source/python/enumset/quality.rst b/docs/source/python/enumset/quality.rst index dfb7df9..d123f8b 100644 --- a/docs/source/python/enumset/quality.rst +++ b/docs/source/python/enumset/quality.rst @@ -5,4 +5,3 @@ Quality .. autoclass:: c104.Quality :members: - :noindex: diff --git a/docs/source/python/enumset/startevents.rst b/docs/source/python/enumset/startevents.rst new file mode 100644 index 0000000..b3e925a --- /dev/null +++ b/docs/source/python/enumset/startevents.rst @@ -0,0 +1,7 @@ +.. _c104.StartEvents: + +StartEvents +########### + +.. autoclass:: c104.StartEvents + :members: diff --git a/docs/source/python/functions.rst b/docs/source/python/functions.rst index 47eaef8..13189d9 100644 --- a/docs/source/python/functions.rst +++ b/docs/source/python/functions.rst @@ -14,11 +14,3 @@ Global functions .. autofunction:: explain_bytes .. autofunction:: explain_bytes_dict - -.. autofunction:: add_client - -.. autofunction:: remove_client - -.. autofunction:: add_server - -.. autofunction:: remove_server diff --git a/docs/source/python/index.rst b/docs/source/python/index.rst index 436dd44..0a1e8c1 100644 --- a/docs/source/python/index.rst +++ b/docs/source/python/index.rst @@ -11,8 +11,10 @@ c104 python module server station point + information/index transportsecurity incomingmessage enumset/index enum/index + number/index functions diff --git a/docs/source/python/information/binarycmd.rst b/docs/source/python/information/binarycmd.rst new file mode 100644 index 0000000..5ad1001 --- /dev/null +++ b/docs/source/python/information/binarycmd.rst @@ -0,0 +1,8 @@ +.. _c104.BinaryCmd: + +BinaryCmd +########### + +.. autoclass:: c104.BinaryCmd + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/binarycounterinfo.rst b/docs/source/python/information/binarycounterinfo.rst new file mode 100644 index 0000000..4c8def1 --- /dev/null +++ b/docs/source/python/information/binarycounterinfo.rst @@ -0,0 +1,8 @@ +.. _c104.BinaryCounterInfo: + +BinaryCounterInfo +################# + +.. autoclass:: c104.BinaryCounterInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/binaryinfo.rst b/docs/source/python/information/binaryinfo.rst new file mode 100644 index 0000000..a24703b --- /dev/null +++ b/docs/source/python/information/binaryinfo.rst @@ -0,0 +1,8 @@ +.. _c104.BinaryInfo: + +BinaryInfo +########### + +.. autoclass:: c104.BinaryInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/doublecmd.rst b/docs/source/python/information/doublecmd.rst new file mode 100644 index 0000000..efb4628 --- /dev/null +++ b/docs/source/python/information/doublecmd.rst @@ -0,0 +1,8 @@ +.. _c104.DoubleCmd: + +DoubleCmd +########### + +.. autoclass:: c104.DoubleCmd + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/doubleinfo.rst b/docs/source/python/information/doubleinfo.rst new file mode 100644 index 0000000..8e8101e --- /dev/null +++ b/docs/source/python/information/doubleinfo.rst @@ -0,0 +1,8 @@ +.. _c104.DoubleInfo: + +DoubleInfo +########### + +.. autoclass:: c104.DoubleInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/index.rst b/docs/source/python/information/index.rst new file mode 100644 index 0000000..7369b0f --- /dev/null +++ b/docs/source/python/information/index.rst @@ -0,0 +1,26 @@ +Information +============ + +.. toctree:: + :maxdepth: 4 + + information + singleinfo + singlecmd + doubleinfo + doublecmd + stepinfo + stepcmd + binaryinfo + binarycmd + normalizedinfo + normalizedcmd + scaledinfo + scaledcmd + shortinfo + shortcmd + binarycounterinfo + protectioneventinfo + protectionstartinfo + protectioncircuitinfo + statusandchanged diff --git a/docs/source/python/information/information.rst b/docs/source/python/information/information.rst new file mode 100644 index 0000000..f48333e --- /dev/null +++ b/docs/source/python/information/information.rst @@ -0,0 +1,15 @@ +.. _c104.Information: + +Information +########### + +.. autoclass:: c104.Information + :members: + :exclude-members: __init__,__new__ + +.. hint:: + + The ``c104.Information.value`` property maps to most significant information of the derived information object. \ + See the specific information object documentation for type details. \ + The ``c104.BinaryCmd.quality`` property maps to the specific quality information (if any) of the derived information object. \ + See the specific information object documentation for type details. diff --git a/docs/source/python/information/normalizedcmd.rst b/docs/source/python/information/normalizedcmd.rst new file mode 100644 index 0000000..bedfc94 --- /dev/null +++ b/docs/source/python/information/normalizedcmd.rst @@ -0,0 +1,8 @@ +.. _c104.NormalizedCmd: + +NormalizedCmd +############# + +.. autoclass:: c104.NormalizedCmd + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/normalizedinfo.rst b/docs/source/python/information/normalizedinfo.rst new file mode 100644 index 0000000..a5f5c40 --- /dev/null +++ b/docs/source/python/information/normalizedinfo.rst @@ -0,0 +1,8 @@ +.. _c104.NormalizedInfo: + +NormalizedInfo +############## + +.. autoclass:: c104.NormalizedInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/protectioncircuitinfo.rst b/docs/source/python/information/protectioncircuitinfo.rst new file mode 100644 index 0000000..9db937f --- /dev/null +++ b/docs/source/python/information/protectioncircuitinfo.rst @@ -0,0 +1,8 @@ +.. _c104.ProtectionCircuitInfo: + +ProtectionCircuitInfo +##################### + +.. autoclass:: c104.ProtectionCircuitInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/protectioneventinfo.rst b/docs/source/python/information/protectioneventinfo.rst new file mode 100644 index 0000000..5219bcd --- /dev/null +++ b/docs/source/python/information/protectioneventinfo.rst @@ -0,0 +1,8 @@ +.. _c104.ProtectionEventInfo: + +ProtectionEventInfo +################### + +.. autoclass:: c104.ProtectionEventInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/protectionstartinfo.rst b/docs/source/python/information/protectionstartinfo.rst new file mode 100644 index 0000000..2fff788 --- /dev/null +++ b/docs/source/python/information/protectionstartinfo.rst @@ -0,0 +1,8 @@ +.. _c104.ProtectionStartInfo: + +ProtectionStartInfo +################### + +.. autoclass:: c104.ProtectionStartInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/scaledcmd.rst b/docs/source/python/information/scaledcmd.rst new file mode 100644 index 0000000..745e9a4 --- /dev/null +++ b/docs/source/python/information/scaledcmd.rst @@ -0,0 +1,8 @@ +.. _c104.ScaledCmd: + +ScaledCmd +########### + +.. autoclass:: c104.ScaledCmd + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/scaledinfo.rst b/docs/source/python/information/scaledinfo.rst new file mode 100644 index 0000000..33da17a --- /dev/null +++ b/docs/source/python/information/scaledinfo.rst @@ -0,0 +1,8 @@ +.. _c104.ScaledInfo: + +ScaledInfo +########### + +.. autoclass:: c104.ScaledInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/shortcmd.rst b/docs/source/python/information/shortcmd.rst new file mode 100644 index 0000000..b018d1f --- /dev/null +++ b/docs/source/python/information/shortcmd.rst @@ -0,0 +1,8 @@ +.. _c104.ShortCmd: + +ShortCmd +########### + +.. autoclass:: c104.ShortCmd + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/shortinfo.rst b/docs/source/python/information/shortinfo.rst new file mode 100644 index 0000000..38f66fb --- /dev/null +++ b/docs/source/python/information/shortinfo.rst @@ -0,0 +1,8 @@ +.. _c104.ShortInfo: + +ShortInfo +########### + +.. autoclass:: c104.ShortInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/singlecmd.rst b/docs/source/python/information/singlecmd.rst new file mode 100644 index 0000000..e1ab962 --- /dev/null +++ b/docs/source/python/information/singlecmd.rst @@ -0,0 +1,8 @@ +.. _c104.SingleCmd: + +SingleCmd +########### + +.. autoclass:: c104.SingleCmd + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/singleinfo.rst b/docs/source/python/information/singleinfo.rst new file mode 100644 index 0000000..c788f4a --- /dev/null +++ b/docs/source/python/information/singleinfo.rst @@ -0,0 +1,8 @@ +.. _c104.SingleInfo: + +SingleInfo +########### + +.. autoclass:: c104.SingleInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/statusandchanged.rst b/docs/source/python/information/statusandchanged.rst new file mode 100644 index 0000000..dd41f43 --- /dev/null +++ b/docs/source/python/information/statusandchanged.rst @@ -0,0 +1,8 @@ +.. _c104.StatusAndChanged: + +StatusAndChanged +################ + +.. autoclass:: c104.StatusAndChanged + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/stepcmd.rst b/docs/source/python/information/stepcmd.rst new file mode 100644 index 0000000..8dba80d --- /dev/null +++ b/docs/source/python/information/stepcmd.rst @@ -0,0 +1,8 @@ +.. _c104.StepCmd: + +StepCmd +########### + +.. autoclass:: c104.StepCmd + :members: + :exclude-members: __new__ diff --git a/docs/source/python/information/stepinfo.rst b/docs/source/python/information/stepinfo.rst new file mode 100644 index 0000000..3d9a4fa --- /dev/null +++ b/docs/source/python/information/stepinfo.rst @@ -0,0 +1,8 @@ +.. _c104.StepInfo: + +StepInfo +########### + +.. autoclass:: c104.StepInfo + :members: + :exclude-members: __new__ diff --git a/docs/source/python/number/byte32.rst b/docs/source/python/number/byte32.rst new file mode 100644 index 0000000..f0c49ff --- /dev/null +++ b/docs/source/python/number/byte32.rst @@ -0,0 +1,8 @@ +.. _c104.Byte32: + +Raw Bytes (32-bit) +################## + +.. autoclass:: c104.Byte32 + :members: + :exclude-members: __new__ diff --git a/docs/source/python/number/index.rst b/docs/source/python/number/index.rst new file mode 100644 index 0000000..2fa920e --- /dev/null +++ b/docs/source/python/number/index.rst @@ -0,0 +1,13 @@ +Number +======= + +.. toctree:: + :maxdepth: 4 + + byte32 + int7 + int16 + normalizedfloat + uint5 + uint7 + uint16 diff --git a/docs/source/python/number/int16.rst b/docs/source/python/number/int16.rst new file mode 100644 index 0000000..7c9abb6 --- /dev/null +++ b/docs/source/python/number/int16.rst @@ -0,0 +1,8 @@ +.. _c104.Int16: + +Signed Integer (16-bit) +######################## + +.. autoclass:: c104.Int16 + :members: + :exclude-members: __new__ diff --git a/docs/source/python/number/int7.rst b/docs/source/python/number/int7.rst new file mode 100644 index 0000000..f53324c --- /dev/null +++ b/docs/source/python/number/int7.rst @@ -0,0 +1,8 @@ +.. _c104.Int7: + +Signed Integer (7-bit) +######################## + +.. autoclass:: c104.Int7 + :members: + :exclude-members: __new__ diff --git a/docs/source/python/number/normalizedfloat.rst b/docs/source/python/number/normalizedfloat.rst new file mode 100644 index 0000000..5f2886f --- /dev/null +++ b/docs/source/python/number/normalizedfloat.rst @@ -0,0 +1,8 @@ +.. _c104.NormalizedFloat: + +Normalized Float +################ + +.. autoclass:: c104.NormalizedFloat + :members: + :exclude-members: __new__ diff --git a/docs/source/python/number/uint16.rst b/docs/source/python/number/uint16.rst new file mode 100644 index 0000000..3bd3f14 --- /dev/null +++ b/docs/source/python/number/uint16.rst @@ -0,0 +1,8 @@ +.. _c104.UInt16: + +Usigned Integer (16-bit) +######################## + +.. autoclass:: c104.UInt16 + :members: + :exclude-members: __new__ diff --git a/docs/source/python/number/uint5.rst b/docs/source/python/number/uint5.rst new file mode 100644 index 0000000..97340dd --- /dev/null +++ b/docs/source/python/number/uint5.rst @@ -0,0 +1,8 @@ +.. _c104.UInt5: + +Usigned Integer (5-bit) +######################## + +.. autoclass:: c104.UInt5 + :members: + :exclude-members: __new__ diff --git a/docs/source/python/number/uint7.rst b/docs/source/python/number/uint7.rst new file mode 100644 index 0000000..a7b6307 --- /dev/null +++ b/docs/source/python/number/uint7.rst @@ -0,0 +1,8 @@ +.. _c104.UInt7: + +Usigned Integer (7-bit) +######################## + +.. autoclass:: c104.UInt7 + :members: + :exclude-members: __new__ diff --git a/examples/dump_client.py b/examples/dump_client.py new file mode 100644 index 0000000..d9f0044 --- /dev/null +++ b/examples/dump_client.py @@ -0,0 +1,118 @@ +import functools +import time +import c104 + +c104.set_debug_mode(mode=c104.Debug.Client|c104.Debug.Connection) +print("CL] DEBUG MODE: {0}".format(c104.get_debug_mode())) + +my_client = c104.Client(tick_rate_ms=1000, command_timeout_ms=5000) +my_client.originator_address = 123 +cl_connection_1 = my_client.add_connection(ip="127.0.0.1", port=2404, init=c104.Init.ALL) + + +################################## +# CONNECTION STATE HANDLER +################################## + +def cl_ct_on_state_change(connection: c104.Connection, state: c104.ConnectionState) -> None: + print("CL] Connection State Changed {0} | State {1}".format(connection.originator_address, state)) + + +cl_connection_1.on_state_change(callable=cl_ct_on_state_change) + + +################################## +# NEW DATA HANDLER +################################## + +def cl_pt_on_receive_point(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + print("CL] {0} REPORT on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) + # print("{0}".format(message.is_negative)) + # print("-->| POINT: 0x{0} | EXPLAIN: {1}".format(message.raw.hex(), c104.explain_bytes(apdu=message.raw))) + return c104.ResponseState.SUCCESS + + +################################## +# NEW OBJECT HANDLER +################################## + +def cl_on_new_station(client: c104.Client, connection: c104.Connection, common_address: int, custom_arg: str, y: str = "default value") -> None: + print("CL] NEW STATION {0} | CLIENT OA {1}".format(common_address, client.originator_address)) + connection.add_station(common_address=common_address) + + +def cl_on_new_point(client: c104.Client, station: c104.Station, io_address: int, point_type: c104.Type) -> None: + print("CL] NEW POINT: {1} with IOA {0} | CLIENT OA {2}".format(io_address, point_type, client.originator_address)) + point = station.add_point(io_address=io_address, type=point_type) + point.on_receive(callable=cl_pt_on_receive_point) + + +my_client.on_new_station(callable=functools.partial(cl_on_new_station, custom_arg="extra argument with default/bounded value passes signature check")) +my_client.on_new_point(callable=cl_on_new_point) + + +################################## +# RAW MESSAGE HANDLER +################################## + +def cl_ct_on_receive_raw(connection: c104.Connection, data: bytes) -> None: + print("CL] <-in-- {1} [{0}] | CONN OA {2}".format(data.hex(), c104.explain_bytes_dict(apdu=data), connection.originator_address)) + + +def cl_ct_on_send_raw(connection: c104.Connection, data: bytes) -> None: + print("CL] -out-> {1} [{0}] | CONN OA {2}".format(data.hex(), c104.explain_bytes_dict(apdu=data), connection.originator_address)) + + +cl_connection_1.on_receive_raw(callable=cl_ct_on_receive_raw) +cl_connection_1.on_send_raw(callable=cl_ct_on_send_raw) + +################################## +# Dump points +################################## + +def cl_dump(): + global my_client, cl_connection_1 + if cl_connection_1.is_connected: + print("") + cl_ct_count = len(my_client.connections) + print("CL] |--+ CLIENT has {0} connections".format(cl_ct_count)) + for ct_iter in range(cl_ct_count): + ct = my_client.connections[ct_iter] + ct_st_count = len(ct.stations) + print(" |--+ CONNECTION has {0} stations".format(ct_st_count)) + for st_iter in range(ct_st_count): + st = ct.stations[st_iter] + st_pt_count = len(st.points) + print(" |--+ STATION {0} has {1} points".format(st.common_address, st_pt_count)) + print(" | TYPE | IOA | VALUE | PROCESSED AT | RECORDED AT | QUALITY ") + print(" |----------------|---------|--------------------|----------------------------|----------------------------|-------------------") + for pt_iter in range(st_pt_count): + pt = st.points[pt_iter] + print(" | %s | %7s | %18s | %26s | %26s | %s" % (pt.type, pt.io_address, pt.value, pt.processed_at.isoformat(), + pt.recorded_at and pt.recorded_at.isoformat() or 'N. A.', pt.quality)) + print(" |----------------|---------|--------------------|----------------------------|----------------------------|-------------------") + + +################################## +# connect loop +################################## + +my_client.start() + +while not cl_connection_1.is_connected: + print("CL] Waiting for connection to {0}:{1}".format(cl_connection_1.ip, cl_connection_1.port)) + time.sleep(1) + +################################## +# Loop through points +################################## + +while cl_connection_1.is_connected: + cl_dump() + time.sleep(3) + +################################## +# done +################################## + +my_client.stop() diff --git a/examples/simple_client.py b/examples/simple_client.py index 77092c6..7196e31 100644 --- a/examples/simple_client.py +++ b/examples/simple_client.py @@ -5,8 +5,8 @@ def main(): # client, connection and station preparation - client = c104.Client(tick_rate_ms=1000, command_timeout_ms=1000) - connection = client.add_connection(ip="127.0.0.1", port=2404, init=c104.Init.INTERROGATION) + client = c104.Client() + connection = client.add_connection(ip="127.0.0.1", port=2404, init=c104.Init.ALL) station = connection.add_station(common_address=47) # monitoring point preparation @@ -19,11 +19,11 @@ def main(): # start client.start() - while not connection.is_connected: + while connection.state != c104.ConnectionState.OPEN: print("Waiting for connection to {0}:{1}".format(connection.ip, connection.port)) time.sleep(1) - #time.sleep(3) + print(f"-> AFTER INIT {point.value}") print("read") print("read") @@ -42,7 +42,8 @@ def main(): print("-> SUCCESS") else: print("-> FAILURE") - #time.sleep(3) + + time.sleep(3) print("exit") print("exit") @@ -50,6 +51,5 @@ def main(): if __name__ == "__main__": - c104.set_debug_mode(c104.Debug.Client|c104.Debug.Connection) + # c104.set_debug_mode(c104.Debug.Client|c104.Debug.Connection|c104.Debug.Point|c104.Debug.Callback) main() - time.sleep(1) diff --git a/examples/simple_server.py b/examples/simple_server.py index 9b657ff..5084a81 100644 --- a/examples/simple_server.py +++ b/examples/simple_server.py @@ -3,10 +3,10 @@ import time -def on_step_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: +def on_step_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: """ handle incoming regulating step command """ - print("{0} STEP COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality)) + print("{0} STEP COMMAND on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) if point.value == c104.Step.LOWER: # do something @@ -19,22 +19,29 @@ def on_step_command(point: c104.Point, previous_state: dict, message: c104.Incom return c104.ResponseState.FAILURE -def before_transmit(point: c104.Point) -> None: +def before_auto_transmit(point: c104.Point) -> None: """ update point value before transmission """ point.value = random.random() * 100 - print("{0} BEFORE TRANSMIT on IOA: {1}".format(point.type, point.io_address)) + print("{0} BEFORE AUTOMATIC REPORT on IOA: {1} VALUE: {2}".format(point.type, point.io_address, point.value)) + + +def before_read(point: c104.Point) -> None: + """ update point value before transmission + """ + point.value = random.random() * 100 + print("{0} BEFORE READ or INTERROGATION on IOA: {1} VALUE: {2}".format(point.type, point.io_address, point.value)) def main(): # server and station preparation - server = c104.Server(ip="0.0.0.0", port=2404) + server = c104.Server() station = server.add_station(common_address=47) # monitoring point preparation - point = station.add_point(io_address=11, type=c104.Type.M_ME_NC_1, report_ms=15000) - point.on_before_auto_transmit(callable=before_transmit) - point.on_before_read(callable=before_transmit) + point = station.add_point(io_address=11, type=c104.Type.M_ME_NC_1, report_ms=1000) + point.on_before_auto_transmit(callable=before_auto_transmit) + point.on_before_read(callable=before_read) # command point preparation command = station.add_point(io_address=12, type=c104.Type.C_RC_TA_1) @@ -57,6 +64,5 @@ def main(): if __name__ == "__main__": - c104.set_debug_mode(c104.Debug.Server) + # c104.set_debug_mode(c104.Debug.Server|c104.Debug.Point|c104.Debug.Callback) main() - time.sleep(1) diff --git a/pyproject.toml b/pyproject.toml index 3dab213..60dfb6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,11 +3,11 @@ requires = [ "setuptools>=42", "wheel", "ninja", - "cmake>=3.18", + "cmake>=3.18,<3.28", #3.29 requires libssl.3 ] build-backend = "setuptools.build_meta" [tool.black] line-length = 120 -target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311', 'py312'] +target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312'] skip-string-normalization = true diff --git a/setup.cfg b/setup.cfg index c0e047f..5e963d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = c104 -version = 1.18.0 +version = 2.0.0 long_description = file: README.md long_description_content_type = text/markdown description = A Python module to simulate SCADA and RTU communication over protocol 60870-5-104 to research ICT behavior in power grids. @@ -16,7 +16,6 @@ classifiers = Topic :: Utilities Programming Language :: C++ Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -47,5 +46,5 @@ project_urls = Source Code = https://github.com/Fraunhofer-FIT-DIEN/iec104-python [options] -python_requires = >=3.6 +python_requires = >=3.7 zip_safe = False diff --git a/setup.py b/setup.py index 3e2f2ca..12b539f 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def build_extension(self, ext: CMakeExtension) -> None: # from Python. cmake_args = [ f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}", - f"-DPYTHON_EXECUTABLE={sys.executable}", + f"-DPython_EXECUTABLE={sys.executable}", f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm ] build_args = [ diff --git a/src/Client.cpp b/src/Client.cpp index a4d32fb..46138fd 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -1,5 +1,5 @@ /** - * Copyright 2020-2023 Fraunhofer Institute for Applied Information Technology + * Copyright 2020-2024 Fraunhofer Institute for Applied Information Technology * FIT * * This file is part of iec104-python. @@ -40,11 +40,13 @@ using namespace Remote; using namespace std::chrono_literals; -Client::Client(const std::uint_fast32_t tick_rate_ms, - const std::uint_fast32_t timeout_ms, +Client::Client(const std::uint_fast16_t tick_rate_ms, + const std::uint_fast16_t timeout_ms, std::shared_ptr transport_security) : tickRate_ms(tick_rate_ms), commandTimeout_ms(timeout_ms), security(std::move(transport_security)) { + if (tickRate_ms < 50) + throw std::range_error("tickRate_ms must be 50 or greater"); DEBUG_PRINT(Debug::Client, "Created"); } @@ -72,6 +74,8 @@ void Client::start() { runThread = new std::thread(&Client::thread_run, this); } + schedulePeriodicTask([this]() { scheduleDataPointTimer(); }, tickRate_ms); + { std::lock_guard const con_lock(connections_mutex); for (auto &c : connections) { @@ -85,9 +89,7 @@ void Client::start() { void Client::stop() { Module::ScopedGilRelease const scoped("Client.stop"); - // stop all connections - disconnectAll(); - + // stop active connection management first bool expected = true; if (!enabled.compare_exchange_strong(expected, false)) { DEBUG_PRINT(Debug::Client, "stop] Already stopped"); @@ -104,6 +106,9 @@ void Client::stop() { runThread = nullptr; } + // stop all connections + disconnectAll(); + DEBUG_PRINT(Debug::Client, "stop] Stopped"); } @@ -134,6 +139,48 @@ bool Client::hasConnections() { return !connections.empty(); } +bool Client::hasOpenConnections() const { + std::lock_guard const lock(connections_mutex); + for (auto &c : connections) { + if (c->isOpen()) { + return true; + } + } + return false; +} + +std::uint_fast8_t Client::getOpenConnectionCount() const { + std::uint_fast8_t count = 0; + std::lock_guard const lock(connections_mutex); + for (auto &c : connections) { + if (c->isOpen()) { + count++; + } + } + return count; +} + +bool Client::hasActiveConnections() const { + std::lock_guard const lock(connections_mutex); + for (auto &c : connections) { + if (ConnectionState::OPEN == c->getState()) { + return true; + } + } + return false; +} + +std::uint_fast8_t Client::getActiveConnectionCount() const { + std::uint_fast8_t count = 0; + std::lock_guard const lock(connections_mutex); + for (auto &c : connections) { + if (ConnectionState::OPEN == c->getState()) { + count++; + } + } + return count; +} + Remote::ConnectionVector Client::getConnections() { std::lock_guard const lock(connections_mutex); @@ -179,7 +226,7 @@ std::shared_ptr Client::addConnection(const std::string &ip, const uint_fast16_t port, const ConnectionInit init) { if (hasConnection(ip, port)) { - std::cerr << "[c104.Client.add_connection] Connection already exists" + std::cerr << "[c104.Client] add_connection] Connection already exists" << std::endl; return {nullptr}; } @@ -250,7 +297,7 @@ void Client::onNewPoint(std::shared_ptr station, DEBUG_PRINT(Debug::Client, "CALLBACK on_new_point (default: add point)"); // default behaviour try { - station->addPoint(io_address, type, 0, 0); + station->addPoint(io_address, type, 0, std::nullopt); } catch (const std::exception &e) { DEBUG_PRINT(Debug::Client, "on_new_point] Failed to add point: " + std::string(e.what())); @@ -258,81 +305,107 @@ void Client::onNewPoint(std::shared_ptr station, } } -void Client::thread_run() { - running.store(true); - std::unique_lock lock(runThread_mutex); - std::chrono::system_clock::time_point desiredEnd; - bool debug = false; - uint_fast8_t count = 0; - uint_fast8_t active = 0; +std::uint_fast16_t Client::getTickRate_ms() const { return tickRate_ms; } - while (enabled.load()) { - debug = DEBUG_TEST(Debug::Client); - desiredEnd = std::chrono::system_clock::now() + tickRate_ms.load() * 1ms; +void Client::scheduleDataPointTimer() { + uint16_t counter = 0; + auto now = std::chrono::steady_clock::now(); - count = 0; - active = 0; - std::unique_lock con_lock(connections_mutex); - for (auto &c : connections) { - ConnectionState const s = c->getState(); - switch (s) { - case OPEN_MUTED: - case OPEN: - count++; - if (!c->isMuted()) { - active++; - } - break; - case OPEN_AWAIT_INTERROGATION: - count++; - try { - c->interrogation(IEC60870_GLOBAL_COMMON_ADDRESS, CS101_COT_ACTIVATION, - QOI_STATION, false); - } catch (const std::exception &e) { - std::cerr << "[c104.Client.loop] Failed to send connection " - "initiation interrogation: " - << e.what() << std::endl; + for (const auto &c : getConnections()) { + if (c->isOpen() && !c->isMuted()) { + for (const auto &station : c->getStations()) { + for (const auto &point : station->getPoints()) { + auto next = point->nextTimerAt(); + if (next.has_value() && next.value() < now) { + scheduleTask([point]() { point->onTimer(); }, counter++); + } } - break; - case OPEN_AWAIT_CLOCK_SYNC: - count++; - c->clockSync(IEC60870_GLOBAL_COMMON_ADDRESS, false); - break; - case CLOSED_AWAIT_RECONNECT: - c->connect(); - break; } } - con_lock.unlock(); - - if (count != openConnections.load()) { - openConnections.store(count); + } +} - DEBUG_PRINT_CONDITION(debug, Debug::Client, - "thread_run] Connected servers: " + - std::to_string(count)); +void Client::schedulePeriodicTask(const std::function &task, + int interval) { + { + if (interval < 50) { + throw std::out_of_range( + "The interval for periodic tasks must be 1000ms at minimum."); } + auto periodic = std::make_shared>([]() {}); + *periodic = [this, task, interval, periodic]() { + // Schedule next execution + scheduleTask(*periodic, interval); + task(); + }; + // Schedule first execution + scheduleTask(*periodic, interval); + } - if (active != activeConnections.load()) { - activeConnections.store(active); + runThread_wait.notify_one(); +} - DEBUG_PRINT_CONDITION(debug, Debug::Client, - "thread_run] Active servers: " + - std::to_string(count)); +void Client::scheduleTask(const std::function &task, int delay) { + { + std::lock_guard lock(runThread_mutex); + if (delay < 0) { + tasks.push({task, std::chrono::steady_clock::time_point::min()}); + } else { + tasks.push({task, std::chrono::steady_clock::now() + + std::chrono::milliseconds(delay)}); } + } - runThread_wait.wait_until(lock, desiredEnd); - if (debug) { - auto diff = std::chrono::duration_cast( - std::chrono::system_clock::now() - desiredEnd) - .count(); - if (diff > 5000) { - DEBUG_PRINT_CONDITION(true, Debug::Client, - "thread_run] Cannot keep up the tick rate: " + - std::to_string(diff) + u8" \xb5s"); + runThread_wait.notify_one(); +} + +void Client::thread_run() { + bool const debug = DEBUG_TEST(Debug::Client); + running.store(true); + while (enabled.load()) { + std::function task; + + { + std::unique_lock lock(runThread_mutex); + if (tasks.empty()) { + runThread_wait.wait(lock); + continue; + } + + auto now = std::chrono::steady_clock::now(); + if (now >= tasks.top().schedule_time) { + auto delay = now - tasks.top().schedule_time; + if (delay > TASK_DELAY_THRESHOLD) { + DEBUG_PRINT_CONDITION( + debug, Debug::Client, + "Warning: Task started delayed by " + + std::to_string( + std::chrono::duration_cast( + delay) + .count()) + + " ms"); + } + task = tasks.top().function; + tasks.pop(); + } else { + runThread_wait.wait_until(lock, tasks.top().schedule_time); + continue; } } - } + try { + task(); + } catch (const std::exception &e) { + std::cerr << "[c104.Client] loop] Task aborted: " << e.what() + << std::endl; + } + } + if (!tasks.empty()) { + DEBUG_PRINT_CONDITION(debug, Debug::Client, + "loop] Tasks dropped due to stop: " + + std::to_string(tasks.size())); + std::priority_queue empty; + std::swap(tasks, empty); + } running.store(false); } diff --git a/src/Client.h b/src/Client.h index 2fe01a0..074239b 100644 --- a/src/Client.h +++ b/src/Client.h @@ -1,5 +1,5 @@ /** - * Copyright 2020-2023 Fraunhofer Institute for Applied Information Technology + * Copyright 2020-2024 Fraunhofer Institute for Applied Information Technology * FIT * * This file is part of iec104-python. @@ -43,6 +43,7 @@ * @brief service model for IEC60870-5-104 communication as client */ class Client : public std::enable_shared_from_this { + // @todo import/export station with DataPoints to string // @todo add callback each packet public: @@ -51,8 +52,8 @@ class Client : public std::enable_shared_from_this { Client &operator=(const Client &) = delete; [[nodiscard]] static std::shared_ptr create( - std::uint_fast32_t tick_rate_ms = 1000, - std::uint_fast32_t timeout_ms = 1000, + std::uint_fast16_t tick_rate_ms = 100, + std::uint_fast16_t timeout_ms = 100, std::shared_ptr transport_security = nullptr) { // Not using std::make_shared because the constructor is private. return std::shared_ptr( @@ -98,6 +99,31 @@ class Client : public std::enable_shared_from_this { bool hasConnections(); + /** + * @brief Test if Client has open connections to clients + * @return information if at least one connection exists + */ + bool hasOpenConnections() const; + + /** + * @brief get number of open connections to servers + * @return open connection count + */ + std::uint_fast8_t getOpenConnectionCount() const; + + /** + * @brief Test if Client has active (open and not muted) connections to + * servers + * @return information if at least one connection is active + */ + bool hasActiveConnections() const; + + /** + * @brief get number of active (open and not muted) connections to servers + * @return active connection count + */ + std::uint_fast8_t getActiveConnectionCount() const; + Remote::ConnectionVector getConnections(); bool hasConnection(const std::string &ip, @@ -161,21 +187,34 @@ class Client : public std::enable_shared_from_this { void onNewPoint(std::shared_ptr station, std::uint_fast32_t io_address, IEC60870_5_TypeID type); + std::uint_fast16_t getTickRate_ms() const; + + void schedulePeriodicTask(const std::function &task, int interval); + void scheduleTask(const std::function &task, int delay = 0); + private: + void scheduleDataPointTimer(); /** * @brief Create a new remote connection handler instance that acts as a * client * @details create a map of possible connections - * @param tick_rate_ms intervall in milliseconds between the client checks connection states + * @param tick_rate_ms intervall in milliseconds between the client checks + * connection states * @param timeout_ms timeout in milliseconds before an inactive connection * @param transport_security communication encryption instance reference * gets closed */ - Client(std::uint_fast32_t tick_rate_ms, std::uint_fast32_t timeout_ms, + Client(std::uint_fast16_t tick_rate_ms, std::uint_fast16_t timeout_ms, std::shared_ptr transport_security); + /// @brief minimum interval between to periodic broadcasts in milliseconds + const std::uint_fast16_t tickRate_ms{1000}; + + /// @brief timeout in milliseconds before an inactive connection gets closed + const std::uint_fast16_t commandTimeout_ms{100}; + /// @brief tls handler - std::shared_ptr security{nullptr}; + const std::shared_ptr security{nullptr}; /// @brief originator address of outgoing messages std::atomic_uint_fast8_t originatorAddress{0}; @@ -183,8 +222,8 @@ class Client : public std::enable_shared_from_this { /// @brief state that describes if the client component is enabled or not std::atomic_bool enabled{false}; - /// @brief timeout in milliseconds before an inactive connection gets closed - std::atomic_uint_fast32_t commandTimeout_ms{1000}; + /// @brief client thread state + std::atomic_bool running{false}; /// @brief MUTEX Lock to access connectionMap mutable Module::GilAwareMutex connections_mutex{"Client::connections_mutex"}; @@ -192,21 +231,11 @@ class Client : public std::enable_shared_from_this { /// @brief list of all created connections to remote servers Remote::ConnectionVector connections; - /// @brief number of active connections - std::atomic_uint_fast8_t activeConnections{0}; - - /// @brief number of open connections - std::atomic_uint_fast8_t openConnections{0}; - - /// @brief minimum interval between to reconnects in milliseconds - std::atomic_uint_fast32_t tickRate_ms{1000}; + std::priority_queue tasks; /// @brief client thread to execute reconnects std::thread *runThread = nullptr; - /// @brief client thread state - std::atomic_bool running{false}; - /// @brief client thread mutex to not lock thread execution mutable std::mutex runThread_mutex{}; @@ -235,6 +264,21 @@ class Client : public std::enable_shared_from_this { */ std::shared_ptr getConnectionFromString(const std::string &connectionString); + +public: + std::string toString() const { + size_t len = 0; + { + std::scoped_lock const lock(connections_mutex); + len = connections.size(); + } + std::ostringstream oss; + oss << "<104.Client originator_address=" + << std::to_string(originatorAddress.load()) + << ", #connections=" << std::to_string(len) << " at " << std::hex + << std::showbase << reinterpret_cast(this) << ">"; + return oss.str(); + }; }; #endif // C104_CLIENT_H diff --git a/src/Server.cpp b/src/Server.cpp index 9420524..b343820 100644 --- a/src/Server.cpp +++ b/src/Server.cpp @@ -35,23 +35,27 @@ #include "remote/TransportSecurity.h" #include "remote/message/PointCommand.h" #include "remote/message/PointMessage.h" -#include +#include using namespace Remote; using namespace std::chrono_literals; Server::Server(const std::string &bind_ip, const std::uint_fast16_t tcp_port, - const std::uint_fast32_t tick_rate_ms, + const std::uint_fast16_t tick_rate_ms, + const std::uint_fast16_t select_timeout_ms, const std::uint_fast8_t max_open_connections, std::shared_ptr transport_security) : ip(bind_ip), port(tcp_port), tickRate_ms(tick_rate_ms), - maxOpenConnections(max_open_connections) { + selectTimeout_ms(select_timeout_ms), + maxOpenConnections(max_open_connections), + security(std::move(transport_security)) { + if (tickRate_ms < 50) + throw std::range_error("tickRate_ms must be 50 or greater"); // create a new slave/server instance with default connection parameters and // default message queue size - if (transport_security.get() != nullptr) { - slave = CS104_Slave_createSecure(100, 100, transport_security->get()); - security = std::move(transport_security); + if (security) { + slave = CS104_Slave_createSecure(100, 100, security->get()); } else { slave = CS104_Slave_create(100, 100); } @@ -125,7 +129,6 @@ void Server::start() { Module::ScopedGilRelease const scoped("Server.start"); CS104_Slave_start(slave); - std::this_thread::sleep_for(1s); if (!CS104_Slave_isRunning(slave)) { enabled.store(false); @@ -137,51 +140,116 @@ void Server::start() { if (!runThread) { runThread = new std::thread(&Server::thread_run, this); } -} - -void Server::thread_run() { - running.store(true); - std::unique_lock lock(runThread_mutex); - uint_fast16_t count = 0; - std::chrono::system_clock::time_point desiredEnd; - bool debug = false; - while (enabled.load()) { - debug = DEBUG_TEST(Debug::Server); - desiredEnd = std::chrono::system_clock::now() + tickRate_ms.load() * 1ms; + // Schedule periodics based on tickRate + schedulePeriodicTask([this]() { sendInventory(CS101_COT_PERIODIC); }, + tickRate_ms); + schedulePeriodicTask( + [this]() { + cleanupSelections(); + scheduleDataPointTimer(); + }, + tickRate_ms); +} - count = CS104_Slave_getOpenConnections(slave); - if (count != openConnections) { - openConnections = count; +void Server::scheduleDataPointTimer() { + if (!hasActiveConnections()) + return; - DEBUG_PRINT_CONDITION(debug, Debug::Server, - "thread_run] Connected clients: " + - std::to_string(count)); + uint16_t counter = 0; + auto now = std::chrono::steady_clock::now(); + for (const auto &station : getStations()) { + for (const auto &point : station->getPoints()) { + auto next = point->nextTimerAt(); + if (next.has_value() && next.value() < now) { + scheduleTask([point]() { point->onTimer(); }, counter++); + } } + } +} - // @TODO periodic transmission should be handled on per connection basis or - // dropped - if (activeConnections) { - sendInterrogationResponse(CS101_COT_PERIODIC); +void Server::schedulePeriodicTask(const std::function &task, + int interval) { + { + if (interval < 50) { + throw std::out_of_range( + "The interval for periodic tasks must be 1000ms at minimum."); } + auto periodic = std::make_shared>([]() {}); + *periodic = [this, task, interval, periodic]() { + // Schedule next execution + scheduleTask(*periodic, interval); + task(); + }; + // Schedule first execution + scheduleTask(*periodic, interval); + } - runThread_wait.wait_until(lock, desiredEnd); + runThread_wait.notify_one(); +} - if (debug) { - auto diff = std::chrono::duration_cast( - std::chrono::system_clock::now() - desiredEnd) - .count(); - if (diff > 5000) { - DEBUG_PRINT_CONDITION(true, Debug::Server, - "thread_run] Cannot keep up the tick rate: " + - std::to_string(diff) + u8" \xb5s"); - } /* else { - DEBUG_PRINT_CONDITION(true, Debug::Server, "Server.thread_run] Tick | - CON " + std::to_string(activeConnections)); - }*/ +void Server::scheduleTask(const std::function &task, int delay) { + { + std::lock_guard lock(runThread_mutex); + if (delay < 0) { + tasks.push({task, std::chrono::steady_clock::time_point::min()}); + } else { + tasks.push({task, std::chrono::steady_clock::now() + + std::chrono::milliseconds(delay)}); } } + runThread_wait.notify_one(); +} + +void Server::thread_run() { + bool const debug = DEBUG_TEST(Debug::Server); + running.store(true); + while (enabled.load()) { + std::function task; + + { + std::unique_lock lock(runThread_mutex); + if (tasks.empty()) { + runThread_wait.wait(lock); + continue; + } + + auto now = std::chrono::steady_clock::now(); + if (now >= tasks.top().schedule_time) { + auto delay = now - tasks.top().schedule_time; + if (delay > TASK_DELAY_THRESHOLD) { + DEBUG_PRINT_CONDITION( + debug, Debug::Server, + "Warning: Task started delayed by " + + std::to_string( + std::chrono::duration_cast( + delay) + .count()) + + " ms"); + } + task = tasks.top().function; + tasks.pop(); + } else { + runThread_wait.wait_until(lock, tasks.top().schedule_time); + continue; + } + } + + try { + task(); + } catch (const std::exception &e) { + std::cerr << "[c104.Server] loop] Task aborted: " << e.what() + << std::endl; + } + } + if (!tasks.empty()) { + DEBUG_PRINT_CONDITION(debug, Debug::Server, + "loop] Tasks dropped due to stop: " + + std::to_string(tasks.size())); + std::priority_queue empty; + std::swap(tasks, empty); + } running.store(false); } @@ -204,9 +272,7 @@ void Server::stop() { } CS104_Slave_stop(slave); - for (auto &it : connectionMap) { - IMasterConnection_close(it.first); - } + connectionMap.clear(); activeConnections.store(0); openConnections.store(0); @@ -222,10 +288,19 @@ bool Server::hasStations() const { return !stations.empty(); } -bool Server::hasOpenConnections() const { return openConnections.load() > 0; } +bool Server::isExistingConnection(IMasterConnection connection) { + std::lock_guard const lock(connection_mutex); + + auto it = connectionMap.find(connection); + return it != connectionMap.end(); +} + +bool Server::hasOpenConnections() const { + return CS104_Slave_getOpenConnections(slave) > 0; +} std::uint_fast8_t Server::getOpenConnectionCount() const { - return openConnections.load(); + return CS104_Slave_getOpenConnections(slave); } bool Server::hasActiveConnections() const { @@ -233,7 +308,7 @@ bool Server::hasActiveConnections() const { } std::uint_fast8_t Server::getActiveConnectionCount() const { - return openConnections.load(); + return activeConnections.load(); } Object::StationVector Server::getStations() const { @@ -281,19 +356,168 @@ Server::addStation(std::uint_fast16_t commonAddress) { return station; } +void Server::cleanupSelections() { + auto now = std::chrono::steady_clock::now(); + std::lock_guard const lock(selection_mutex); + selectionVector.erase( + std::remove_if(selectionVector.begin(), selectionVector.end(), + [this, now](const Selection &s) { + if ((now - s.created) < selectTimeout_ms) { + return false; + } else { + unselect(s); + return true; + } + }), + selectionVector.end()); +} + +void Server::cleanupSelections(IMasterConnection connection) { + std::lock_guard const lock(selection_mutex); + selectionVector.erase(std::remove_if(selectionVector.begin(), + selectionVector.end(), + [this, connection](const Selection &s) { + if (s.connection == connection) { + unselect(s); + return true; + } else { + return false; + } + }), + selectionVector.end()); +} + +void Server::cleanupSelection(uint16_t ca, uint32_t ioa) { + std::lock_guard const lock(selection_mutex); + selectionVector.erase(std::remove_if(selectionVector.begin(), + selectionVector.end(), + [this, ca, ioa](const Selection &s) { + if (s.ca == ca && s.ioa == ioa) { + unselect(s); + return true; + } else { + return false; + } + }), + selectionVector.end()); +} + +std::optional Server::getSelector(const uint16_t ca, + const uint32_t ioa) { + auto now = std::chrono::steady_clock::now(); + std::lock_guard const lock(selection_mutex); + auto it = std::find_if( + selectionVector.begin(), selectionVector.end(), + [ca, ioa](const Selection &s) { return s.ca == ca && s.ioa == ioa; }); + + // selection NOT found + if (it != selectionVector.end() && (now - it->created) < selectTimeout_ms) { + return it->oa; + } + return std::nullopt; +} + +bool Server::select(IMasterConnection connection, + std::shared_ptr message) { + const auto type = message->getType(); + if (type < C_SC_NA_1 || C_BO_NA_1 == type || C_SE_TC_1 < type) { + throw std::invalid_argument( + "Only control points, except for binary commands can be selected"); + } + + const uint8_t oa = message->getOriginatorAddress(); + const uint16_t ca = message->getCommonAddress(); + const uint32_t ioa = message->getIOA(); + auto now = std::chrono::steady_clock::now(); + + std::lock_guard const lock(selection_mutex); + auto it = std::find_if( + selectionVector.begin(), selectionVector.end(), + [ca, ioa](const Selection &s) { return s.ca == ca && s.ioa == ioa; }); + + // selection NOT found + if (it == selectionVector.end()) { + CS101_ASDU cp = CS101_ASDU_clone(message->getAsdu(), nullptr); + selectionVector.emplace_back({cp, oa, ca, ioa, connection, now}); + } + // selection found + else { + if ((it->connection != connection) && + (now - it->created) < selectTimeout_ms) { + return false; + } else { + it->created = now; + } + } + + return true; +} + +void Server::unselect(const Selection &selection) { + scheduleTask([this, selection]() { + sendActivationTermination(selection.connection, selection.asdu); + CS101_ASDU_destroy(selection.asdu); + }); +} + +CommandResponseState +Server::execute(IMasterConnection connection, + std::shared_ptr message, + std::shared_ptr point) { + bool selected = false; + const uint16_t ca = message->getCommonAddress(); + const uint32_t ioa = message->getIOA(); + + if (SELECT_AND_EXECUTE_COMMAND == point->getCommandMode()) { + auto now = std::chrono::steady_clock::now(); + + std::lock_guard const lock(selection_mutex); + auto it = std::find_if( + selectionVector.begin(), selectionVector.end(), + [ca, ioa](const Selection &s) { return s.ca == ca && s.ioa == ioa; }); + + // selection NOT found + selected = (it != selectionVector.end() && (it->connection == connection) && + (now - it->created) < selectTimeout_ms); + if (!selected) { + std::cerr << "[c104.Server] Cannot execute command on point in " + "SELECT_AND_EXECUTE " + "command mode without selection" + << std::endl; + return RESPONSE_STATE_FAILURE; + } + } + + auto res = point->onReceive(std::move(message)); + + if (selected) { + scheduleTask([this, ca, ioa]() { cleanupSelection(ca, ioa); }, 1); + } + + return res; +} + void Server::setOnReceiveRawCallback(py::object &callable) { py_onReceiveRaw.reset(callable); } void Server::onReceiveRaw(unsigned char *msg, unsigned char msgSize) { if (py_onReceiveRaw.is_set()) { - DEBUG_PRINT(Debug::Server, "CALLBACK on_receive_raw"); - Module::ScopedGilAcquire const scoped("Server.on_receive_raw"); - PyObject *pymemview = - PyMemoryView_FromMemory((char *)msg, msgSize, PyBUF_READ); - PyObject *pybytes = PyBytes_FromObject(pymemview); + // create a copy + auto *cp = new char[msgSize]; + memcpy(cp, msg, msgSize); + + scheduleTask([this, cp, msgSize]() { + DEBUG_PRINT(Debug::Server, "CALLBACK on_receive_raw"); + Module::ScopedGilAcquire const scoped("Server.on_receive_raw"); + PyObject *pymemview = + PyMemoryView_FromMemory((char *)cp, msgSize, PyBUF_READ); + PyObject *pybytes = PyBytes_FromObject(pymemview); + + py_onReceiveRaw.call(shared_from_this(), py::handle(pybytes)); - py_onReceiveRaw.call(shared_from_this(), py::handle(pybytes)); + delete[] cp; + }); } } @@ -303,13 +527,21 @@ void Server::setOnSendRawCallback(py::object &callable) { void Server::onSendRaw(unsigned char *msg, unsigned char msgSize) { if (py_onSendRaw.is_set()) { - DEBUG_PRINT(Debug::Server, "CALLBACK on_send_raw"); - Module::ScopedGilAcquire const scoped("Server.on_send_raw"); - PyObject *pymemview = - PyMemoryView_FromMemory((char *)msg, msgSize, PyBUF_READ); - PyObject *pybytes = PyBytes_FromObject(pymemview); + // create a copy + auto *cp = new char[msgSize]; + memcpy(cp, msg, msgSize); - py_onSendRaw.call(shared_from_this(), py::handle(pybytes)); + scheduleTask([this, cp, msgSize]() { + DEBUG_PRINT(Debug::Server, "CALLBACK on_send_raw"); + Module::ScopedGilAcquire const scoped("Server.on_send_raw"); + PyObject *pymemview = + PyMemoryView_FromMemory((char *)cp, msgSize, PyBUF_READ); + PyObject *pybytes = PyBytes_FromObject(pymemview); + + py_onSendRaw.call(shared_from_this(), py::handle(pybytes)); + + delete[] cp; + }); } } @@ -317,23 +549,35 @@ void Server::setOnClockSyncCallback(py::object &callable) { py_onClockSync.reset(callable); } -CommandResponseState Server::onClockSync(std::string _ip, CP56Time2a time) { +CommandResponseState +Server::onClockSync(const std::string _ip, + const std::chrono::system_clock::time_point time) { if (py_onClockSync.is_set()) { DEBUG_PRINT(Debug::Server, "CALLBACK on_clock_sync"); Module::ScopedGilAcquire const scoped("Server.on_clock_sync"); PyDateTime_IMPORT; - uint_fast64_t const hrsUtc = - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - uint_fast64_t const century = (1970 + (hrsUtc / (24 * 365))) / 100 * 100; + // pybind11/chrono.h caster code copy + if (!PyDateTimeAPI) { + PyDateTime_IMPORT; + } + + using us_t = std::chrono::duration; + auto us = std::chrono::duration_cast(time.time_since_epoch() % + std::chrono::seconds(1)); + if (us.count() < 0) { + us += std::chrono::seconds(1); + } + + std::time_t tt = std::chrono::system_clock::to_time_t( + std::chrono::time_point_cast(time - + us)); + + std::tm localtime = *std::localtime(&tt); PyObject *pydate = PyDateTime_FromDateAndTime( - century + CP56Time2a_getYear(time), CP56Time2a_getMonth(time), - CP56Time2a_getDayOfMonth(time), CP56Time2a_getHour(time), - CP56Time2a_getMinute(time), CP56Time2a_getSecond(time), - CP56Time2a_getMillisecond(time) * 1000); + localtime.tm_year + 1900, localtime.tm_mon + 1, localtime.tm_mday, + localtime.tm_hour, localtime.tm_min, localtime.tm_sec, us.count()); if (py_onClockSync.call(shared_from_this(), _ip, py::handle(pydate))) { try { @@ -398,9 +642,11 @@ void Server::onUnexpectedMessage( } if (py_onUnexpectedMessage.is_set()) { - DEBUG_PRINT(Debug::Server, "CALLBACK on_unexpected_message"); - Module::ScopedGilAcquire const scoped("Server.on_unexpected_message"); - py_onUnexpectedMessage.call(shared_from_this(), message, cause); + scheduleTask([this, message, cause]() { + DEBUG_PRINT(Debug::Server, "CALLBACK on_unexpected_message"); + Module::ScopedGilAcquire const scoped("Server.on_unexpected_message"); + py_onUnexpectedMessage.call(shared_from_this(), message, cause); + }); } } @@ -408,8 +654,17 @@ void Server::setOnConnectCallback(py::object &callable) { py_onConnect.reset(callable); } +std::uint_fast16_t Server::getTickRate_ms() const { return tickRate_ms; } + bool Server::connectionRequestHandler(void *parameter, const char *ipAddress) { - auto instance = static_cast(parameter)->shared_from_this(); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Server, "Reject connection request in shutdown"); + return false; + } if (instance->py_onConnect.is_set()) { DEBUG_PRINT(Debug::Server, "CALLBACK on_connect"); @@ -438,7 +693,16 @@ void Server::connectionEventHandler(void *parameter, begin = std::chrono::steady_clock::now(); } - auto instance = static_cast(parameter)->shared_from_this(); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Server, "Ignore connection event " + + PeerConnectionEvent_toString(event) + + " in shutdown"); + return; + } char ipAddrStr[60]; IMasterConnection_getPeerAddress(connection, ipAddrStr, 60); @@ -466,7 +730,10 @@ void Server::connectionEventHandler(void *parameter, } instance->connectionMap.erase(it); } - + // remove selections + instance->scheduleTask([instance, connection]() { + instance->cleanupSelections(connection); + }); } else if (event == CS104_CON_EVENT_ACTIVATED) { // set as valid receiver auto it = instance->connectionMap.find(connection); @@ -494,16 +761,11 @@ void Server::connectionEventHandler(void *parameter, if (debug) { end = std::chrono::steady_clock::now(); - DEBUG_PRINT_CONDITION( - true, Debug::Server, - "connection_event_handler] Connection " + - PeerConnectionEvent_toString(event) + " by " + - std::string(ipAddrStr) + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Server, + "connection_event_handler] Connection " + + PeerConnectionEvent_toString(event) + " by " + + std::string(ipAddrStr) + " | TOTAL " + + TICTOC(begin, end)); } } @@ -568,18 +830,13 @@ bool Server::send(std::shared_ptr message, if (debug) { end = std::chrono::steady_clock::now(); - DEBUG_PRINT_CONDITION( - true, Debug::Server, - "send] Send " + std::string(TypeID_toString(message->getType())) + - " | COT: " + - CS101_CauseOfTransmission_toString( - message->getCauseOfTransmission()) + - " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Server, + "send] Send " + + std::string(TypeID_toString(message->getType())) + + " | COT: " + + CS101_CauseOfTransmission_toString( + message->getCauseOfTransmission()) + + " | TOTAL " + TICTOC(begin, end)); } return true; @@ -587,6 +844,9 @@ bool Server::send(std::shared_ptr message, void Server::sendActivationConfirmation(IMasterConnection connection, CS101_ASDU asdu, bool negative) { + if (!isExistingConnection(connection)) + return; + CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON); CS101_ASDU_setNegative(asdu, negative); @@ -606,6 +866,9 @@ void Server::sendActivationConfirmation(IMasterConnection connection, void Server::sendActivationTermination(IMasterConnection connection, CS101_ASDU asdu) { + if (!isExistingConnection(connection)) + return; + CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_TERMINATION); if (isGlobalCommonAddress(CS101_ASDU_getCA(asdu))) { @@ -620,9 +883,9 @@ void Server::sendActivationTermination(IMasterConnection connection, } } -void Server::sendInterrogationResponse(const CS101_CauseOfTransmission cot, - const uint_fast16_t commonAddress, - IMasterConnection connection) { +void Server::sendInventory(const CS101_CauseOfTransmission cot, + const uint_fast16_t commonAddress, + IMasterConnection connection) { if (!enabled.load() || !hasActiveConnections()) return; @@ -633,10 +896,9 @@ void Server::sendInterrogationResponse(const CS101_CauseOfTransmission cot, } bool empty = true; - const std::uint_fast64_t now = GetTimestamp_ms(); IEC60870_5_TypeID type = C_TS_TA_1; - for (const auto &station : stations) { + for (const auto &station : getStations()) { if (isGlobalCommonAddress(commonAddress) || station->getCommonAddress() == commonAddress) { @@ -656,23 +918,15 @@ void Server::sendInterrogationResponse(const CS101_CauseOfTransmission cot, // full interrogation contain all monitoring data // Update all (!) data points before transmitting them if (CS101_COT_PERIODIC == cot) { - - // disabled cyclic report - if (point->getReportInterval_ms() == 0) { - continue; - } // is ready for cyclic transmission ? - const std::uint_fast64_t next = - point->getReportedAt_ms() + point->getReportInterval_ms(); - if (now < next) { + const auto next = point->nextReportAt(); + // disabled cyclic report + if (!next.has_value() || begin < next.value()) { continue; } // value polling callback point->onBeforeAutoTransmit(); - - // updated reportedAt timestamp in case of periodic transmission - point->setReportedAt_ms(now); } else { point->onBeforeRead(); } @@ -694,9 +948,8 @@ void Server::sendInterrogationResponse(const CS101_CauseOfTransmission cot, g->second[point->getInformationObjectAddress()] = message; } } catch (const std::exception &e) { - DEBUG_PRINT(Debug::Server, - "send_interrogation_response] Invalid point message: " + - std::string(e.what())); + DEBUG_PRINT(Debug::Server, "Invalid point message for inventory: " + + std::string(e.what())); } } @@ -742,7 +995,7 @@ void Server::sendInterrogationResponse(const CS101_CauseOfTransmission cot, asdu, message.second->getInformationObject()); if (!added) { DEBUG_PRINT(Debug::Server, - "send_interrogation_response] Dropped message, " + "Dropped message for inventory, " "cannot be added to new asdu: " + std::to_string(message.second->getIOA())); } @@ -778,142 +1031,17 @@ void Server::sendInterrogationResponse(const CS101_CauseOfTransmission cot, if (CS101_COT_PERIODIC == cot) { if (!empty) { - DEBUG_PRINT_CONDITION( - true, Debug::Server, - "send_interrogation_response] Periodic | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Server, + "auto_transmit] TOTAL " + TICTOC(begin, end)); } } else { - DEBUG_PRINT_CONDITION( - true, Debug::Server, - "send_interrogation_response] Request | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Server, + "interrogation_response] TOTAL " + + TICTOC(begin, end)); } } } -/* -void Server::sendPeriodic(const uint_fast16_t commonAddress, IMasterConnection -connection) { if (!enabled.load() || !hasActiveConnections()) return; - - bool debug = DEBUG_TEST(Debug::Server); - std::chrono::steady_clock::time_point begin, end; - if (debug) { - begin = std::chrono::steady_clock::now(); - } - - bool empty = true; - const std::uint_fast64_t now = GetTimestamp_ms(); - IEC60870_5_TypeID type = C_TS_TA_1; - - for (auto station: stations) { - if (isGlobalCommonAddress(commonAddress) || station->getCommonAddress() -== commonAddress) { - - // group messages per station by type - std::map>> pointGroup; - - for (auto point: station->getPoints()) { - type = point->getType(); - - // only monitoring points - if (type > 41) - continue; - - // disabled cyclic report - if (point->getReportInterval_ms() == 0) { - continue; - } - - // is ready for cyclic transmission ? - const std::uint_fast64_t next = point->getReportedAt_ms() + -point->getReportInterval_ms(); if (now < next) { continue; - } - - // value polling callback - point->onBeforeAutoTransmit(); - - // updated reportedAt timestamp in case of periodic transmission - point->setReportedAt_ms(now); - - try { - // transmit value to client - auto message = Remote::Message::PointMessage::create(point); - message->setCauseOfTransmission(CS101_COT_PERIODIC); - //message->send(connection); - - // add message to group - auto g = pointGroup.find(type); - if (g == pointGroup.end()) { - pointGroup[type] = std::map>{{point->getInformationObjectAddress(), -message}}; } else { g->second[point->getInformationObjectAddress()] = message; - } - } catch (const std::exception &e) { - DEBUG_PRINT(Debug::Server, -"Server.sendInterrogationResponse] Invalid point message: " + -std::string(e.what())); - } - } - - // send grouped messages of current station - for (auto &group: pointGroup) { - empty = false; - bool isSequence = false; - bool isTest = false; - bool isNegative = false; - - CS101_ASDU asdu = CS101_ASDU_create(appLayerParameters, -isSequence, cot, 0, station->getCommonAddress(), isTest, isNegative); - //std::stringstream c; - //c << "GRP-" << TypeID_toString(group.first) << ": "; - for (auto &message: group.second) { - //c << -std::to_string(message.second->getInformationObjectAddress()) << ","; - CS101_ASDU_addInformationObject(asdu, -message.second->getInformationObject()); - } - //std::cout << c.str() << std::endl; - - if (connection) { - IMasterConnection_sendASDU(connection, asdu); - } else { - CS104_Slave_enqueueASDU(slave, asdu); - } - - // @todo what about ASDU destruction ?! - // high priority, ASDU gets destroyed automatically - // low priority, destroy ASDU manually - CS101_ASDU_destroy(asdu); - - // free messages - for (auto &message: group.second) { - message.second.reset(); - } - } - } - } - - if (debug) { - end = std::chrono::steady_clock::now(); - if (!empty) { - DEBUG_PRINT_CONDITION(true, Debug::Server, "Server.sendPeriodic] -TOTAL " + -std::to_string(std::chrono::duration_cast(end - -begin).count()) + u8" \xb5s"); - } - } -}*/ - std::shared_ptr Server::getValidMessage(IMasterConnection connection, CS101_ASDU asdu) { try { @@ -961,30 +1089,20 @@ Server::getValidMessage(IMasterConnection connection, CS101_ASDU asdu) { void Server::rawMessageHandler(void *parameter, IMasterConnection connection, uint_fast8_t *msg, int msgSize, bool sent) { - bool const debug = DEBUG_TEST(Debug::Server); - std::chrono::steady_clock::time_point begin, end; - if (debug) { - begin = std::chrono::steady_clock::now(); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Server, "Ignore raw message in shutdown"); + return; } - auto instance = static_cast(parameter)->shared_from_this(); if (sent) { instance->onSendRaw(msg, msgSize); } else { instance->onReceiveRaw(msg, msgSize); } - - if (debug) { - end = std::chrono::steady_clock::now(); - DEBUG_PRINT_CONDITION( - true, Debug::Server, - "raw_message_handler] TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); - } } /* @@ -995,7 +1113,14 @@ CS101_ASDU asdu, CP56Time2a newtime) { bool debug = DEBUG_TEST(Debug::Server); begin = std::chrono::steady_clock::now(); } - auto instance = static_cast(parameter)->shared_from_this(); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Server, "Reject clock sync command in shutdown"); + return false; + } char ipAddrStr[60]; IMasterConnection_getPeerAddress(connection, ipAddrStr, 60); @@ -1012,9 +1137,7 @@ CS101_ASDU asdu, CP56Time2a newtime) { bool debug = DEBUG_TEST(Debug::Server); "Server.clockSyncHandler] TIME " + CP56Time2a_toString(newtime) + " | IP " + std::string(ipAddrStr) + " | OA " + std::to_string(CS101_ASDU_getOA(asdu)) + " | CA " + -std::to_string(CS101_ASDU_getCA(asdu)) + " | TOTAL " + -std::to_string(std::chrono::duration_cast(end - -begin).count()) + u8" \xb5s"); +std::to_string(CS101_ASDU_getCA(asdu)) + " | TOTAL " + TICTOC(begin, end)); } return true; }*/ @@ -1028,7 +1151,14 @@ bool Server::interrogationHandler(void *parameter, IMasterConnection connection, begin = std::chrono::steady_clock::now(); } - auto instance = static_cast(parameter)->shared_from_this(); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Server, "Reject interrogation command in shutdown"); + return false; + } if (auto message = instance->getValidMessage(connection, asdu)) { @@ -1039,9 +1169,8 @@ bool Server::interrogationHandler(void *parameter, IMasterConnection connection, instance->sendActivationConfirmation(connection, asdu, false); // send all available information - instance->sendInterrogationResponse((CS101_CauseOfTransmission)qoi, - message->getCommonAddress(), - connection); + instance->sendInventory((CS101_CauseOfTransmission)qoi, + message->getCommonAddress(), connection); // Notify Master of command finalization instance->sendActivationTermination(connection, asdu); @@ -1058,18 +1187,14 @@ bool Server::interrogationHandler(void *parameter, IMasterConnection connection, char ipAddrStr[60]; IMasterConnection_getPeerAddress(connection, ipAddrStr, 60); - DEBUG_PRINT_CONDITION( - true, Debug::Server, - "interrogation_handler]" - " | IP " + - std::string(ipAddrStr) + " | OA " + - std::to_string(CS101_ASDU_getOA(asdu)) + " | CA " + - std::to_string(CS101_ASDU_getCA(asdu)) + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Server, + "interrogation_handler]" + " | IP " + + std::string(ipAddrStr) + " | OA " + + std::to_string(CS101_ASDU_getOA(asdu)) + + " | CA " + + std::to_string(CS101_ASDU_getCA(asdu)) + + " | TOTAL " + TICTOC(begin, end)); } return true; } @@ -1083,7 +1208,15 @@ bool Server::counterInterrogationHandler(void *parameter, begin = std::chrono::steady_clock::now(); } - auto instance = static_cast(parameter)->shared_from_this(); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Server, + "Reject counter interrogation command in shutdown"); + return false; + } if (auto message = instance->getValidMessage(connection, asdu)) { @@ -1112,18 +1245,14 @@ bool Server::counterInterrogationHandler(void *parameter, char ipAddrStr[60]; IMasterConnection_getPeerAddress(connection, ipAddrStr, 60); - DEBUG_PRINT_CONDITION( - true, Debug::Server, - "counter_interrogation_handler]" - " | IP " + - std::string(ipAddrStr) + " | OA " + - std::to_string(CS101_ASDU_getOA(asdu)) + " | CA " + - std::to_string(CS101_ASDU_getCA(asdu)) + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Server, + "counter_interrogation_handler]" + " | IP " + + std::string(ipAddrStr) + " | OA " + + std::to_string(CS101_ASDU_getOA(asdu)) + + " | CA " + + std::to_string(CS101_ASDU_getCA(asdu)) + + " | TOTAL " + TICTOC(begin, end)); } return true; } @@ -1136,11 +1265,19 @@ bool Server::readHandler(void *parameter, IMasterConnection connection, begin = std::chrono::steady_clock::now(); } - auto instance = static_cast(parameter)->shared_from_this(); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Server, "Reject read command in shutdown"); + return false; + } if (auto message = instance->getValidMessage(connection, asdu)) { UnexpectedMessageCause cause = NO_ERROR_CAUSE; + bool success = false; if (auto station = instance->getStation(message->getCommonAddress())) { if (auto point = station->getPoint(message->getIOA())) { @@ -1155,11 +1292,12 @@ bool Server::readHandler(void *parameter, IMasterConnection connection, // value polling callback point->onBeforeRead(); + success = true; try { instance->transmit(point, CS101_COT_REQUEST); } catch (const std::exception &e) { - std::cerr << "[c104.Server.read] Auto respond failed for " + std::cerr << "[c104.Server] read] Auto respond failed for " << TypeID_toString(point->getType()) << " at IOA " << point->getInformationObjectAddress() << ": " << e.what() << std::endl; @@ -1175,6 +1313,8 @@ bool Server::readHandler(void *parameter, IMasterConnection connection, cause = UNKNOWN_CA; } + instance->sendActivationConfirmation(connection, asdu, !success); + // report error cause if (cause != NO_ERROR_CAUSE) { instance->onUnexpectedMessage(connection, message, cause); @@ -1186,17 +1326,13 @@ bool Server::readHandler(void *parameter, IMasterConnection connection, char ipAddrStr[60]; IMasterConnection_getPeerAddress(connection, ipAddrStr, 60); - DEBUG_PRINT_CONDITION( - true, Debug::Server, - "read_handler] IOA " + std::to_string(ioAddress) + " | IP " + - std::string(ipAddrStr) + " | OA " + - std::to_string(CS101_ASDU_getOA(asdu)) + " | CA " + - std::to_string(CS101_ASDU_getCA(asdu)) + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Server, + "read_handler] IOA " + std::to_string(ioAddress) + + " | IP " + std::string(ipAddrStr) + " | OA " + + std::to_string(CS101_ASDU_getOA(asdu)) + + " | CA " + + std::to_string(CS101_ASDU_getCA(asdu)) + + " | TOTAL " + TICTOC(begin, end)); } return true; } @@ -1209,50 +1345,82 @@ bool Server::asduHandler(void *parameter, IMasterConnection connection, begin = std::chrono::steady_clock::now(); } - auto instance = static_cast(parameter)->shared_from_this(); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Server, "Reject asdu in shutdown"); + return false; + } // message with more than one object is not allowed for command type ids if (auto message = instance->getValidMessage(connection, asdu)) { CommandResponseState responseState = RESPONSE_STATE_FAILURE; UnexpectedMessageCause cause = NO_ERROR_CAUSE; - std::shared_ptr related_point{nullptr}; - bool requireTermination = false; // new clockSyncHandler if (message->getType() == C_CS_NA_1) { - auto csc = (ClockSynchronizationCommand)CS101_ASDU_getElement(asdu, 0); - CP56Time2a newTime = ClockSynchronizationCommand_getTime(csc); + auto info = message->getInfo(); + auto time_point = info->getRecordedAt().value_or(info->getProcessedAt()); char ipAddrStr[60]; IMasterConnection_getPeerAddress(connection, ipAddrStr, 60); // execute python callback - responseState = instance->onClockSync(std::string(ipAddrStr), newTime); + responseState = instance->onClockSync(std::string(ipAddrStr), time_point); DEBUG_PRINT_CONDITION(debug, Debug::Server, "clock_sync_handler] TIME " + - CP56Time2a_toString(newTime)); + TimePoint_toString(time_point)); } else { if (message->getType() >= C_SC_NA_1) { if (auto station = instance->getStation(message->getCommonAddress())) { if (auto point = station->getPoint(message->getIOA())) { if (point->getType() == message->getType()) { - responseState = point->onReceive(message); - - if ((responseState == RESPONSE_STATE_SUCCESS) && - !message->isSelectCommand()) { - // only in case of select-and-execute - if (point->getCommandMode() == SELECT_AND_EXECUTE_COMMAND) { - requireTermination = true; + if (message->isSelectCommand()) { + if (SELECT_AND_EXECUTE_COMMAND == point->getCommandMode()) { + responseState = instance->select(connection, message) + ? RESPONSE_STATE_SUCCESS + : RESPONSE_STATE_FAILURE; + } else { + std::cerr << "[c104.Point] Failed to select point in DIRECT " + "command mode" + << std::endl; + responseState = RESPONSE_STATE_FAILURE; } - // only in case of auto return - if (point->getRelatedInformationObjectAutoReturn()) { - related_point = station->getPoint( - point->getRelatedInformationObjectAddress()); + } else { + responseState = instance->execute(connection, message, point); + + if (responseState == RESPONSE_STATE_SUCCESS && + point->getRelatedInformationObjectAutoReturn()) { + const auto related_ioa = + point->getRelatedInformationObjectAddress(); + // send related point info in case of auto return + if (related_ioa.has_value()) { + auto related_point = station->getPoint(related_ioa.value()); + instance->scheduleTask( + [instance, related_point]() { + try { + instance->transmit(related_point, + CS101_COT_RETURN_INFO_REMOTE); + } catch (const std::exception &e) { + std::cerr + << "[c104.Server] asdu_handler] Auto transmit " + "related point failed for " + << TypeID_toString(related_point->getType()) + << " at IOA " + << related_point->getInformationObjectAddress() + << ": " << e.what() << std::endl; + } + }, + 2); + } } - } + + } // else: message->isSelectCommand() } else { cause = MISMATCHED_TYPE_ID; @@ -1270,33 +1438,16 @@ bool Server::asduHandler(void *parameter, IMasterConnection connection, // confirm activation if ((responseState != RESPONSE_STATE_NONE) && - message->requireConfirmation()) { + (message->getCauseOfTransmission() == CS101_COT_ACTIVATION || + message->getCauseOfTransmission() == CS101_COT_DEACTIVATION)) { instance->sendActivationConfirmation( connection, asdu, (responseState == RESPONSE_STATE_FAILURE)); } - // activation termination - if (requireTermination) { - instance->sendActivationTermination(connection, asdu); - } - // report error cause if (cause != NO_ERROR_CAUSE) { instance->onUnexpectedMessage(connection, message, cause); } - - // send related point info - if (related_point) { - try { - instance->transmit(related_point, CS101_COT_RETURN_INFO_REMOTE); - } catch (const std::exception &e) { - std::cerr - << "[c104.Server.asdu] Auto transmit related point failed for " - << TypeID_toString(related_point->getType()) << " at IOA " - << related_point->getInformationObjectAddress() << ": " << e.what() - << std::endl; - } - } } if (debug) { @@ -1311,11 +1462,7 @@ bool Server::asduHandler(void *parameter, IMasterConnection connection, " | IP " + std::string(ipAddrStr) + " | OA " + std::to_string(CS101_ASDU_getOA(asdu)) + " | CA " + std::to_string(CS101_ASDU_getCA(asdu)) + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + TICTOC(begin, end)); } return true; } diff --git a/src/Server.h b/src/Server.h index eb15e2c..f350b1e 100644 --- a/src/Server.h +++ b/src/Server.h @@ -41,6 +41,15 @@ #include "remote/TransportSecurity.h" #include "remote/message/IncomingMessage.h" +struct Selection { + CS101_ASDU asdu; + uint8_t oa; + uint16_t ca; + uint32_t ioa; + IMasterConnection connection; + std::chrono::steady_clock::time_point created; +}; + /** * @brief service model for IEC60870-5-104 communication as server */ @@ -55,13 +64,13 @@ class Server : public std::enable_shared_from_this { [[nodiscard]] static std::shared_ptr create( std::string bind_ip = "0.0.0.0", uint_fast16_t tcp_port = IEC_60870_5_104_DEFAULT_PORT, - uint_fast32_t tick_rate_ms = 1000, + uint_fast16_t tick_rate_ms = 100, uint_fast16_t select_timeout_ms = 100, std::uint_fast8_t max_open_connections = 0, std::shared_ptr transport_security = nullptr) { // Not using std::make_shared because the constructor is private. - return std::shared_ptr(new Server(bind_ip, tcp_port, tick_rate_ms, - max_open_connections, - std::move(transport_security))); + return std::shared_ptr( + new Server(bind_ip, tcp_port, tick_rate_ms, select_timeout_ms, + max_open_connections, std::move(transport_security))); } // DESTRUCTOR @@ -112,6 +121,8 @@ class Server : public std::enable_shared_from_this { */ bool hasStations() const; + bool isExistingConnection(IMasterConnection connection); + /** * @brief Test if Server has open connections to clients * @return information if at least one connection exists @@ -185,7 +196,8 @@ class Server : public std::enable_shared_from_this { */ void setOnClockSyncCallback(py::object &callable); - CommandResponseState onClockSync(std::string _ip, CP56Time2a time); + CommandResponseState onClockSync(std::string _ip, + std::chrono::system_clock::time_point time); /** * @brief set python callback that will be executed on unexpected incoming @@ -206,6 +218,8 @@ class Server : public std::enable_shared_from_this { */ void setOnConnectCallback(py::object &callable); + std::uint_fast16_t getTickRate_ms() const; + /** * @brief transmit a datapoint related message to a remote client * @param point datapoint that should be send via server @@ -240,9 +254,9 @@ class Server : public std::enable_shared_from_this { * @param connection send to a single client identified via internal * connection object */ - void sendInterrogationResponse( - CS101_CauseOfTransmission cot, - uint_fast16_t commonAddress = IEC60870_GLOBAL_COMMON_ADDRESS, + void sendInventory( + const CS101_CauseOfTransmission cot, + const uint_fast16_t commonAddress = IEC60870_GLOBAL_COMMON_ADDRESS, IMasterConnection connection = nullptr); /* void sendCounterInterrogationResponse(CS101_CauseOfTransmission cot, @@ -253,7 +267,12 @@ class Server : public std::enable_shared_from_this { IEC60870_GLOBAL_COMMON_ADDRESS, IMasterConnection connection = nullptr); */ + void schedulePeriodicTask(const std::function &task, int interval); + void scheduleTask(const std::function &task, int delay = 0); + private: + void scheduleDataPointTimer(); + /** * @brief Create a new remote connection handler instance that acts as a * server @@ -266,18 +285,40 @@ class Server : public std::enable_shared_from_this { * @param transport_security communication encryption instance reference */ Server(const std::string &bind_ip, std::uint_fast16_t tcp_port, - std::uint_fast32_t tick_rate_ms, + std::uint_fast16_t tick_rate_ms, std::uint_fast16_t select_timeout_ms, std::uint_fast8_t max_open_connections, std::shared_ptr transport_security); + void cleanupSelections(); + + void cleanupSelections(IMasterConnection connection); + + void cleanupSelection(uint16_t ca, uint32_t ioa); + + bool select(IMasterConnection connection, + std::shared_ptr message); + + void unselect(const Selection &selection); + + CommandResponseState + execute(IMasterConnection connection, + std::shared_ptr message, + std::shared_ptr point); + /// @brief IP address of remote server - std::string ip{}; + const std::string ip{}; ///< @brief Port of remote server - std::uint_fast16_t port = 0; + const std::uint_fast16_t port = 0; + + /// @brief minimum interval between to periodic broadcasts in milliseconds + const std::uint_fast16_t tickRate_ms{100}; + + /// @brief selection init timestamp, to test against timeout + const std::chrono::milliseconds selectTimeout_ms{100}; /// @brief tls handler - std::shared_ptr security{nullptr}; + const std::shared_ptr security{nullptr}; /// @brief vector of stations accessible via this connection Object::StationVector stations{}; @@ -291,18 +332,24 @@ class Server : public std::enable_shared_from_this { /// @brief state that defines if server thread should be running std::atomic_bool enabled{false}; - /// @brief minimum interval between to periodic broadcasts in milliseconds - std::atomic_uint_fast32_t tickRate_ms{1000}; + /// @brief server thread state + std::atomic_bool running{false}; /// @brief parameters of current server intance CS101_AppLayerParameters appLayerParameters; - /// @brief MUTEX Lock to access connection_mutex + /// @brief MUTEX Lock to access connectionMap mutable Module::GilAwareMutex connection_mutex{"Server::connection_mutex"}; /// @brief map of all connections to store connection state std::map connectionMap{}; + /// @brief MUTEX Lock to access selectionVEcotr + mutable Module::GilAwareMutex selection_mutex{"Server::selection_mutex"}; + + /// @brief vector of all selections + std::vector selectionVector{}; + /// @brief number of active connections std::atomic_uint_fast8_t activeConnections{0}; @@ -312,8 +359,7 @@ class Server : public std::enable_shared_from_this { /// @brief maximum number of connections (0-255), 0 = no limit std::atomic_uint_fast8_t maxOpenConnections{0}; - /// @brief server thread state - std::atomic_bool running{false}; + std::priority_queue tasks; /// @brief server thread to execute periodic transmission std::thread *runThread = nullptr; @@ -356,6 +402,8 @@ class Server : public std::enable_shared_from_this { // void thread_callback(); public: + std::optional getSelector(uint16_t ca, uint32_t ioa); + /** * @brief Callback to accept or decline incoming client connections * @param parameter reference to custom bound connection data @@ -462,6 +510,25 @@ class Server : public std::enable_shared_from_this { */ static bool asduHandler(void *parameter, IMasterConnection connection, CS101_ASDU asdu); + + std::string toString() const { + size_t lencon = 0; + { + std::scoped_lock const lock(connection_mutex); + lencon = connectionMap.size(); + } + size_t lenst = 0; + { + std::scoped_lock const lock(station_mutex); + lenst = stations.size(); + } + std::ostringstream oss; + oss << "<104.Server ip=" << ip << ", port=" << std::to_string(port) + << ", #clients=" << std::to_string(lencon) + << ", #stations=" << std::to_string(lenst) << " at " << std::hex + << std::showbase << reinterpret_cast(this) << ">"; + return oss.str(); + }; }; #endif // C104_SERVER_H diff --git a/src/enums.cpp b/src/enums.cpp index 721c706..a77b912 100644 --- a/src/enums.cpp +++ b/src/enums.cpp @@ -1,5 +1,5 @@ /** - * Copyright 2020-2023 Fraunhofer Institute for Applied Information Technology + * Copyright 2020-2024 Fraunhofer Institute for Applied Information Technology * FIT * * This file is part of iec104-python. @@ -34,7 +34,7 @@ #include "enums.h" -std::string Debug_toString(Debug mode) { +std::string Debug_toString(const Debug &mode) { if (is_none(mode)) { return "Debug set: {}, is_none: True"; } @@ -63,7 +63,7 @@ std::string Debug_toString(Debug mode) { " }, is_none: False"; } -std::string Debug_toFlagString(Debug mode) { +std::string Debug_toFlagString(const Debug &mode) { if (is_none(mode)) { return "None"; } @@ -89,15 +89,15 @@ std::string Debug_toFlagString(Debug mode) { [](const std::string &a, const std::string &b) { return a + " | " + b; }); } -std::string Quality_toString(Quality quality) { +std::string Quality_toString(const Quality &quality) { if (is_none(quality)) { return "Quality set: {}, is_good: True"; } std::vector sv{}; if (test(quality, Quality::Overflow)) sv.emplace_back("Overflow"); - if (test(quality, Quality::Reserved)) - sv.emplace_back("Reserved"); + // if (test(quality, Quality::Reserved)) + // sv.emplace_back("Reserved"); if (test(quality, Quality::ElapsedTimeInvalid)) sv.emplace_back("ElapsedTimeInvalid"); if (test(quality, Quality::Blocked)) @@ -116,7 +116,133 @@ std::string Quality_toString(Quality quality) { " }, is_good: False"; } -std::string ConnectionState_toString(const ConnectionState state) { +std::string BinaryCounterQuality_toString(const BinaryCounterQuality &quality) { + if (is_none(quality)) { + return "BinaryCounterQuality set: {}, is_good: True"; + } + std::vector sv{}; + if (test(quality, BinaryCounterQuality::Adjusted)) + sv.emplace_back("Adjusted"); + if (test(quality, BinaryCounterQuality::Carry)) + sv.emplace_back("Carry"); + if (test(quality, BinaryCounterQuality::Invalid)) + sv.emplace_back("Invalid"); + return "BinaryCounterQuality set: { " + + std::accumulate(std::next(sv.begin()), sv.end(), sv[0], + [](const std::string &a, const std::string &b) { + return a + " | " + b; + }) + + " }, is_good: False"; +} + +std::string StartEvents_toString(const StartEvents &events) { + if (is_none(events)) { + return "StartEvent set: {}"; + } + std::vector sv{}; + if (test(events, StartEvents::General)) + sv.emplace_back("General"); + if (test(events, StartEvents::PhaseL1)) + sv.emplace_back("PhaseL1"); + if (test(events, StartEvents::PhaseL2)) + sv.emplace_back("PhaseL2"); + if (test(events, StartEvents::PhaseL3)) + sv.emplace_back("PhaseL3"); + if (test(events, StartEvents::InEarthCurrent)) + sv.emplace_back("InEarthCurrent"); + if (test(events, StartEvents::ReverseDirection)) + sv.emplace_back("ReverseDirection"); + return "StartEvents set: { " + + std::accumulate(std::next(sv.begin()), sv.end(), sv[0], + [](const std::string &a, const std::string &b) { + return a + " | " + b; + }) + + " }"; +} + +std::string OutputCircuits_toString(const OutputCircuits &infos) { + if (is_none(infos)) { + return "OutputCircuit set: {}"; + } + std::vector sv{}; + if (test(infos, OutputCircuits::General)) + sv.emplace_back("General"); + if (test(infos, OutputCircuits::PhaseL1)) + sv.emplace_back("PhaseL1"); + if (test(infos, OutputCircuits::PhaseL2)) + sv.emplace_back("PhaseL2"); + if (test(infos, OutputCircuits::PhaseL3)) + sv.emplace_back("PhaseL3"); + return "OutputCircuit set: { " + + std::accumulate(std::next(sv.begin()), sv.end(), sv[0], + [](const std::string &a, const std::string &b) { + return a + " | " + b; + }) + + " }"; +} + +std::string FieldSet16_toString(const FieldSet16 &infos) { + if (is_none(infos)) { + return "Field set: {}"; + } + std::vector sv{}; + if (test(infos, FieldSet16::I0)) + sv.emplace_back("I0"); + if (test(infos, FieldSet16::I1)) + sv.emplace_back("I1"); + if (test(infos, FieldSet16::I2)) + sv.emplace_back("I2"); + if (test(infos, FieldSet16::I3)) + sv.emplace_back("I3"); + if (test(infos, FieldSet16::I4)) + sv.emplace_back("I4"); + if (test(infos, FieldSet16::I5)) + sv.emplace_back("I5"); + if (test(infos, FieldSet16::I6)) + sv.emplace_back("I6"); + if (test(infos, FieldSet16::I7)) + sv.emplace_back("I7"); + if (test(infos, FieldSet16::I8)) + sv.emplace_back("I8"); + if (test(infos, FieldSet16::I9)) + sv.emplace_back("I9"); + if (test(infos, FieldSet16::I10)) + sv.emplace_back("I10"); + if (test(infos, FieldSet16::I11)) + sv.emplace_back("I11"); + if (test(infos, FieldSet16::I12)) + sv.emplace_back("I12"); + if (test(infos, FieldSet16::I13)) + sv.emplace_back("I13"); + if (test(infos, FieldSet16::I14)) + sv.emplace_back("I14"); + if (test(infos, FieldSet16::I15)) + sv.emplace_back("I15"); + return "PackedSingle set: { " + + std::accumulate(std::next(sv.begin()), sv.end(), sv[0], + [](const std::string &a, const std::string &b) { + return a + " | " + b; + }) + + " }"; +} + +std::string +QualifierOfCommand_toString(const CS101_QualifierOfCommand &qualifier) { + switch (qualifier) { + case CS101_QualifierOfCommand::NONE: + return "NONE"; + case CS101_QualifierOfCommand::SHORT_PULSE: + return "SHORT_PULSE"; + case CS101_QualifierOfCommand::LONG_PULSE: + return "LONG_PULSE"; + case CS101_QualifierOfCommand::PERSISTENT: + return "PERSISTENT"; + default: + return "UNKNOWN"; + } +} + +std::string ConnectionState_toString(const ConnectionState &state) { switch (state) { case CLOSED: return "CLOSED"; @@ -126,10 +252,6 @@ std::string ConnectionState_toString(const ConnectionState state) { return "CLOSED_AWAIT_RECONNECT"; case OPEN_MUTED: return "OPEN_MUTED"; - case OPEN_AWAIT_INTERROGATION: - return "OPEN_AWAIT_INTERROGATION"; - case OPEN_AWAIT_CLOCK_SYNC: - return "OPEN_AWAIT_CLOCK_SYNC"; case OPEN: return "OPEN"; case OPEN_AWAIT_CLOSED: @@ -139,7 +261,7 @@ std::string ConnectionState_toString(const ConnectionState state) { } } -std::string ConnectionEvent_toString(const CS104_ConnectionEvent event) { +std::string ConnectionEvent_toString(const CS104_ConnectionEvent &event) { switch (event) { case CS104_CONNECTION_OPENED: return "OPENED"; @@ -149,13 +271,15 @@ std::string ConnectionEvent_toString(const CS104_ConnectionEvent event) { return "ACTIVATED"; case CS104_CONNECTION_STOPDT_CON_RECEIVED: return "DEACTIVATED"; + case CS104_CONNECTION_FAILED: + return "FAILED"; default: return "UNKNOWN"; } } std::string -PeerConnectionEvent_toString(const CS104_PeerConnectionEvent event) { +PeerConnectionEvent_toString(const CS104_PeerConnectionEvent &event) { switch (event) { case CS104_CON_EVENT_CONNECTION_OPENED: return "OPENED"; @@ -169,3 +293,149 @@ PeerConnectionEvent_toString(const CS104_PeerConnectionEvent event) { return "UNKNOWN"; } } + +std::string DoublePointValue_toString(const DoublePointValue &value) { + switch (value) { + case IEC60870_DOUBLE_POINT_INDETERMINATE: + return "INDETERMINATE"; + case IEC60870_DOUBLE_POINT_OFF: + return "OFF"; + case IEC60870_DOUBLE_POINT_ON: + return "ON"; + case IEC60870_DOUBLE_POINT_INTERMEDIATE: + return "INTERMEDIATE"; + default: + return "UNKNOWN"; + } +} + +std::string StepCommandValue_toString(const StepCommandValue &value) { + switch (value) { + case IEC60870_STEP_INVALID_0: + return "INVALID_0"; + case IEC60870_STEP_LOWER: + return "LOWER"; + case IEC60870_STEP_HIGHER: + return "HIGHER"; + case IEC60870_STEP_INVALID_3: + return "INVALID_3"; + default: + return "UNKNOWN"; + } +} + +std::string EventState_toString(const EventState &state) { + switch (state) { + case IEC60870_EVENTSTATE_INDETERMINATE_0: + return "INDETERMINATE_0"; + case IEC60870_EVENTSTATE_OFF: + return "OFF"; + case IEC60870_EVENTSTATE_ON: + return "ON"; + case IEC60870_EVENTSTATE_INDETERMINATE_3: + return "INDETERMINATE_3"; + default: + return "UNKNOWN"; + } +} + +std::string +CommandTransmissionMode_toString(const CommandTransmissionMode &mode) { + switch (mode) { + case DIRECT_COMMAND: + return "DIRECT"; + case SELECT_AND_EXECUTE_COMMAND: + return "SELECT_AND_EXECUTE"; + default: + return "UNKNOWN"; + } +} + +std::string +UnexpectedMessageCause_toString(const UnexpectedMessageCause &cause) { + switch (cause) { + case NO_ERROR_CAUSE: + return "NO_ERROR_CAUSE"; + case UNKNOWN_TYPE_ID: + return "UNKNOWN_TYPE_ID"; + case UNKNOWN_COT: + return "UNKNOWN_COT"; + case UNKNOWN_CA: + return "UNKNOWN_CA"; + case UNKNOWN_IOA: + return "UNKNOWN_IOA"; + case INVALID_COT: + return "INVALID_COT"; + case INVALID_TYPE_ID: + return "INVALID_TYPE_ID"; + case MISMATCHED_TYPE_ID: + return "MISMATCHED_TYPE_ID"; + case UNIMPLEMENTED_GROUP: + return "UNIMPLEMENTED_GROUP"; + default: + return "UNKNOWN"; + } +} + +std::string +CauseOfInitialization_toString(const CS101_CauseOfInitialization &cause) { + switch (cause) { + case CS101_CauseOfInitialization::LOCAL_POWER_ON: + return "LOCAL_POWER_ON"; + case CS101_CauseOfInitialization::LOCAL_MANUAL_RESET: + return "LOCAL_MANUAL_RESET"; + case CS101_CauseOfInitialization::REMOTE_RESET: + return "REMOTE_RESET"; + default: + return "UNKNOWN"; + } +} + +std::string ConnectionInit_toString(const ConnectionInit &init) { + switch (init) { + case ConnectionInit::INIT_ALL: + return "INIT_ALL"; + case ConnectionInit::INIT_INTERROGATION: + return "INIT_INTERROGATION"; + case ConnectionInit::INIT_CLOCK_SYNC: + return "INIT_CLOCK_SYNC"; + case ConnectionInit::INIT_MUTED: + return "INIT_MUTED"; + case ConnectionInit::INIT_NONE: + return "INIT_NONE"; + default: + return "UNKNOWN"; + } +} + +std::string CommandResponseState_toString(const CommandResponseState &state) { + switch (state) { + case CommandResponseState::RESPONSE_STATE_FAILURE: + return "RESPONSE_STATE_FAILURE"; + case CommandResponseState::RESPONSE_STATE_SUCCESS: + return "RESPONSE_STATE_SUCCESS"; + case CommandResponseState::RESPONSE_STATE_NONE: + return "RESPONSE_STATE_NONE"; + default: + return "UNKNOWN"; + } +} + +std::string CommandProcessState_toString(const CommandProcessState &state) { + switch (state) { + case CommandProcessState::COMMAND_FAILURE: + return "COMMAND_FAILURE"; + case CommandProcessState::COMMAND_SUCCESS: + return "COMMAND_SUCCESS"; + case CommandProcessState::COMMAND_AWAIT_CON: + return "COMMAND_AWAIT_CON"; + case CommandProcessState::COMMAND_AWAIT_TERM: + return "COMMAND_AWAIT_TERM"; + case CommandProcessState::COMMAND_AWAIT_CON_TERM: + return "COMMAND_AWAIT_CON_TERM"; + case CommandProcessState::COMMAND_AWAIT_REQUEST: + return "COMMAND_AWAIT_REQUEST"; + default: + return "UNKNOWN"; + } +} diff --git a/src/enums.h b/src/enums.h index 450c87a..bfa55dc 100644 --- a/src/enums.h +++ b/src/enums.h @@ -32,7 +32,7 @@ #ifndef C104_ENUMS_H #define C104_ENUMS_H -#include +#include #include #include @@ -203,9 +203,22 @@ enum class CS101_QualifierOfCommand { NONE = IEC60870_QOC_NO_ADDITIONAL_DEFINITION, SHORT_PULSE = IEC60870_QOC_SHORT_PULSE_DURATION, LONG_PULSE = IEC60870_QOC_LONG_PULSE_DURATION, - CONTINUOUS = IEC60870_QOC_PERSISTANT_OUTPUT + PERSISTENT = IEC60870_QOC_PERSISTANT_OUTPUT }; +std::string +QualifierOfCommand_toString(const CS101_QualifierOfCommand &qualifier); + +enum class CS101_CauseOfInitialization { + LOCAL_POWER_ON = IEC60870_COI_LOCAL_SWITCH_ON, + LOCAL_MANUAL_RESET = IEC60870_COI_LOCAL_MANUAL_RESET, + REMOTE_RESET = IEC60870_COI_REMOTE_RESET + // <3..31> := Reserved for future norm definitions + // <32..127> := Reserved for user definitions (private range) +}; +std::string +CauseOfInitialization_toString(const CS101_CauseOfInitialization &cause); + enum UnexpectedMessageCause { NO_ERROR_CAUSE, UNKNOWN_TYPE_ID, @@ -217,6 +230,7 @@ enum UnexpectedMessageCause { MISMATCHED_TYPE_ID, UNIMPLEMENTED_GROUP }; +std::string UnexpectedMessageCause_toString(const UnexpectedMessageCause &mode); enum class Debug : uint8_t { None = 0, @@ -231,35 +245,74 @@ enum class Debug : uint8_t { All = 0xFF }; constexpr bool enum_bitmask(Debug &&); -std::string Debug_toString(Debug mode); -std::string Debug_toFlagString(Debug mode); +std::string Debug_toString(const Debug &mode); +std::string Debug_toFlagString(const Debug &mode); enum class Quality { None = 0, - Overflow = IEC60870_QUALITY_OVERFLOW, - Reserved = IEC60870_QUALITY_RESERVED, - ElapsedTimeInvalid = IEC60870_QUALITY_ELAPSED_TIME_INVALID, + Overflow = IEC60870_QUALITY_OVERFLOW, // only in sp, dp + ElapsedTimeInvalid = + IEC60870_QUALITY_ELAPSED_TIME_INVALID, // only equipment protection Blocked = IEC60870_QUALITY_BLOCKED, Substituted = IEC60870_QUALITY_SUBSTITUTED, NonTopical = IEC60870_QUALITY_NON_TOPICAL, Invalid = IEC60870_QUALITY_INVALID }; constexpr bool enum_bitmask(Quality &&); -std::string Quality_toString(Quality quality); - -enum InformationType { - SINGLE, - DOUBLE, - STEP, - BITS, - NORMALIZED, - SCALED, - SHORT, - INTEGRATED, - NORMALIZED_PARAMETER, - SCALED_PARAMETER, - SHORT_PARAMETER +std::string Quality_toString(const Quality &quality); + +enum class BinaryCounterQuality { + None = 0, + Adjusted = 0x20, + Carry = 0x40, + Invalid = 0x80 +}; +constexpr bool enum_bitmask(BinaryCounterQuality &&); +std::string BinaryCounterQuality_toString(const BinaryCounterQuality &quality); + +enum class StartEvents { + None = 0, + General = IEC60870_START_EVENT_GS, + PhaseL1 = IEC60870_START_EVENT_SL1, + PhaseL2 = IEC60870_START_EVENT_SL2, + PhaseL3 = IEC60870_START_EVENT_SL3, + InEarthCurrent = IEC60870_START_EVENT_SIE, + ReverseDirection = IEC60870_START_EVENT_SRD, }; +constexpr bool enum_bitmask(StartEvents &&); +std::string StartEvents_toString(const StartEvents &events); + +enum class OutputCircuits { + None = 0, + General = IEC60870_OUTPUT_CI_GC, + PhaseL1 = IEC60870_OUTPUT_CI_CL1, + PhaseL2 = IEC60870_OUTPUT_CI_CL2, + PhaseL3 = IEC60870_OUTPUT_CI_CL3 +}; +constexpr bool enum_bitmask(OutputCircuits &&); +std::string OutputCircuits_toString(const OutputCircuits &infos); + +enum class FieldSet16 { + None = 0x0000, + I0 = 0x0001, + I1 = 0x0002, + I2 = 0x0004, + I3 = 0x0008, + I4 = 0x0010, + I5 = 0x0020, + I6 = 0x0040, + I7 = 0x0080, + I8 = 0x0100, + I9 = 0x0200, + I10 = 0x0400, + I11 = 0x0800, + I12 = 0x1000, + I13 = 0x2000, + I14 = 0x4000, + I15 = 0x8000 +}; +constexpr bool enum_bitmask(FieldSet16 &&); +std::string FieldSet16_toString(const FieldSet16 &infos); /** * @brief link states for connection state machine behaviour @@ -269,17 +322,21 @@ enum ConnectionState { CLOSED_AWAIT_OPEN, CLOSED_AWAIT_RECONNECT, OPEN_MUTED, - OPEN_AWAIT_INTERROGATION, - OPEN_AWAIT_CLOCK_SYNC, OPEN, OPEN_AWAIT_CLOSED }; +std::string ConnectionState_toString(const ConnectionState &state); + +std::string ConnectionEvent_toString(const CS104_ConnectionEvent &event); + +std::string +PeerConnectionEvent_toString(const CS104_PeerConnectionEvent &event); -std::string ConnectionState_toString(ConnectionState state); +std::string DoublePointValue_toString(const DoublePointValue &value); -std::string ConnectionEvent_toString(CS104_ConnectionEvent event); +std::string StepCommandValue_toString(const StepCommandValue &value); -std::string PeerConnectionEvent_toString(CS104_PeerConnectionEvent event); +std::string EventState_toString(const EventState &state); /** * @brief initial commands send to a connection that starts data transmission @@ -288,8 +345,10 @@ enum ConnectionInit { INIT_ALL, INIT_INTERROGATION, INIT_CLOCK_SYNC, + INIT_MUTED, INIT_NONE }; +std::string ConnectionInit_toString(const ConnectionInit &init); /** * @brief command response states, control servers response behaviour with @@ -300,6 +359,7 @@ enum CommandResponseState { RESPONSE_STATE_SUCCESS, RESPONSE_STATE_NONE }; +std::string CommandResponseState_toString(const CommandResponseState &state); /** * @brief command processing progress states @@ -312,6 +372,7 @@ enum CommandProcessState { COMMAND_AWAIT_CON_TERM, COMMAND_AWAIT_REQUEST }; +std::string CommandProcessState_toString(const CommandProcessState &state); /** * @brief command transmission modes (execute directly or select before execute @@ -321,5 +382,7 @@ enum CommandTransmissionMode { DIRECT_COMMAND, SELECT_AND_EXECUTE_COMMAND, }; +std::string +CommandTransmissionMode_toString(const CommandTransmissionMode &mode); #endif // C104_ENUMS_H diff --git a/src/main_client.cpp b/src/main_client.cpp index b6151b5..39939ab 100644 --- a/src/main_client.cpp +++ b/src/main_client.cpp @@ -52,33 +52,30 @@ void cl_dump(std::shared_ptr my_client, std::cout << " |--+ STATION " << std::to_string(st_iter->getCommonAddress()) << " has " << std::to_string(st_pt_count) << " points" << std::endl; - std::cout << " | TYPE | IOA | " - "VALUE | UPDATED AT | REPORTED AT " - " | QUALITY " + std::cout << " | TYPE | IOA | VALUE | " + "PROCESSED AT | RECORDED AT | QUALITY " + << std::endl; + std::cout << " " + "|-----------|---------|---------------|---------------|--" + "-------------|-------------------" << std::endl; - std::cout - << " " - "|----------------|------------|----------------------|---------" - "-------------|----------------------|-------------------" - << std::endl; for (auto &pt_iter : st_iter->getPoints()) { std::cout << " | " << TypeID_toString(pt_iter->getType()) - << " | " << std::setw(10) + << " | " << std::setw(7) << std::to_string(pt_iter->getInformationObjectAddress()) - << " | " << std::setw(20) - << std::to_string(pt_iter->getValue()) << " | " - << std::setw(20) - << std::to_string(pt_iter->getUpdatedAt_ms()) << " | " - << std::setw(20) - << std::to_string(pt_iter->getReportedAt_ms()) << " | " - << Quality_toString(pt_iter->getQuality()) << std::endl; + << " | " << std::setw(13) + << InfoValue_toString(pt_iter->getValue()) << " | " + << std::setw(13) + << TimePoint_toString(pt_iter->getProcessedAt()) << " | " + << std::setw(13) + << TimePoint_toString(pt_iter->getRecordedAt()) << " | " + << InfoQuality_toString(pt_iter->getQuality()) << std::endl; } - std::cout - << " " - "|----------------|------------|----------------------|---------" - "-------------|----------------------|-------------------" - << std::endl; + std::cout << " " + "|-----------|---------|---------------|---------------|--" + "-------------|-------------------" + << std::endl; } } } @@ -103,7 +100,7 @@ int main(int argc, char *argv[]) { } ROOT = ROOT + "/tests/"; - setDebug(Debug::Client | Debug::Connection); + setDebug(Debug::Client | Debug::Connection | Debug::Point | Debug::Callback); std::cout << "CL] DEBUG MODE: " << Debug_toString(getDebug()) << std::endl; std::shared_ptr tlsconf{nullptr}; @@ -117,10 +114,11 @@ int main(int argc, char *argv[]) { tlsconf->addAllowedRemoteCertificate(ROOT + "certs/server1.crt"); } - auto my_client = Client::create(1000, 5000, tlsconf); + auto my_client = Client::create(100, 100, tlsconf); my_client->setOriginatorAddress(123); - auto cl_connection_1 = my_client->addConnection("127.0.0.1", 19998); + auto cl_connection_1 = + my_client->addConnection("127.0.0.1", 19998, INIT_NONE); auto cl_station_1 = cl_connection_1->addStation(47); auto cl_step_command = cl_station_1->addPoint(32, C_RC_TA_1); @@ -132,6 +130,14 @@ int main(int argc, char *argv[]) { * connect loop */ + // while(true) { + // std::cout << "start" << std::endl; + // my_client->start(); + // std::cout << "stop" << std::endl; + // //std::this_thread::sleep_for(1s); + // cl_connection_1->disconnect(); + // my_client->stop(); + // } my_client->start(); while (!cl_connection_1->isOpen()) { @@ -163,17 +169,21 @@ int main(int argc, char *argv[]) { */ auto cl_single_command = cl_station_2->addPoint(16, C_SC_NA_1); - cl_single_command->setValue(0); + cl_single_command->setValue(false); if (cl_single_command->transmit(CS101_COT_ACTIVATION)) { std::cout << "CL] transmit: Single command OFF successful" << std::endl; } else { - std::cout << "CL] transmit: Single command OFF failed" << std::endl; + std::cout << "CL] transmit: Single command OFF failed (not selected)" + << std::endl; } std::this_thread::sleep_for(1s); cl_single_command->setCommandMode(SELECT_AND_EXECUTE_COMMAND); + cl_single_command->setInfo( + Object::SingleCmd::create(false, CS101_QualifierOfCommand::SHORT_PULSE)); if (cl_single_command->transmit(CS101_COT_ACTIVATION)) { - std::cout << "CL] transmit: Single command OFF successful" << std::endl; + std::cout << "CL] transmit: Single command OFF successful (selected)" + << std::endl; } else { std::cout << "CL] transmit: Single command OFF failed" << std::endl; } @@ -184,9 +194,11 @@ int main(int argc, char *argv[]) { */ auto cl_double_command = cl_station_2->addPoint(22, C_DC_TA_1); + cl_double_command->setInfo(Object::DoubleCmd::create( + IEC60870_DOUBLE_POINT_ON, CS101_QualifierOfCommand::NONE, + std::chrono::system_clock::time_point( + std::chrono::milliseconds(1711111111111)))); - cl_double_command->setValueEx(IEC60870_DOUBLE_POINT_ON, Quality::None, - 1711111111111); if (cl_double_command->transmit(CS101_COT_ACTIVATION)) { std::cout << "CL] transmit: Double command ON successful" << std::endl; } else { @@ -209,7 +221,7 @@ int main(int argc, char *argv[]) { auto cl_setpoint_1 = cl_station_2->addPoint(12, C_SE_NC_1); auto cl_setpoint_2 = cl_station_2->addPoint(13, C_SE_NC_1); - cl_setpoint_1->setValue(13.45); + cl_setpoint_1->setInfo(Object::ShortCmd::create(13.45)); if (cl_setpoint_1->transmit(CS101_COT_ACTIVATION)) { std::cout << "CL] transmit: Setpoint1 command successful" << std::endl; } else { @@ -217,7 +229,7 @@ int main(int argc, char *argv[]) { } std::this_thread::sleep_for(1s); - cl_setpoint_2->setValue(13.45); + cl_setpoint_2->setInfo(Object::ShortCmd::create(13.45)); if (cl_setpoint_2->transmit(CS101_COT_ACTIVATION)) { std::cout << "CL] transmit: Setpoint2 command successful" << std::endl; } else { diff --git a/src/main_server.cpp b/src/main_server.cpp index 59cdf1b..bf0432d 100644 --- a/src/main_server.cpp +++ b/src/main_server.cpp @@ -53,7 +53,7 @@ int main(int argc, char *argv[]) { } ROOT = ROOT + "/tests/"; - setDebug(Debug::Server | Debug::Callback); + setDebug(Debug::Server | Debug::Point | Debug::Callback); std::cout << "SV] DEBUG MODE: " << Debug_toString(getDebug()) << std::endl; std::shared_ptr tlsconf{nullptr}; @@ -67,12 +67,12 @@ int main(int argc, char *argv[]) { tlsconf->addAllowedRemoteCertificate(ROOT + "certs/client1.crt"); } - auto my_server = Server::create("127.0.0.1", 19998, 1000, 0, tlsconf); + auto my_server = Server::create("127.0.0.1", 19998, 100, 100, 0, tlsconf); auto sv_station_2 = my_server->addStation(47); auto sv_measurement_point = sv_station_2->addPoint(11, M_ME_TF_1, 1000); - sv_measurement_point->setValue(12.34); + sv_measurement_point->setValue((float)12.34); auto sv_control_setpoint = sv_station_2->addPoint( 12, C_SE_NC_1, 0, sv_measurement_point->getInformationObjectAddress(), @@ -95,75 +95,81 @@ int main(int argc, char *argv[]) { 22, C_DC_TA_1, 0, sv_double_point->getInformationObjectAddress(), true); auto sv_step_point = sv_station_2->addPoint(31, M_ST_TB_1, 2000); - sv_step_point->setValue(1); + sv_step_point->setValue(LimitedInt7(1)); auto sv_step_command = sv_station_2->addPoint( 32, C_RC_TA_1, 0, sv_step_point->getInformationObjectAddress(), true); - auto locals = py::dict("sv_control_setpoint"_a = sv_control_setpoint, - "sv_control_setpoint_2"_a = sv_control_setpoint_2, - "sv_single_command"_a = sv_single_command, - "sv_double_command"_a = sv_double_command, - "sv_step_point"_a = sv_step_point, - "sv_step_command"_a = sv_step_command); + auto locals = py::dict( + "my_server"_a = my_server, "sv_control_setpoint"_a = sv_control_setpoint, + "sv_control_setpoint_2"_a = sv_control_setpoint_2, + "sv_single_command"_a = sv_single_command, + "sv_double_command"_a = sv_double_command, + "sv_step_point"_a = sv_step_point, "sv_step_command"_a = sv_step_command); try { py::exec(R"( import c104 +import datetime -def sv_pt_on_setpoint_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - import c104 - print("SV] {0} SETPOINT COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality)) - - if point.quality.is_good(): - if point.related_io_address: - print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) - related_point = point.station.get_point(point.related_io_address) - if related_point: - print("SV] -> RELATED POINT VALUE UPDATE") - related_point.value = point.value - else: - print("SV] -> RELATED POINT NOT FOUND!") - return c104.ResponseState.SUCCESS +def sv_on_clock_sync(server: c104.Server, ip: str, date_time: datetime.datetime) -> c104.ResponseState: + print("SV] ->@| Time {0} from {1} | SERVER {2}:{3}".format(date_time, ip, server.ip, server.port)) + return c104.ResponseState.SUCCESS - return c104.ResponseState.FAILURE +def sv_on_receive_raw(server: c104.Server, data: bytes) -> None: + import c104 + print("SV] -->| {1} [{0}] | SERVER {2}:{3}".format(data.hex(), c104.explain_bytes_dict(apdu=data), server.ip, server.port)) -def sv_pt_on_single_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: +def sv_on_send_raw(server: c104.Server, data: bytes) -> None: import c104 - print("SV] {0} SINGLE COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}, command_qualifier: {6}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality, message.command_qualifier)) + print("SV] <--| {1} [{0}] | SERVER {2}:{3}".format(data.hex(), c104.explain_bytes_dict(apdu=data), server.ip, server.port)) - if point.quality.is_good(): - if message.is_select_command: - print("SV] -> SELECTED BY: {}".format(point.selected_by)) +def sv_pt_on_setpoint_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + import c104 + print("SV] {0} SETPOINT COMMAND on IOA: {1}, cot: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message.cot, previous_info, point.info)) + + if point.related_io_address: + print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) + related_point = point.station.get_point(point.related_io_address) + if related_point: + print("SV] -> RELATED POINT VALUE UPDATE") + related_point.value = point.value else: - print("SV] -> EXECUTED BY {}, NEW SELECTED BY={}".format(message.originator_address, point.selected_by)) - return c104.ResponseState.SUCCESS - - return c104.ResponseState.FAILURE + print("SV] -> RELATED POINT NOT FOUND!") + return c104.ResponseState.SUCCESS -sv_global_step_point_value = 0 -def sv_pt_on_double_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: +def sv_pt_on_single_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: import c104 - print("SV] {0} DOUBLE COMMAND on IOA: {1}, new: {2}, timestamp: {3}, prev: {4}, cot: {5}, quality: {6}, command_qualifier: {7}".format(point.type, point.io_address, point.value, point.updated_at_ms, previous_state, message.cot, point.quality, message.command_qualifier)) - - if point.quality.is_good(): - if point.related_io_address: - print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) - related_point = point.station.get_point(point.related_io_address) - if related_point: - print("SV] -> RELATED POINT VALUE UPDATE") - related_point.value = point.value - else: - print("SV] -> RELATED POINT NOT FOUND!") - return c104.ResponseState.SUCCESS + print("SV] {0} SINGLE COMMAND on IOA: {1}, cot: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message.cot, previous_info, point.info)) - return c104.ResponseState.FAILURE + if message.is_select_command: + print("SV] -> SELECTED BY: {}".format(point.selected_by)) + else: + print("SV] -> EXECUTED BY {}, NEW SELECTED BY={}".format(message.originator_address, point.selected_by)) + return c104.ResponseState.SUCCESS + + +sv_global_step_point_value = c104.Int7(0) + +def sv_pt_on_double_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + import c104 + print("SV] {0} DOUBLE COMMAND on IOA: {1}, cot: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message.cot, previous_info, point.info)) + + if point.related_io_address: + print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) + related_point = point.station.get_point(point.related_io_address) + if related_point: + print("SV] -> RELATED POINT VALUE UPDATE") + related_point.value = point.value + else: + print("SV] -> RELATED POINT NOT FOUND!") + return c104.ResponseState.SUCCESS -def sv_pt_on_step_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: +def sv_pt_on_step_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: import c104 global sv_global_step_point_value - print("SV] {0} STEP COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}, command_qualifier: {6}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality, message.command_qualifier)) + print("SV] {0} STEP COMMAND on IOA: {1}, cot: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message.cot, previous_info, point.info)) if point.value == c104.Step.LOWER: sv_global_step_point_value -= 1 @@ -183,6 +189,9 @@ def sv_pt_on_before_transmit_step_point(point: c104.Point) -> None: point.value = sv_global_step_point_value +my_server.on_receive_raw(callable=sv_on_receive_raw) +my_server.on_send_raw(callable=sv_on_send_raw) +my_server.on_clock_sync(callable=sv_on_clock_sync) sv_control_setpoint.on_receive(callable=sv_pt_on_setpoint_command) sv_control_setpoint_2.on_receive(callable=sv_pt_on_setpoint_command) sv_single_command.on_receive(callable=sv_pt_on_single_command) @@ -212,6 +221,12 @@ sv_step_command.on_receive(callable=sv_pt_on_step_command) * connect loop */ + // while(true) { + // std::cout << "start" << std::endl; + // my_server->start(); + // std::cout << "stop" << std::endl; + // my_server->stop(); + // } my_server->start(); while (!my_server->hasActiveConnections()) { @@ -225,7 +240,10 @@ sv_step_command.on_receive(callable=sv_pt_on_step_command) std::this_thread::sleep_for(10s); - sv_measurement_point->setValueEx(1234, Quality::None, 1711111111111); + sv_measurement_point->setInfo( + Object::ShortInfo::create(1234, Quality::None, + std::chrono::system_clock::time_point( + std::chrono::milliseconds(1711111111111)))); if (sv_measurement_point->transmit(CS101_COT_SPONTANEOUS)) { std::cout << "SV] transmit: Measurement point send successful" << std::endl; } else { @@ -234,7 +252,10 @@ sv_step_command.on_receive(callable=sv_pt_on_step_command) std::this_thread::sleep_for(10s); - sv_measurement_point->setValueEx(-1234.56, Quality::Invalid, 1711111111111); + sv_measurement_point->setInfo( + Object::ShortInfo::create(-1234.56, Quality::Invalid, + std::chrono::system_clock::time_point( + std::chrono::milliseconds(1711111111111)))); if (sv_measurement_point->transmit(CS101_COT_SPONTANEOUS)) { std::cout << "SV] transmit: Measurement point send successful" << std::endl; } else { diff --git a/src/module/Callback.h b/src/module/Callback.h index 7e436c8..06dca2e 100644 --- a/src/module/Callback.h +++ b/src/module/Callback.h @@ -203,14 +203,9 @@ template class Callback : public CallbackBase { if (DEBUG_TEST(Debug::Callback)) { this->end = std::chrono::steady_clock::now(); - DEBUG_PRINT_CONDITION( - true, Debug::Callback, - name + "] Stats | TOTAL " + - std::to_string( - std::chrono::duration_cast( - this->end - this->begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Callback, + name + "] Stats | TOTAL " + + TICTOC(this->begin, this->end)); } return this->success; @@ -287,14 +282,9 @@ template <> class Callback : public CallbackBase { if (DEBUG_TEST(Debug::Callback)) { this->end = std::chrono::steady_clock::now(); - DEBUG_PRINT_CONDITION( - true, Debug::Callback, - name + "] Stats | TOTAL " + - std::to_string( - std::chrono::duration_cast( - this->end - this->begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Callback, + name + "] Stats | TOTAL " + + TICTOC(this->begin, this->end)); } return this->success; diff --git a/src/module/GilAwareMutex.h b/src/module/GilAwareMutex.h index 2fa0bba..37bebe5 100644 --- a/src/module/GilAwareMutex.h +++ b/src/module/GilAwareMutex.h @@ -49,7 +49,7 @@ namespace Module { * automatically. * * The GilAwareMutex class provides a mutex that automatically releases and - * re-acquires the GIL when locking and unlocking. This is useful in + * re-acquires the GIL when locking. This is useful in * multi-threaded applications that interact with the Python interpreter, as it * allows other Python threads to continue executing while the lock is held. */ @@ -81,19 +81,12 @@ class GilAwareMutex { /** * @brief Unlocks the GilAwareMutex. * - * This function unlocks the GilAwareMutex by releasing the lock. It ensures - * that the GIL (Global Interpreter Lock) is re-acquired after unlocking. This - * is useful in multi-threaded applications that interact with the Python - * interpreter, as it allows other Python threads to continue executing while - * the lock is released. + * This function unlocks the GilAwareMutex by releasing the lock. * * @note This function should only be called after acquiring the lock using * the `lock()` function. */ - inline void unlock() { - ScopedGilRelease const scoped_gil(name + "::unlock_gil_aware"); - this->wrapped_mutex.unlock(); - } + inline void unlock() { this->wrapped_mutex.unlock(); } /** * @brief Tries to lock the GilAwareMutex. diff --git a/src/numbers.h b/src/numbers.h new file mode 100644 index 0000000..2402691 --- /dev/null +++ b/src/numbers.h @@ -0,0 +1,302 @@ +/** + * Copyright 2024-2024 Fraunhofer Institute for Applied Information Technology + * FIT + * + * This file is part of iec104-python. + * iec104-python is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iec104-python is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with iec104-python. If not, see . + * + * See LICENSE file for the complete license text. + * + * + * @file numbers.h + * @brief allow smaller ints and floats with validation + * + * @package iec104-python + * @namespace + * + * @authors Martin Unkel + * + */ + +#ifndef C104_NUMBERS_H +#define C104_NUMBERS_H + +#include +#include +#include +#include + +/** + * @brief integer representation with special limits + */ +template class LimitedInteger { +public: + // Constructor + LimitedInteger() = default; + + explicit LimitedInteger(int v) { throw std::domain_error("using base ctor"); } + + [[nodiscard]] virtual int getMin() const { return 0; } + + [[nodiscard]] virtual int getMax() const { return 0; } + + // Overloading operators with different types + virtual int operator+(const int &other) const { return value + other; } + + virtual int operator-(const int &other) const { return value - other; } + + virtual int operator*(const int &other) const { return value * other; } + + virtual int operator/(const int &other) const { + if (other == 0) { + throw std::runtime_error("Division by zero"); + } + return value / other; + } + + LimitedInteger &operator+=(const int &other) { + value = check_range(value + other); + return *this; + } + + LimitedInteger &operator-=(const int &other) { + value = check_range(value - other); + return *this; + } + + LimitedInteger &operator*=(const int &other) { + value = check_range(value * other); + return *this; + } + + LimitedInteger &operator/=(const int &other) { + if (other == 0) { + throw std::runtime_error("Division by zero"); + } + value = check_range(value / other); + return *this; + } + + [[nodiscard]] T get() const { return value; } + + void set(int v) { value = check_range(v); } + +protected: + T value{0}; + + [[nodiscard]] T check_range(int v) const { + if (v < getMin() || v > getMax()) { + throw std::out_of_range("Value is out of range."); + } + return v; + } +}; + +/** + * @brief unsigned integer of 5 bits size (0 - 31) + */ +class LimitedUInt5 : public LimitedInteger { +public: + // Constructor + LimitedUInt5() = default; + + explicit LimitedUInt5(int v) { set(v); } + + [[nodiscard]] int getMin() const override { return 0; } + + [[nodiscard]] int getMax() const override { return 31; } +}; + +/** + * @brief unsigned integer of 7 bits size (0 - 127) + */ +class LimitedUInt7 : public LimitedInteger { +public: + // Constructor + LimitedUInt7() = default; + + explicit LimitedUInt7(int v) { set(v); } + + [[nodiscard]] int getMin() const override { return 0; } + + [[nodiscard]] int getMax() const override { return 127; } +}; + +/** + * @brief unsigned integer of 16 bits size (0 - 65535) + */ +class LimitedUInt16 : public LimitedInteger { +public: + // Constructor + LimitedUInt16() = default; + + explicit LimitedUInt16(int v) { set(v); } + + [[nodiscard]] int getMin() const override { return 0; } + + [[nodiscard]] int getMax() const override { return 65535; } +}; + +/** + * @brief signed integer of 7 bits size (-64 - 63) + */ +class LimitedInt7 : public LimitedInteger { +public: + // Constructor + LimitedInt7() = default; + + explicit LimitedInt7(int v) { set(v); } + + [[nodiscard]] int getMin() const override { return -64; } + + [[nodiscard]] int getMax() const override { return 63; } +}; + +/** + * @brief signed integer of 16 bits size (-32768 - 32767) + */ +class LimitedInt16 : public LimitedInteger { +public: + // Constructor + LimitedInt16() = default; + + explicit LimitedInt16(int v) { set(v); } + + [[nodiscard]] int getMin() const override { return -32768; } + + [[nodiscard]] int getMax() const override { return 32767; } +}; + +/** + * @brief normalized floating point value of 32 bits size (-1.0 - 1.0) + */ +class NormalizedFloat { +public: + // Constructor + NormalizedFloat() = default; + + explicit NormalizedFloat(int v) { set(v); } + + explicit NormalizedFloat(float v) { set(v); } + + [[nodiscard]] float getMin() const { return -1.f; } + + [[nodiscard]] float getMax() const { return 1.f; } + + // Overloading operators with different types + float operator+(const int &other) const { return value + other; } + + float operator-(const int &other) const { return value - other; } + + float operator*(const int &other) const { return value * other; } + + float operator/(const int &other) const { + if (other == 0) { + throw std::runtime_error("Division by zero"); + } + return value / other; + } + + float operator+(const float &other) const { return value + other; } + + float operator-(const float &other) const { return value - other; } + + float operator*(const float &other) const { return value * other; } + + float operator/(const float &other) const { + if (other == 0) { + throw std::runtime_error("Division by zero"); + } + return value / other; + } + + NormalizedFloat &operator+=(const int &other) { + value = check_range(value + other); + return *this; + } + + NormalizedFloat &operator-=(const int &other) { + value = check_range(value - other); + return *this; + } + + NormalizedFloat &operator*=(const int &other) { + value = check_range(value * other); + return *this; + } + + NormalizedFloat &operator/=(const int &other) { + if (other == 0) { + throw std::runtime_error("Division by zero"); + } + value = check_range(value / other); + return *this; + } + + NormalizedFloat &operator+=(const float &other) { + value = check_range(value + other); + return *this; + } + + NormalizedFloat &operator-=(const float &other) { + value = check_range(value - other); + return *this; + } + + NormalizedFloat &operator*=(const float &other) { + value = check_range(value * other); + return *this; + } + + NormalizedFloat &operator/=(const float &other) { + if (other == 0) { + throw std::runtime_error("Division by zero"); + } + value = check_range(value / other); + return *this; + } + + [[nodiscard]] float get() const { return value; } + + void set(float v) { value = check_range(v); } + +protected: + float value{0}; + + [[nodiscard]] float check_range(float v) const { + if (v < getMin() || v > getMax()) { + throw std::out_of_range("Value is out of range."); + } + return v; + } +}; + +/** + * @brief raw bytes of 32 bits size + */ +class Byte32 { +public: + Byte32() : value(0) {} + + explicit Byte32(uint32_t val) : value(val) {} + + [[nodiscard]] uint32_t get() const { return value; } + + void set(uint32_t val) { value = val; } + +private: + uint32_t value; +}; + +#endif // C104_NUMBERS_H diff --git a/src/object/DataPoint.cpp b/src/object/DataPoint.cpp index 9689c74..6c862d5 100644 --- a/src/object/DataPoint.cpp +++ b/src/object/DataPoint.cpp @@ -32,6 +32,7 @@ #include "object/DataPoint.h" #include "Server.h" #include "module/ScopedGilAcquire.h" +#include "object/Information.h" #include "object/Station.h" #include "remote/Connection.h" #include "remote/message/IncomingMessage.h" @@ -41,15 +42,15 @@ using namespace Object; DataPoint::DataPoint(const std::uint_fast32_t dp_ioa, const IEC60870_5_TypeID dp_type, std::shared_ptr dp_station, - const std::uint_fast32_t dp_report_ms, - const std::uint_fast32_t dp_related_ioa, + const std::uint_fast16_t dp_report_ms, + const std::optional dp_related_ioa, const bool dp_related_auto_return, - const CommandTransmissionMode dp_cmd_mode) + const CommandTransmissionMode dp_cmd_mode, + const std::uint_fast16_t tick_rate_ms) : informationObjectAddress(dp_ioa), type(dp_type), station(dp_station), - reportInterval_ms(dp_report_ms), - relatedInformationObjectAddress(dp_related_ioa), + timerNext(std::chrono::steady_clock::now()), relatedInformationObjectAutoReturn(dp_related_auto_return), - commandMode(dp_cmd_mode) { + commandMode(dp_cmd_mode), tickRate_ms(tick_rate_ms) { if (type >= M_EI_NA_1) { throw std::invalid_argument("Unsupported type " + std::string(TypeID_toString(type))); @@ -57,31 +58,29 @@ DataPoint::DataPoint(const std::uint_fast32_t dp_ioa, is_server = dp_station && dp_station->isLocal(); - if (dp_ioa < 1 || dp_ioa > 16777216) { + // unsigned is always >= 0 + if (MAX_INFORMATION_OBJECT_ADDRESS < dp_ioa) { throw std::invalid_argument("Invalid information object address " + std::to_string(dp_ioa)); } - if (reportInterval_ms) { - if (dp_type > M_EP_TF_1) { - throw std::invalid_argument("Report interval option is only allowed for " - "monitoring types, but not for " + - std::string(TypeID_toString(type))); - } - if (!is_server) { + lastSentAt = std::chrono::steady_clock::now(); + setReportInterval_ms(dp_report_ms); + + if (dp_related_ioa.has_value()) { + if (MAX_INFORMATION_OBJECT_ADDRESS < dp_related_ioa) { throw std::invalid_argument( - "Report interval option is only allowed for server-sided points"); + "Invalid related information object address " + + std::to_string(dp_related_ioa.value())); } - } - - if (relatedInformationObjectAddress > 0) { + relatedInformationObjectAddress = dp_related_ioa.value(); if (!is_server) { throw std::invalid_argument( "Related IO address option is only allowed for server-sided points"); } } if (relatedInformationObjectAutoReturn) { - if (relatedInformationObjectAddress < 1) { + if (MAX_INFORMATION_OBJECT_ADDRESS < relatedInformationObjectAddress) { throw std::invalid_argument("Related IO auto return option cannot be " "used without the related IO address option"); } @@ -92,6 +91,159 @@ DataPoint::DataPoint(const std::uint_fast32_t dp_ioa, } } + switch (type) { + case M_SP_NA_1: + info = + std::make_shared(false, Quality::None, std::nullopt, false); + break; + case M_SP_TB_1: + info = std::make_shared( + false, Quality::None, std::chrono::system_clock::now(), false); + break; + case C_SC_NA_1: + info = std::make_shared( + false, false, CS101_QualifierOfCommand::NONE, std::nullopt, false); + break; + case C_SC_TA_1: + info = std::make_shared(false, false, + CS101_QualifierOfCommand::NONE, + std::chrono::system_clock::now(), false); + break; + case M_DP_NA_1: + info = std::make_shared(IEC60870_DOUBLE_POINT_OFF, + Quality::None, std::nullopt, false); + break; + case M_DP_TB_1: + info = + std::make_shared(IEC60870_DOUBLE_POINT_OFF, Quality::None, + std::chrono::system_clock::now(), false); + break; + case C_DC_NA_1: + info = std::make_shared(IEC60870_DOUBLE_POINT_OFF, false, + CS101_QualifierOfCommand::NONE, + std::nullopt, false); + case C_DC_TA_1: + info = std::make_shared(IEC60870_DOUBLE_POINT_OFF, false, + CS101_QualifierOfCommand::NONE, + std::chrono::system_clock::now(), false); + break; + case M_ST_NA_1: + info = std::make_shared(LimitedInt7(0), false, Quality::None, + std::nullopt, false); + break; + case M_ST_TB_1: + info = std::make_shared(LimitedInt7(0), false, Quality::None, + std::chrono::system_clock::now(), false); + break; + case C_RC_NA_1: + info = std::make_shared(IEC60870_STEP_LOWER, false, + CS101_QualifierOfCommand::NONE, + std::nullopt, false); + break; + case C_RC_TA_1: + info = std::make_shared(IEC60870_STEP_LOWER, false, + CS101_QualifierOfCommand::NONE, + std::chrono::system_clock::now(), false); + break; + case M_ME_NA_1: + case M_ME_ND_1: + info = std::make_shared(NormalizedFloat(0), Quality::None, + std::nullopt, false); + break; + case M_ME_TD_1: + info = std::make_shared(NormalizedFloat(0), Quality::None, + std::chrono::system_clock::now(), + false); + break; + case C_SE_NA_1: + info = std::make_shared( + NormalizedFloat(0), false, LimitedUInt7(0), std::nullopt, false); + break; + case C_SE_TA_1: + info = std::make_shared( + NormalizedFloat(0), false, LimitedUInt7(0), + std::chrono::system_clock::now(), false); + break; + case M_ME_NB_1: + info = std::make_shared(LimitedInt16(0), Quality::None, + std::nullopt, false); + break; + case M_ME_TE_1: + info = + std::make_shared(LimitedInt16(0), Quality::None, + std::chrono::system_clock::now(), false); + break; + case C_SE_NB_1: + info = std::make_shared(LimitedInt16(0), false, LimitedUInt7(0), + std::nullopt, false); + break; + case C_SE_TB_1: + info = std::make_shared(LimitedInt16(0), false, LimitedUInt7(0), + std::chrono::system_clock::now(), false); + break; + case M_ME_NC_1: + info = std::make_shared(0.0, Quality::None, std::nullopt, false); + break; + case M_ME_TF_1: + info = std::make_shared(0.0, Quality::None, + std::chrono::system_clock::now(), false); + break; + case C_SE_NC_1: + info = std::make_shared(0.0, false, LimitedUInt7(0), std::nullopt, + false); + break; + case C_SE_TC_1: + info = std::make_shared(0.0, false, LimitedUInt7(0), + std::chrono::system_clock::now(), false); + break; + case M_BO_NA_1: + info = std::make_shared(Byte32(0), Quality::None, std::nullopt, + false); + break; + case M_BO_TB_1: + info = std::make_shared( + Byte32(0), Quality::None, std::chrono::system_clock::now(), false); + break; + case C_BO_NA_1: + info = std::make_shared(Byte32(0), std::nullopt, false); + break; + case C_BO_TA_1: + info = std::make_shared(Byte32(0), + std::chrono::system_clock::now(), false); + break; + case M_IT_NA_1: + info = std::make_shared( + 0, LimitedUInt5(0), BinaryCounterQuality::None, std::nullopt, false); + break; + case M_IT_TB_1: + info = std::make_shared( + 0, LimitedUInt5(0), BinaryCounterQuality::None, + std::chrono::system_clock::now(), false); + break; + case M_EP_TD_1: + info = std::make_shared( + IEC60870_EVENTSTATE_OFF, LimitedUInt16(0), Quality::None, + std::chrono::system_clock::now(), false); + break; + case M_EP_TE_1: + info = std::make_shared( + StartEvents::None, LimitedUInt16(0), Quality::None, + std::chrono::system_clock::now(), false); + break; + case M_EP_TF_1: + info = std::make_shared( + OutputCircuits::None, LimitedUInt16(0), Quality::None, + std::chrono::system_clock::now(), false); + break; + case M_PS_NA_1: + info = std::make_shared( + FieldSet16(0), FieldSet16(0), Quality::None, std::nullopt, false); + break; + default: + throw std::invalid_argument("Unsupported type " + + std::string(TypeID_toString(type))); + } + DEBUG_PRINT(Debug::Point, "Created"); } @@ -108,26 +260,32 @@ std::uint_fast32_t DataPoint::getInformationObjectAddress() const { return informationObjectAddress; } -std::uint_fast32_t DataPoint::getRelatedInformationObjectAddress() const { - return relatedInformationObjectAddress.load(); +std::optional +DataPoint::getRelatedInformationObjectAddress() const { + std::uint_fast32_t ioa = relatedInformationObjectAddress.load(); + if (MAX_INFORMATION_OBJECT_ADDRESS < ioa) { + return std::nullopt; + } + return ioa; } void DataPoint::setRelatedInformationObjectAddress( - const std::uint_fast32_t related_io_address) { - if (related_io_address > 0) { - - if (related_io_address > 16777216) { + const std::optional related_io_address) { + if (related_io_address.has_value()) { + if (MAX_INFORMATION_OBJECT_ADDRESS < related_io_address) { throw std::invalid_argument( "Invalid related information object address " + - std::to_string(related_io_address)); + std::to_string(related_io_address.value())); } if (!is_server) { throw std::invalid_argument( "Related IO address option is only allowed for server-sided points"); } + relatedInformationObjectAddress.store(related_io_address.value()); + } else { + relatedInformationObjectAddress.store(UNDEFINED_INFORMATION_OBJECT_ADDRESS); } - relatedInformationObjectAddress.store(related_io_address); } bool DataPoint::getRelatedInformationObjectAutoReturn() const { @@ -136,7 +294,8 @@ bool DataPoint::getRelatedInformationObjectAutoReturn() const { void DataPoint::setRelatedInformationObjectAutoReturn(const bool auto_return) { if (auto_return) { - if (relatedInformationObjectAddress.load() < 1) { + if (MAX_INFORMATION_OBJECT_ADDRESS < + relatedInformationObjectAddress.load()) { throw std::invalid_argument("Related IO auto return option cannot be " "used without the related IO address option"); } @@ -154,228 +313,783 @@ CommandTransmissionMode DataPoint::getCommandMode() const { } void DataPoint::setCommandMode(const CommandTransmissionMode mode) { + if (SELECT_AND_EXECUTE_COMMAND == mode && + (type < C_SC_NA_1 || C_BO_NA_1 == type || C_SE_TC_1 < type)) { + throw std::invalid_argument("Only control points, except for C_BO_* " + "support select and execute mode"); + } commandMode.store(mode); } -std::uint_fast8_t DataPoint::getSelectedByOriginatorAddress() const { - return selectedByOriginatorAddress.load(); -} - -void DataPoint::setSelectedByOriginatorAddress( - const std::uint_fast8_t originatorAddress) { - if (!is_server || (type < C_SC_NA_1 && type > C_SE_NC_1) || - (type < C_SC_TA_1 && type > C_SE_TC_1)) { - throw std::invalid_argument( - "Only server-sided control points can be selected"); +std::optional DataPoint::getSelectedByOriginatorAddress() { + if (auto st = getStation()) { + if (auto server = st->getServer()) { + return server->getSelector(st->getCommonAddress(), + informationObjectAddress); + } } - selectedByOriginatorAddress.store(originatorAddress); + return std::nullopt; } IEC60870_5_TypeID DataPoint::getType() const { return type; } -Quality DataPoint::getQuality() const { return quality.load(); } +std::shared_ptr DataPoint::getInfo() const { return info; } + +void DataPoint::setInfo(std::shared_ptr new_info) { + bool const debug = DEBUG_TEST(Debug::Point); -void DataPoint::setQuality(const Quality &new_quality) { - Quality const prev_quality = quality.load(); - if (prev_quality != new_quality) { - quality.store(new_quality); - DEBUG_PRINT(Debug::Point, - "set_quality] prev: " + Quality_toString(prev_quality) + - ") new: " + Quality_toString(new_quality) + " at IOA " + - std::to_string(informationObjectAddress)); + switch (type) { + case M_SP_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type SingleInfo, but is " + + new_info->name()); + } + } break; + case M_SP_TB_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type SingleInfo, but is " + + new_info->name()); + } + } break; + case C_SC_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type SingleCmd, but is " + + new_info->name()); + } + } break; + case C_SC_TA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type SingleCmd, but is " + + new_info->name()); + } + } break; + case M_DP_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type DoubleInfo, but is " + + new_info->name()); + } + } break; + case M_DP_TB_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type DoubleInfo, but is " + + new_info->name()); + } + } break; + case C_DC_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type DoubleCmd, but is " + + new_info->name()); + } + } break; + case C_DC_TA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type DoubleCmd, but is " + + new_info->name()); + } + } break; + case M_ST_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type StepInfo, but is " + + new_info->name()); + } + } break; + case M_ST_TB_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type StepInfo, but is " + + new_info->name()); + } + } break; + case C_RC_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type StepCmd, but is " + new_info->name()); + } + } break; + case C_RC_TA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type StepCmd, but is " + new_info->name()); + } + } break; + case M_ME_NA_1: + case M_ME_ND_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type NormalizedInfo, but is " + + new_info->name()); + } + } break; + case M_ME_TD_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type NormalizedInfo, but is " + + new_info->name()); + } + } break; + case C_SE_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type NormalizedCmd, but is " + + new_info->name()); + } + } break; + case C_SE_TA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type NormalizedCmd, but is " + + new_info->name()); + } + } break; + case M_ME_NB_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ScaledInfo, but is " + + new_info->name()); + } + } break; + case M_ME_TE_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ScaledInfo, but is " + + new_info->name()); + } + } break; + case C_SE_NB_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ScaledCmd, but is " + + new_info->name()); + } + } break; + case C_SE_TB_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ScaledCmd, but is " + + new_info->name()); + } + } break; + case M_ME_NC_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ShortInfo, but is " + + new_info->name()); + } + } break; + case M_ME_TF_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ShortInfo, but is " + + new_info->name()); + } + } break; + case C_SE_NC_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ShortCmd, but is " + + new_info->name()); + } + } break; + case C_SE_TC_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ShortCmd, but is " + + new_info->name()); + } + } break; + case M_BO_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type BinaryInfo, but is " + + new_info->name()); + } + } break; + case M_BO_TB_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type BinaryInfo, but is " + + new_info->name()); + } + } break; + case C_BO_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type BinaryCmd, but is " + + new_info->name()); + } + } break; + case C_BO_TA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type BinaryCmd, but is " + + new_info->name()); + } + } break; + case M_IT_NA_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type BinaryCounterInfo, but is " + + new_info->name()); + } + } break; + case M_IT_TB_1: { + auto i = std::dynamic_pointer_cast(new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type BinaryCounterInfo, but is " + + new_info->name()); + } + } break; + case M_EP_TD_1: { + auto i = std::dynamic_pointer_cast( + new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ProtectionEventInfo, but is " + + new_info->name()); + } + } break; + case M_EP_TE_1: { + auto i = + std::dynamic_pointer_cast( + new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ProtectionStartInfo, but is " + + new_info->name()); + } + } break; + case M_EP_TF_1: { + auto i = + std::dynamic_pointer_cast( + new_info); + if (i) { + if (!i->getRecordedAt().has_value()) { + i->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT_CONDITION(debug, Debug::Point, + "Injecting current local timestamp into " + "information for [c104.Type." + + std::string(TypeID_toString(type)) + + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type ProtectionCircuitInfo, but is " + + new_info->name()); + } + } break; + case M_PS_NA_1: { + auto i = + std::dynamic_pointer_cast(new_info); + if (i) { + if (i->getRecordedAt().has_value()) { + i->setRecordedAt(std::nullopt); + DEBUG_PRINT_CONDITION( + debug, Debug::Point, + "Dropping timestamp of information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + } + } else { + throw std::invalid_argument( + "[c104.Type." + std::string(TypeID_toString(type)) + + "] requires Information of type StatusAndChanged, but is " + + new_info->name()); + } + } break; + default: + throw std::invalid_argument("Unsupported type " + + std::string(TypeID_toString(type))); } + info = std::move(new_info); } -double DataPoint::getValue() const { return value; } - -std::int32_t DataPoint::getValueAsInt32() const { return (int)value; } - -float DataPoint::getValueAsFloat() const { return (float)value; } - -std::uint32_t DataPoint::getValueAsUInt32() const { return (uint32_t)value; } +InfoValue DataPoint::getValue() { return info->getValue(); } -void DataPoint::setValue(const double new_value) { - setValueEx(new_value, Quality::None, 0); -} - -void DataPoint::setValueEx(const double new_value, const Quality new_quality, - const std::uint_fast64_t timestamp_ms) { - // set predefined timestamp if provided (as client) - if (timestamp_ms > 0) { - updatedAt_ms = timestamp_ms; - } else { - updatedAt_ms = GetTimestamp_ms(); +void DataPoint::setValue(const InfoValue new_value) { + info->setValue(new_value); + switch (type) { + case M_SP_TB_1: + case C_SC_TA_1: + case M_DP_TB_1: + case C_DC_TA_1: + case M_ST_TB_1: + case C_RC_TA_1: + case M_ME_TD_1: + case C_SE_TA_1: + case M_ME_TE_1: + case C_SE_TB_1: + case M_ME_TF_1: + case C_SE_TC_1: + case M_BO_TB_1: + case C_BO_TA_1: + case M_IT_TB_1: + case M_EP_TD_1: + case M_EP_TE_1: + case M_EP_TF_1: + info->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT( + Debug::Point, + "Injecting current local timestamp into information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + break; } +} - // detect NaN values - if (std::isnan(new_value)) { - // quality.store(Quality::Invalid); - DEBUG_PRINT(Debug::Point, "set_value_ex] detected NaN value at IOA " + - std::to_string(informationObjectAddress)); - // return; - } +InfoQuality DataPoint::getQuality() { return info->getQuality(); } - double const val = value.load(); - Quality const qval = quality.load(); - if (val != new_value) { - value.store(new_value); +void DataPoint::setQuality(const InfoQuality new_Quality) { + info->setQuality(new_Quality); + switch (type) { + case M_SP_TB_1: + case C_SC_TA_1: + case M_DP_TB_1: + case C_DC_TA_1: + case M_ST_TB_1: + case C_RC_TA_1: + case M_ME_TD_1: + case C_SE_TA_1: + case M_ME_TE_1: + case C_SE_TB_1: + case M_ME_TF_1: + case C_SE_TC_1: + case M_BO_TB_1: + case C_BO_TA_1: + case M_IT_TB_1: + case M_EP_TD_1: + case M_EP_TE_1: + case M_EP_TF_1: + info->setRecordedAt(std::chrono::system_clock::now()); + DEBUG_PRINT( + Debug::Point, + "Injecting current local timestamp into information for [c104.Type." + + std::string(TypeID_toString(type)) + "] at IOA " + + std::to_string(informationObjectAddress)); + break; } - quality.store(new_quality); - - if (is_none(new_quality)) { - switch (type) { - case M_SP_NA_1: - case M_SP_TA_1: - case M_SP_TB_1: - case C_SC_NA_1: - case C_SC_TA_1: { - if (new_value != 0 && new_value != 1) { - // unexpected bad quality - std::cerr << "[c104.Point.setValueEx] Cannot set value of M_SP and " - "C_SC to numbers other than 0 and 1 at IOA " - << std::to_string(informationObjectAddress) << std::endl; - quality.store(Quality::Invalid); - } - } break; - case M_DP_NA_1: - case M_DP_TA_1: - case M_DP_TB_1: { - if (new_value != 0 && new_value != 1 && new_value != 2 && - new_value != 3) { - // unexpected bad quality - std::cerr << "[c104.Point.setValueEx] Cannot set value of M_DP to " - "numbers other than 0,1,2,3 at IOA " - << std::to_string(informationObjectAddress) << std::endl; - quality.store(Quality::Invalid); - } - } break; - case C_DC_NA_1: - case C_DC_TA_1: - case C_RC_NA_1: - case C_RC_TA_1: { - if (new_value != 1 && new_value != 2) { - // unexpected bad quality - std::cerr << "[c104.Point.setValueEx] Cannot set value of C_DC and " - "C_RC to numbers other than 1,2 at IOA " - << std::to_string(informationObjectAddress) << std::endl; - quality.store(Quality::Invalid); - } - } break; - case M_ST_NA_1: - case M_ST_TA_1: - case M_ST_TB_1: { - double int_part = 0; - if (std::modf(new_value, &int_part) != 0.0 || new_value < -63 || - new_value > 64) { - // unexpected bad quality - std::cerr << "[c104.Point.setValueEx] Cannot set value of M_ST to " - "numbers other than [-63, ... , +64] at IOA " - << std::to_string(informationObjectAddress) << std::endl; - quality.store(Quality::Invalid); - } - } break; - case M_BO_NA_1: - case M_BO_TA_1: - case M_BO_TB_1: - case C_BO_NA_1: - case C_BO_TA_1: { - double int_part = 0; - if (std::modf(new_value, &int_part) != 0.0 || new_value < 0 || - new_value >= std::pow(2, 32)) { - // unexpected bad quality - std::cerr << "[c104.Point.setValueEx] Cannot set value of M_BO and " - "C_BO to numbers other than [0, ... , 2^32 - 1] at IOA " - << std::to_string(informationObjectAddress) << std::endl; - quality.store(Quality::Invalid); - } - } break; - case M_ME_NA_1: - case M_ME_ND_1: - case M_ME_TA_1: - case M_ME_TD_1: - case C_SE_NA_1: - case C_SE_TA_1: { - if (new_value < -1.f || new_value > 1.f) { - // unexpected bad quality - std::cerr - << "[c104.Point.setValueEx] Cannot set value of M_ME (normalized) " - "to numbers other than [-1.0, ... , +1.0] at IOA " - << std::to_string(informationObjectAddress) << std::endl; - quality.store(Quality::Invalid); - } - } break; - case M_ME_NB_1: - case M_ME_TB_1: - case M_ME_TE_1: - case C_SE_NB_1: - case C_SE_TB_1: { - if (new_value < -65536. || new_value > 65535.) { - // unexpected bad quality - std::cerr - << "[c104.Point.setValueEx] Cannot set value of M_ME (scaled) to " - "numbers other than [-2^16, ... , +2^16 - 1] at IOA " - << std::to_string(informationObjectAddress) << std::endl; - quality.store(Quality::Invalid); - } - } break; - case M_ME_NC_1: - case M_ME_TC_1: - case M_ME_TF_1: - case C_SE_NC_1: - case C_SE_TC_1: { - // no validation required - } break; - case M_IT_NA_1: - case M_IT_TA_1: - case M_IT_TB_1: { - double int_part = 0; - if (std::modf(new_value, &int_part) != 0.0 || new_value < -65536 || - new_value > 65535) { - // unexpected bad quality - std::cerr - << "[c104.Point.setValueEx] Cannot set value of M_IT to numbers " - "other than [-2^16, ... , +2^16 - 1] (4x uint8) at IOA " - << std::to_string(informationObjectAddress) << std::endl; - quality.store(Quality::Invalid); - } - } break; - } - } - DEBUG_PRINT(Debug::Point, "set_value_ex] prev: " + std::to_string(val) + - " (" + Quality_toString(qval) + ") new: " + - std::to_string(value.load()) + " (" + - Quality_toString(new_quality) + ") at IOA " + - std::to_string(informationObjectAddress)); } -uint64_t DataPoint::getUpdatedAt_ms() const { return updatedAt_ms.load(); } +std::optional +DataPoint::getRecordedAt() const { + return info->getRecordedAt(); +} -uint64_t DataPoint::getReportedAt_ms() const { return reportedAt_ms.load(); } +std::chrono::system_clock::time_point DataPoint::getProcessedAt() const { + return info->getProcessedAt(); +} -void DataPoint::setReportedAt_ms(const std::uint_fast64_t timestamp_ms) { - reportedAt_ms.store(timestamp_ms); +void DataPoint::setProcessedAt( + const std::chrono::system_clock::time_point val) { + lastSentAt.store(std::chrono::steady_clock::now()); + info->setProcessedAt(val); } -std::uint_fast32_t DataPoint::getReportInterval_ms() const { +std::uint_fast16_t DataPoint::getReportInterval_ms() const { return reportInterval_ms.load(); } -void DataPoint::setReportInterval_ms(const std::uint_fast32_t interval_ms) { - if (type > M_EP_TF_1) { - throw std::invalid_argument("Report interval option is only allowed for " - "monitoring types, but not for " + - std::string(TypeID_toString(type))); - } - if (!is_server) { - throw std::invalid_argument( - "Report interval option is only allowed for server-sided points"); +void DataPoint::setReportInterval_ms(const std::uint_fast16_t interval_ms) { + if (interval_ms > 0) { + if (interval_ms < tickRate_ms || interval_ms % tickRate_ms != 0) { + throw std::range_error("interval_ms (=" + std::to_string(interval_ms) + + ") must be a positive integer multiple " + "of server/client tickRate_ms (=" + + std::to_string(tickRate_ms) + ")"); + } + if (type > M_IT_TB_1) { + throw std::invalid_argument("Report interval option is only allowed for " + "monitoring types, but not for " + + std::string(TypeID_toString(type))); + } + if (!is_server) { + throw std::invalid_argument( + "Report interval option is only allowed for server-sided points"); + } } reportInterval_ms.store(interval_ms); } -uint64_t DataPoint::getReceivedAt_ms() const { return receivedAt_ms.load(); } - -uint64_t DataPoint::getSentAt_ms() const { return sentAt_ms.load(); } +std::uint_fast16_t DataPoint::getTimerInterval_ms() const { + return timerInterval_ms.load(); +} void DataPoint::setOnReceiveCallback(py::object &callable) { py_onReceive.reset(callable); @@ -383,69 +1097,14 @@ void DataPoint::setOnReceiveCallback(py::object &callable) { CommandResponseState DataPoint::onReceive( std::shared_ptr message) { - bool select_only = false; - double const prev_value = value.load(); - Quality const prev_quality = quality.load(); - uint64_t const prev_updatedAt = updatedAt_ms.load(); - uint8_t prev_selected = selectedByOriginatorAddress.load(); - - if ((type >= C_SC_NA_1 && type <= C_SE_NC_1) || - (type >= C_SC_TA_1 && type <= C_SE_TC_1)) { - std::uint_fast8_t originatorAddress = message->getOriginatorAddress(); - // SELECT - if (message->isSelectCommand()) { - if (commandMode == DIRECT_COMMAND) { - std::cerr << "Cannot select point in DIRECT command mode" << std::endl; - return RESPONSE_STATE_FAILURE; - } - if (selectedByOriginatorAddress > 0 && - selectedByOriginatorAddress != originatorAddress) { - std::cerr << "Cannot select point by X, already selected by Y" - << std::endl; - return RESPONSE_STATE_FAILURE; - } - // set select lock - selectedByOriginatorAddress.store(originatorAddress); - select_only = true; - } - // EXECUTE - else { - if (commandMode == SELECT_AND_EXECUTE_COMMAND) { - if (selectedByOriginatorAddress == 0) { - std::cerr << "Cannot execute command on point in SELECT_AND_EXECUTE " - "command mode without selection" - << std::endl; - return RESPONSE_STATE_FAILURE; - } - if (selectedByOriginatorAddress != originatorAddress) { - std::cerr << "Cannot select point by X, already selected by Y" - << std::endl; - return RESPONSE_STATE_FAILURE; - } - // release select lock - selectedByOriginatorAddress.store(0); - } - } - } - - // do not store a select command value - if (!select_only) { - setValueEx(message->getValue(), message->getQuality(), - message->getUpdatedAt()); - receivedAt_ms = GetTimestamp_ms(); - } + auto prev = std::move(info); + info = message->getInfo(); if (py_onReceive.is_set()) { DEBUG_PRINT(Debug::Point, "CALLBACK on_receive at IOA " + std::to_string(informationObjectAddress)); Module::ScopedGilAcquire const scoped("Point.on_receive"); - py::dict prev; - prev["value"] = prev_value; - prev["quality"] = prev_quality; - prev["updatedAt_ms"] = prev_updatedAt; - prev["selected_by"] = prev_selected; - if (py_onReceive.call(shared_from_this(), prev, message)) { try { return py_onReceive.getResult(); @@ -492,6 +1151,42 @@ void DataPoint::onBeforeAutoTransmit() { } } +std::optional +DataPoint::nextReportAt() const { + if (reportInterval_ms > 0) { + return lastSentAt.load() + std::chrono::milliseconds(reportInterval_ms); + } + return std::nullopt; +} + +std::optional +DataPoint::nextTimerAt() const { + if (timerInterval_ms > 0 && py_onTimer.is_set()) { + return timerNext; + } + return std::nullopt; +} + +void DataPoint::setOnTimerCallback(py::object &callable, + const std::uint_fast16_t interval_ms) { + if (interval_ms < tickRate_ms || interval_ms % tickRate_ms != 0) + throw std::range_error("interval_ms must be a positive integer multiple of " + "server/client tickRate_ms"); + timerInterval_ms.store(callable.is_none() ? 0 : interval_ms); + py_onTimer.reset(callable); +} + +void DataPoint::onTimer() { + if (py_onTimer.is_set()) { + timerNext = std::chrono::steady_clock::now() + + std::chrono::milliseconds(timerInterval_ms); + DEBUG_PRINT(Debug::Point, "CALLBACK on_timer at IOA " + + std::to_string(informationObjectAddress)); + Module::ScopedGilAcquire scoped("Point.on_timer"); + py_onTimer.call(shared_from_this()); + } +} + bool DataPoint::read() { auto _station = getStation(); if (!_station) { @@ -512,8 +1207,7 @@ bool DataPoint::read() { return _connection->read(shared_from_this()); } -bool DataPoint::transmit(const CS101_CauseOfTransmission cause, - const CS101_QualifierOfCommand qualifier) { +bool DataPoint::transmit(const CS101_CauseOfTransmission cause) { DEBUG_PRINT(Debug::Point, "transmit_ex] " + std::string(TypeID_toString(type)) + " at IOA " + std::to_string(informationObjectAddress)); @@ -525,15 +1219,10 @@ bool DataPoint::transmit(const CS101_CauseOfTransmission cause, // as server if (_station->isLocal()) { - if (qualifier != CS101_QualifierOfCommand::NONE) { - throw std::invalid_argument( - "The qualifier argument must not be used in monitoring direction"); - } auto server = _station->getServer(); if (!server) { throw std::invalid_argument("Server reference deleted"); } - sentAt_ms = GetTimestamp_ms(); return server->transmit(shared_from_this(), cause); } @@ -542,6 +1231,5 @@ bool DataPoint::transmit(const CS101_CauseOfTransmission cause, if (!connection) { throw std::invalid_argument("Client connection reference deleted"); } - sentAt_ms = GetTimestamp_ms(); - return connection->transmit(shared_from_this(), cause, qualifier); + return connection->transmit(shared_from_this(), cause); } diff --git a/src/object/DataPoint.h b/src/object/DataPoint.h index 5f8f204..dbf825b 100644 --- a/src/object/DataPoint.h +++ b/src/object/DataPoint.h @@ -20,7 +20,7 @@ * * * @file DataPoint.h - * @brief 60870-5-104 information object + * @brief abstract data point * * @package iec104-python * @namespace object @@ -33,11 +33,12 @@ #define C104_OBJECT_DATAPOINT_H #include "module/Callback.h" -#include "module/GilAwareMutex.h" #include "module/ScopedGilAcquire.h" +#include "object/Information.h" #include "types.h" namespace Object { + class DataPoint : public std::enable_shared_from_this { public: // noncopyable @@ -50,24 +51,26 @@ class DataPoint : public std::enable_shared_from_this { * @param dp_type iec60870-5-104 information type * @param dp_station station object reference * @param dp_report_ms auto reporting interval - * @param dp_related_ioa related information object address + * @param dp_related_ioa related information object address, if any * @param dp_related_auto_return auto transmit related point on command * @param dp_cmd_mode command transmission mode (direct or select-and-execute) + * @param tick_rate_ms outer tick rate * @throws std::invalid_argument if type is invalid */ [[nodiscard]] static std::shared_ptr create(std::uint_fast32_t dp_ioa, IEC60870_5_TypeID dp_type, std::shared_ptr dp_station, - std::uint_fast32_t dp_report_ms = 0, - std::uint_fast32_t dp_related_ioa = 0, + std::uint_fast16_t dp_report_ms = 0, + std::optional dp_related_ioa = std::nullopt, bool dp_related_auto_return = false, - CommandTransmissionMode dp_cmd_mode = DIRECT_COMMAND) { + CommandTransmissionMode dp_cmd_mode = DIRECT_COMMAND, + std::uint_fast16_t tick_rate_ms = 0) { Module::ScopedGilAcquire scoped("DataPoint.create"); // Not using std::make_shared because the constructor is private. - return std::shared_ptr( - new DataPoint(dp_ioa, dp_type, std::move(dp_station), dp_report_ms, - dp_related_ioa, dp_related_auto_return, dp_cmd_mode)); + return std::shared_ptr(new DataPoint( + dp_ioa, dp_type, std::move(dp_station), dp_report_ms, dp_related_ioa, + dp_related_auto_return, dp_cmd_mode, tick_rate_ms)); } /** @@ -85,17 +88,26 @@ class DataPoint : public std::enable_shared_from_this { * @param dp_related_ioa related information object address * @param dp_related_auto_return auto transmit related point on command * @param dp_cmd_mode command transmission mode (direct or select-and-execute) + * @param tick_rate_ms outer tick rate * @throws std::invalid_argument if arguments provided are not compatible */ DataPoint(std::uint_fast32_t dp_ioa, IEC60870_5_TypeID dp_type, std::shared_ptr dp_station, - std::uint_fast32_t dp_report_ms, std::uint_fast32_t dp_related_ioa, - bool dp_related_auto_return, CommandTransmissionMode dp_cmd_mode); + std::uint_fast16_t dp_report_ms, + std::optional dp_related_ioa, + bool dp_related_auto_return, CommandTransmissionMode dp_cmd_mode, + std::uint_fast16_t tick_rate_ms); bool is_server{false}; /// @brief IEC60870-5 remote address of this DataPoint - std::uint_fast32_t informationObjectAddress{0}; + const std::uint_fast32_t informationObjectAddress; + + /// @brief IEC60870-5 TypeID for related remote message + const IEC60870_5_TypeID type; + + /// @brief parent Station object (not owning pointer) + const std::weak_ptr station; /// @brief IEC60870-5 remote address of a related measurement DataPoint std::atomic_uint_fast32_t relatedInformationObjectAddress{0}; @@ -107,42 +119,28 @@ class DataPoint : public std::enable_shared_from_this { /// @brief command transmission mode (direct or select-and-execute) std::atomic commandMode{DIRECT_COMMAND}; - /// @brief current client execution lock - selected by address for exclusive - /// update execution - std::atomic_uint_fast8_t selectedByOriginatorAddress{0}; - - /// @brief current value - std::atomic value{0}; - - /// @brief value quality descriptor - std::atomic quality{Quality::None}; + /// @brief abstract representation of information + std::shared_ptr info{nullptr}; - /// @brief timestamp (in milliseconds) of last value assignment - std::atomic_uint_fast64_t updatedAt_ms{0}; + /// @brief steady clock to calculate nextReportAt + std::atomic lastSentAt; - /// @brief timestamp (in milliseconds) of last periodic transmission - std::atomic_uint_fast64_t reportedAt_ms{0}; + const std::uint_fast16_t tickRate_ms; /// @brief interval (in milliseconds) between periodic transmissions, 0 => no /// periodic transmission - std::atomic_uint_fast32_t reportInterval_ms{0}; - - /// @brief timestamp (in milliseconds) of last receiving - std::atomic_uint_fast64_t receivedAt_ms{0}; - - /// @brief timestamp (in milliseconds) of last transmission - std::atomic_uint_fast64_t sentAt_ms{0}; + std::atomic reportInterval_ms; - /// @brief IEC60870-5 TypeID for related remote message - IEC60870_5_TypeID type{IEC60870_5_TypeID::C_TS_TA_1}; + /// @brief interval (in milliseconds) between timer execution, 0 => no timer + std::atomic timerInterval_ms; - /// @brief parent Station object (not owning pointer) - std::weak_ptr station{}; + std::atomic timerNext{}; /// @brief python callback function pointer Module::Callback py_onReceive{ - "Point.on_receive", "(point: c104.Point, previous_state: dict, message: " - "c104.IncomingMessage) -> c104.ResponseState"}; + "Point.on_receive", + "(point: c104.Point, previous_info: c104.Information, message: " + "c104.IncomingMessage) -> c104.ResponseState"}; /// @brief python callback function pointer Module::Callback py_onBeforeRead{"Point.on_before_read", @@ -152,6 +150,10 @@ class DataPoint : public std::enable_shared_from_this { Module::Callback py_onBeforeAutoTransmit{ "Point.on_before_auto_transmit", "(point: c104.Point) -> None"}; + /// @brief python callback function pointer + Module::Callback py_onTimer{"Point.on_timer", + "(point: c104.Point) -> None"}; + public: /** * @brief Get the NetworkStation that owns this DataPoint @@ -169,15 +171,15 @@ class DataPoint : public std::enable_shared_from_this { * @brief Get the information object address of a related monitoring point * @return IOA */ - std::uint_fast32_t getRelatedInformationObjectAddress() const; + std::optional getRelatedInformationObjectAddress() const; /** * @brief Set the information object address of a related monitoring point * @throws std::invalid_argument if not a server-sided control point or * invalid IOA */ - void - setRelatedInformationObjectAddress(std::uint_fast32_t related_io_address); + void setRelatedInformationObjectAddress( + std::optional related_io_address); /** * @brief Test if a related monitoring point should be auto-transmitted on @@ -210,13 +212,7 @@ class DataPoint : public std::enable_shared_from_this { * @return client originator address or zero if no active selection lock * exists */ - std::uint_fast8_t getSelectedByOriginatorAddress() const; - - /** - * @brief Configure select-and-execute lock originator address - * @throws std::invalid_argument if not a server-sided control point - */ - void setSelectedByOriginatorAddress(std::uint_fast8_t originatorAddress); + std::optional getSelectedByOriginatorAddress(); IEC60870_5_TypeID getType() const; @@ -224,93 +220,70 @@ class DataPoint : public std::enable_shared_from_this { * @brief Get automatic report transmission interval of this point * @return interval in milliseconds, 0 if disabled */ - std::uint_fast32_t getReportInterval_ms() const; + std::uint_fast16_t getReportInterval_ms() const; /** * @brief Configure automatic report transmission interval of this monitoring * point * @throws std::invalid_argument if not a server-sided monitoring point */ - void setReportInterval_ms(std::uint_fast32_t interval_ms); + void setReportInterval_ms(std::uint_fast16_t interval_ms); /** - * @brief Get quality restriction bitset for the current value - * @return qualit restrictions + * @brief Get automatic timer interval of this point + * @return interval in milliseconds, 0 if disabled */ - Quality getQuality() const; + std::uint_fast16_t getTimerInterval_ms() const; - /** - * @brief Set quality restriction bitset for the current value - */ - void setQuality(const Quality &new_quality); + std::shared_ptr getInfo() const; /** - * @brief Get point value - * @return value + * @brief Set point value */ - double getValue() const; + void setInfo(std::shared_ptr new_info); - /** - * @brief Get point value - * @return value converted to signed integer - */ - std::int32_t getValueAsInt32() const; + InfoValue getValue(); /** - * @brief Get point value - * @return value converted to signed float + * @brief Set point value */ - float getValueAsFloat() const; + void setValue(InfoValue new_value); - /** - * @brief Get point value - * @return value converted to unsigned integer - */ - std::uint32_t getValueAsUInt32() const; + InfoQuality getQuality(); /** * @brief Set point value */ - void setValue(double new_value); + void setQuality(InfoQuality new_value); /** - * @brief Set point value with quality restriction bitset and updated at - * timestamp - * @param new_value the value to be assigned to the point - * @param new_quality value quality information - * @param timestamp_ms value updated at timestamp in milliseconds + * @brief get timestamp bundled with value + * @return milliseconds since unix-epoch */ - void setValueEx(double new_value, const Quality new_quality = Quality::None, - std::uint_fast64_t timestamp_ms = 0); + std::optional getRecordedAt() const; /** - * @brief get timestamp of last value update - * @return seconds since unix-epoch + * @brief get timestamp of last local processing operation (receiving/sending) + * @return milliseconds since unix-epoch */ - std::uint64_t getUpdatedAt_ms() const; + std::chrono::system_clock::time_point getProcessedAt() const; /** - * @brief get timestamp of last cyclic report (server-only) - * @return seconds since unix-epoch + * @brief set timestamp of last local processing operation (receiving/sending) */ - std::uint64_t getReportedAt_ms() const; + void setProcessedAt(std::chrono::system_clock::time_point val); /** - * @brief get timestamp of last incoming transmission + * @brief get next timer event point * @return seconds since unix-epoch */ - std::uint64_t getReceivedAt_ms() const; + std::optional nextReportAt() const; /** - * @brief get timestamp of last non-cyclic transmission + * @brief get next timer event point * @return seconds since unix-epoch */ - std::uint64_t getSentAt_ms() const; - - /** - * @brief set timestamp of last outgoing transmission from server to client - */ - void setReportedAt_ms(std::uint_fast64_t timestamp_ms); + std::optional nextTimerAt() const; /** * @brief handle remote point update, execute python callback @@ -354,6 +327,18 @@ class DataPoint : public std::enable_shared_from_this { */ void setOnBeforeAutoTransmitCallback(py::object &callable); + /** + * @brief handle timer event, execute python callback + */ + void onTimer(); + + /** + * @brief set python callback that will be called at a fixed interval + * @throws std::invalid_argument if callable signature does not match + */ + void setOnTimerCallback(py::object &callable, + std::uint_fast16_t interval_ms = 0); + /** * @brief send read command to update the points value * @throws std::invalid_argument if parent station or connection reference is @@ -369,26 +354,27 @@ class DataPoint : public std::enable_shared_from_this { * @throws std::invalid_argument if parent station or connection reference is * invalid */ - bool - transmit(CS101_CauseOfTransmission cause = CS101_COT_UNKNOWN_COT, - CS101_QualifierOfCommand qualifier = CS101_QualifierOfCommand::NONE); - - inline friend std::ostream &operator<<(std::ostream &os, DataPoint &dp) { - os << std::endl - << "+------------------------------+" << '\n' - << "| DUMP Asset/DataPoint |" << '\n' - << "+------------------------------+" << '\n' - << "|" << std::setw(19) << "IOA: " << std::setw(10) - << std::to_string(dp.informationObjectAddress) << " |" << '\n' - << "|" << std::setw(19) << "value: " << std::setw(10) - << std::to_string(dp.value) << " |" << '\n' - << "|" << std::setw(19) << "type: " << std::setw(10) - << TypeID_toString(dp.type) << " |" << '\n' - << "|" << std::setw(19) << "updated_at: " << std::setw(10) - << dp.updatedAt_ms << " |" << '\n' - << "+------------------------------+" << std::endl; - return os; - } + bool transmit(CS101_CauseOfTransmission cause = CS101_COT_UNKNOWN_COT); + + std::string toString() const { + std::ostringstream oss; + oss << "(this) << ">"; + return oss.str(); + }; }; /** diff --git a/src/object/Information.cpp b/src/object/Information.cpp new file mode 100644 index 0000000..c45ff93 --- /dev/null +++ b/src/object/Information.cpp @@ -0,0 +1,489 @@ +/** + * Copyright 2024-2024 Fraunhofer Institute for Applied Information Technology + * FIT + * + * This file is part of iec104-python. + * iec104-python is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iec104-python is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with iec104-python. If not, see . + * + * See LICENSE file for the complete license text. + * + * + * @file Information.cpp + * @brief abstract protocol information + * + * @package iec104-python + * @namespace object + * + * @authors Martin Unkel + * + */ + +#include "object/Information.h" + +using namespace Object; + +Information::Information( + const std::optional recorded_at, + const bool readonly) + : recorded_at(recorded_at), readonly(readonly) { + processed_at = std::chrono::system_clock::now(); +}; + +Command::Command( + const std::optional recorded_at, + const bool readonly) + : Information(recorded_at, readonly){}; + +InfoValue Information::getValueImpl() const { return std::monostate{}; } + +void Information::setValueImpl(const InfoValue val){}; + +InfoQuality Information::getQualityImpl() const { return std::monostate{}; } + +void Information::setQualityImpl(const InfoQuality val){}; + +InfoValue Information::getValue() { + std::lock_guard lock(mtx); + return getValueImpl(); +} + +void Information::setValue(const InfoValue val) { + if (readonly) { + throw std::logic_error("Information is read-only!"); + } + + try { + std::lock_guard lock(mtx); + setValueImpl(val); + } catch (const std::bad_variant_access &e) { + throw std::invalid_argument( + "Invalid value, please provide an instance of the matching information " + "value class (value.__class__)"); + } +}; + +InfoQuality Information::getQuality() { + std::lock_guard lock(mtx); + return getQualityImpl(); +} + +void Information::setQuality(const InfoQuality val) { + if (readonly) { + throw std::logic_error("Information is read-only!"); + } + + try { + std::lock_guard lock(mtx); + setQualityImpl(val); + } catch (const std::bad_variant_access &e) { + throw std::invalid_argument( + "Invalid quality, please provide an instance of the matching " + "information value class (quality.__class__)"); + } +}; + +void Information::setReadonly() { + if (readonly) { + return; + } + std::lock_guard lock(mtx); + readonly = true; +}; + +void Information::setRecordedAt( + std::optional val) { + if (readonly) { + return; + } + std::lock_guard lock(mtx); + recorded_at = val; +} + +void Information::setProcessedAt(std::chrono::system_clock::time_point val) { + std::lock_guard lock(mtx); + processed_at = val; +} + +std::string Information::base_toString() const { + std::ostringstream oss; + oss << "recorded_at=" << TimePoint_toString(recorded_at) + << ", processed_at=" << TimePoint_toString(processed_at) + << ", readonly=" << bool_toString(readonly) << " at " << std::hex + << std::showbase << reinterpret_cast(this); + return oss.str(); +} + +std::string Information::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +std::string Command::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue SingleInfo::getValueImpl() const { return on; } + +void SingleInfo::setValueImpl(const InfoValue val) { on = std::get(val); } + +InfoQuality SingleInfo::getQualityImpl() const { return quality; } + +void SingleInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string SingleInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue SingleCmd::getValueImpl() const { return on; } + +void SingleCmd::setValueImpl(const InfoValue val) { on = std::get(val); } + +std::string SingleCmd::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue DoubleInfo::getValueImpl() const { return state; } + +void DoubleInfo::setValueImpl(const InfoValue val) { + state = std::get(val); +} + +InfoQuality DoubleInfo::getQualityImpl() const { return quality; } + +void DoubleInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string DoubleInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue DoubleCmd::getValueImpl() const { return state; } + +void DoubleCmd::setValueImpl(const InfoValue val) { + state = std::get(val); +} + +std::string DoubleCmd::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue StepInfo::getValueImpl() const { return position; } + +void StepInfo::setValueImpl(const InfoValue val) { + position = std::get(val); +} + +InfoQuality StepInfo::getQualityImpl() const { return quality; } + +void StepInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string StepInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue StepCmd::getValueImpl() const { return step; } + +void StepCmd::setValueImpl(const InfoValue val) { + step = std::get(val); +} + +std::string StepCmd::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue BinaryInfo::getValueImpl() const { return blob; } + +void BinaryInfo::setValueImpl(const InfoValue val) { + blob = std::get(val); +} + +InfoQuality BinaryInfo::getQualityImpl() const { return quality; } + +void BinaryInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string BinaryInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue BinaryCmd::getValueImpl() const { return blob; } + +void BinaryCmd::setValueImpl(const InfoValue val) { + blob = std::get(val); +} + +std::string BinaryCmd::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue NormalizedInfo::getValueImpl() const { return actual; } + +void NormalizedInfo::setValueImpl(const InfoValue val) { + actual = std::get(val); +} + +InfoQuality NormalizedInfo::getQualityImpl() const { return quality; } + +void NormalizedInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string NormalizedInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue NormalizedCmd::getValueImpl() const { return target; } + +void NormalizedCmd::setValueImpl(const InfoValue val) { + target = std::get(val); +} + +std::string NormalizedCmd::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue ScaledInfo::getValueImpl() const { return actual; } + +void ScaledInfo::setValueImpl(const InfoValue val) { + actual = std::get(val); +} + +InfoQuality ScaledInfo::getQualityImpl() const { return quality; } + +void ScaledInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string ScaledInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue ScaledCmd::getValueImpl() const { return target; } + +void ScaledCmd::setValueImpl(const InfoValue val) { + target = std::get(val); +} + +std::string ScaledCmd::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue ShortInfo::getValueImpl() const { return actual; } + +void ShortInfo::setValueImpl(const InfoValue val) { + actual = std::get(val); +} + +InfoQuality ShortInfo::getQualityImpl() const { return quality; } + +void ShortInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string ShortInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue ShortCmd::getValueImpl() const { return target; } + +void ShortCmd::setValueImpl(const InfoValue val) { + target = std::get(val); +} + +std::string ShortCmd::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue BinaryCounterInfo::getValueImpl() const { return counter; } + +void BinaryCounterInfo::setValueImpl(const InfoValue val) { + counter = std::get(val); +} + +InfoQuality BinaryCounterInfo::getQualityImpl() const { return quality; } + +void BinaryCounterInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string BinaryCounterInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue ProtectionEquipmentEventInfo::getValueImpl() const { return state; } + +void ProtectionEquipmentEventInfo::setValueImpl(const InfoValue val) { + state = std::get(val); +} + +InfoQuality ProtectionEquipmentEventInfo::getQualityImpl() const { + return quality; +} + +void ProtectionEquipmentEventInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string ProtectionEquipmentEventInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue ProtectionEquipmentStartEventsInfo::getValueImpl() const { + return events; +} + +void ProtectionEquipmentStartEventsInfo::setValueImpl(const InfoValue val) { + events = std::get(val); +} + +InfoQuality ProtectionEquipmentStartEventsInfo::getQualityImpl() const { + return quality; +} + +void ProtectionEquipmentStartEventsInfo::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string ProtectionEquipmentStartEventsInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue ProtectionEquipmentOutputCircuitInfo::getValueImpl() const { + return circuits; +} + +void ProtectionEquipmentOutputCircuitInfo::setValueImpl(const InfoValue val) { + circuits = std::get(val); +} + +InfoQuality ProtectionEquipmentOutputCircuitInfo::getQualityImpl() const { + return quality; +} + +void ProtectionEquipmentOutputCircuitInfo::setQualityImpl( + const InfoQuality val) { + quality = std::get(val); +} + +std::string ProtectionEquipmentOutputCircuitInfo::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} + +InfoValue StatusWithChangeDetection::getValueImpl() const { return status; } + +void StatusWithChangeDetection::setValueImpl(const InfoValue val) { + status = std::get(val); +} + +InfoQuality StatusWithChangeDetection::getQualityImpl() const { + return quality; +} + +void StatusWithChangeDetection::setQualityImpl(const InfoQuality val) { + quality = std::get(val); +} + +std::string StatusWithChangeDetection::toString() const { + std::ostringstream oss; + oss << ""; + return oss.str(); +} diff --git a/src/object/Information.h b/src/object/Information.h new file mode 100644 index 0000000..9ed333f --- /dev/null +++ b/src/object/Information.h @@ -0,0 +1,880 @@ +/** + * Copyright 2024-2024 Fraunhofer Institute for Applied Information Technology + * FIT + * + * This file is part of iec104-python. + * iec104-python is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iec104-python is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with iec104-python. If not, see . + * + * See LICENSE file for the complete license text. + * + * + * @file Information.h + * @brief abstract protocol information + * + * @package iec104-python + * @namespace object + * + * @authors Martin Unkel + * + */ + +#ifndef C104_OBJECT_INFORMATION_H +#define C104_OBJECT_INFORMATION_H + +#include +#include +#include + +#include + +#include "types.h" + +namespace Object { + +class Information : public std::enable_shared_from_this { +private: + std::mutex mtx; + +protected: + std::optional recorded_at; + std::chrono::system_clock::time_point processed_at; + bool readonly; + + [[nodiscard]] virtual InfoValue getValueImpl() const; + virtual void setValueImpl(InfoValue val); + + [[nodiscard]] virtual InfoQuality getQualityImpl() const; + virtual void setQualityImpl(InfoQuality val); + + std::string base_toString() const; + +public: + explicit Information(std::optional + recorded_at = std::nullopt, + bool readonly = false); + + [[nodiscard]] virtual InfoValue getValue(); + virtual void setValue(InfoValue val); + + [[nodiscard]] virtual InfoQuality getQuality(); + virtual void setQuality(InfoQuality val); + + [[nodiscard]] const std::optional & + getRecordedAt() const { + return recorded_at; + } + virtual void + setRecordedAt(std::optional val); + + [[nodiscard]] std::chrono::system_clock::time_point getProcessedAt() const { + return processed_at; + } + + void setProcessedAt(std::chrono::system_clock::time_point val); + + virtual void setReadonly(); + [[nodiscard]] bool isReadonly() const { return readonly; } + + [[nodiscard]] static std::string name() { return "Information"; } + + virtual std::string toString() const; +}; + +class Command : public Information { +public: + explicit Command(std::optional + recorded_at = std::nullopt, + bool readonly = false); + + [[nodiscard]] virtual bool isSelectable() const { return false; } + [[nodiscard]] virtual bool isSelect() const { return false; } + + [[nodiscard]] static std::string name() { return "Command"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief bool value, quality and optional recorded_at timestamp + */ +class SingleInfo : public Information { +protected: + bool on; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const bool on, const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(on, quality, recorded_at, false); + }; + + SingleInfo( + const bool on, const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), on(on), quality(quality){}; + + [[nodiscard]] bool isOn() const { return on; } + + [[nodiscard]] static std::string name() { return "SingleInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief bool value, select or execute flag, qualifier of command and optional + * recorded_at timestamp + */ +class SingleCmd : public Command { +protected: + bool on; + bool select; + CS101_QualifierOfCommand qualifier; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const bool on, + const CS101_QualifierOfCommand qualifier = CS101_QualifierOfCommand::NONE, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(on, false, qualifier, recorded_at, + false); + }; + + SingleCmd( + const bool on, const bool select, + const CS101_QualifierOfCommand qualifier, + const std::optional recorded_at, + bool readonly) + : Command(recorded_at, readonly), on(on), select(select), + qualifier(qualifier){}; + + [[nodiscard]] bool isOn() const { return on; } + + [[nodiscard]] bool isSelectable() const override { return true; } + + [[nodiscard]] bool isSelect() const override { return select; } + + [[nodiscard]] CS101_QualifierOfCommand getQualifier() const { + return qualifier; + } + + [[nodiscard]] static std::string name() { return "SingleCmd"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief bool value with transition, quality and optional recorded_at timestamp + */ +class DoubleInfo : public Information { +protected: + DoublePointValue state; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const DoublePointValue state, const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(state, quality, recorded_at, false); + }; + + DoubleInfo( + const DoublePointValue state, const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), state(state), quality(quality){}; + + [[nodiscard]] DoublePointValue getState() const { return state; } + + [[nodiscard]] static std::string name() { return "DoubleInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief bool value with transition, select or execute flag, qualifier of + * command and optional recorded_at timestamp + */ +class DoubleCmd : public Command { +protected: + DoublePointValue state; + bool select; + CS101_QualifierOfCommand qualifier; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const DoublePointValue state, + const CS101_QualifierOfCommand qualifier = CS101_QualifierOfCommand::NONE, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(state, false, qualifier, recorded_at, + false); + }; + + DoubleCmd( + const DoublePointValue state, const bool select, + const CS101_QualifierOfCommand qualifier, + const std::optional recorded_at, + bool readonly) + : Command(recorded_at, readonly), state(state), select(select), + qualifier(qualifier){}; + + [[nodiscard]] DoublePointValue getState() const { return state; } + + [[nodiscard]] bool isSelectable() const override { return true; } + + [[nodiscard]] bool isSelect() const override { return select; } + + [[nodiscard]] CS101_QualifierOfCommand getQualifier() const { + return qualifier; + } + + [[nodiscard]] static std::string name() { return "DoubleCmd"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief step position value with transition info, quality and optional + * recorded_at timestamp + */ +class StepInfo : public Information { +protected: + LimitedInt7 position; + bool transient; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const LimitedInt7 position, const bool transient = false, + const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(position, transient, quality, recorded_at, + false); + }; + + StepInfo( + const LimitedInt7 position, const bool transient, const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), position(position), + transient(transient), quality(quality){}; + + [[nodiscard]] const LimitedInt7 &getPosition() const { return position; } + + [[nodiscard]] bool isTransient() const { return transient; } + + [[nodiscard]] static std::string name() { return "StepInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief step direction, select or execute flag, qualifier of command and + * optional recorded_at timestamp + */ +class StepCmd : public Command { +protected: + StepCommandValue step; + bool select; + CS101_QualifierOfCommand qualifier; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const StepCommandValue direction, + const CS101_QualifierOfCommand qualifier = CS101_QualifierOfCommand::NONE, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(direction, false, qualifier, recorded_at, + false); + }; + + StepCmd( + const StepCommandValue direction, const bool select, + const CS101_QualifierOfCommand qualifier, + const std::optional recorded_at, + bool readonly) + : Command(recorded_at, readonly), step(direction), select(select), + qualifier(qualifier){}; + + [[nodiscard]] StepCommandValue getStep() const { return step; } + + [[nodiscard]] bool isSelectable() const override { return true; } + + [[nodiscard]] bool isSelect() const override { return select; } + + [[nodiscard]] CS101_QualifierOfCommand getQualifier() const { + return qualifier; + } + + [[nodiscard]] static std::string name() { return "StepCmd"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief binary value, quality and optional recorded_at timestamp + */ +class BinaryInfo : public Information { +protected: + Byte32 blob; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const Byte32 blob, const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(blob, quality, recorded_at, false); + } + + BinaryInfo( + const Byte32 blob, const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), blob(blob), quality(quality){}; + + [[nodiscard]] const Byte32 &getBlob() const { return blob; } + + [[nodiscard]] static std::string name() { return "BinaryInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief binary value and optional recorded_at timestamp + */ +class BinaryCmd : public Command { +protected: + Byte32 blob; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const Byte32 blob, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(blob, recorded_at, false); + }; + + BinaryCmd( + const Byte32 blob, + const std::optional recorded_at, + bool readonly) + : Command(recorded_at, readonly), blob(blob){}; + + [[nodiscard]] const Byte32 &getBlob() const { return blob; } + + [[nodiscard]] static std::string name() { return "BinaryCmd"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief NormalizedFloat value, quality and optional recorded_at timestamp + */ +class NormalizedInfo : public Information { +protected: + NormalizedFloat actual; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const NormalizedFloat actual, const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(actual, quality, recorded_at, + false); + } + + NormalizedInfo( + const NormalizedFloat actual, const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), actual(actual), quality(quality){}; + + [[nodiscard]] const NormalizedFloat &getActual() const { return actual; } + + [[nodiscard]] static std::string name() { return "NormalizedInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief NormalizedFloat value, select or execute flag, qualifier of set-point + * command and optional recorded_at timestamp + */ +class NormalizedCmd : public Command { +protected: + NormalizedFloat target; + bool select; + LimitedUInt7 qualifier; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const NormalizedFloat target, + const LimitedUInt7 qualifier = LimitedUInt7{0}, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(target, false, qualifier, + recorded_at, false); + }; + + NormalizedCmd( + const NormalizedFloat target, const bool select, + const LimitedUInt7 qualifier, + const std::optional recorded_at, + bool readonly) + : Command(recorded_at, readonly), target(target), select(select), + qualifier(qualifier){}; + + [[nodiscard]] const NormalizedFloat &getTarget() const { return target; } + + [[nodiscard]] bool isSelectable() const override { return true; } + + [[nodiscard]] bool isSelect() const override { return select; } + + [[nodiscard]] const LimitedUInt7 &getQualifier() const { return qualifier; } + + [[nodiscard]] static std::string name() { return "NormalizedCmd"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief scaled LimitedInt16 value, quality and optional recorded_at timestamp + */ +class ScaledInfo : public Information { +protected: + LimitedInt16 actual; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const LimitedInt16 actual, const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(actual, quality, recorded_at, false); + }; + + ScaledInfo( + const LimitedInt16 actual, const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), actual(actual), quality(quality){}; + + [[nodiscard]] const LimitedInt16 &getActual() const { return actual; } + + [[nodiscard]] static std::string name() { return "ScaledInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief LimitedInt16 value, select or execute flag, qualifier of set-point + * command and optional recorded_at timestamp + */ +class ScaledCmd : public Command { +protected: + LimitedInt16 target; + bool select; + LimitedUInt7 qualifier; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const LimitedInt16 target, const LimitedUInt7 qualifier = LimitedUInt7{0}, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(target, false, qualifier, recorded_at, + false); + }; + + ScaledCmd( + const LimitedInt16 target, const bool select, + const LimitedUInt7 qualifier, + const std::optional recorded_at, + bool readonly) + : Command(recorded_at, readonly), target(target), select(select), + qualifier(qualifier){}; + + [[nodiscard]] const LimitedInt16 &getTarget() const { return target; } + + [[nodiscard]] bool isSelectable() const override { return true; } + + [[nodiscard]] bool isSelect() const override { return select; } + + [[nodiscard]] const LimitedUInt7 &getQualifier() const { return qualifier; } + + [[nodiscard]] static std::string name() { return "ScaledCmd"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief float value, quality and optional recorded_at timestamp + */ +class ShortInfo : public Information { +protected: + float actual; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const float actual, const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(actual, quality, recorded_at, false); + }; + + ShortInfo( + const float actual, const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), actual(actual), quality(quality){}; + + [[nodiscard]] float getActual() const { return actual; } + + [[nodiscard]] static std::string name() { return "ShortInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief float value, select or execute flag, qualifier of set-point command + * and optional recorded_at timestamp + */ +class ShortCmd : public Command { +protected: + float target; + bool select; + LimitedUInt7 qualifier; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const float target, const LimitedUInt7 qualifier = LimitedUInt7{0}, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(target, false, qualifier, recorded_at, + false); + }; + + ShortCmd( + const float target, const bool select, const LimitedUInt7 qualifier, + const std::optional recorded_at, + bool readonly) + : Command(recorded_at, readonly), target(target), select(select), + qualifier(qualifier){}; + + [[nodiscard]] float getTarget() const { return target; } + + [[nodiscard]] bool isSelectable() const override { return true; } + + [[nodiscard]] bool isSelect() const override { return select; } + + [[nodiscard]] const LimitedUInt7 &getQualifier() const { return qualifier; } + + [[nodiscard]] static std::string name() { return "ShortCmd"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief binary counter value with read sequence number, quality and optional + * recorded_at timestamp + */ +class BinaryCounterInfo : public Information { +protected: + int32_t counter; + LimitedUInt5 sequence; + BinaryCounterQuality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const int32_t counter, const LimitedUInt5 sequence = LimitedUInt5{0}, + const BinaryCounterQuality quality = BinaryCounterQuality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(counter, sequence, quality, + recorded_at, false); + }; + + BinaryCounterInfo( + const int32_t counter, const LimitedUInt5 sequence, + const BinaryCounterQuality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), counter(counter), + sequence(sequence), quality(quality){}; + + [[nodiscard]] int32_t getCounter() const { return counter; } + + [[nodiscard]] const LimitedUInt5 &getSequence() const { return sequence; } + + [[nodiscard]] static std::string name() { return "BinaryCounterInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief event state info with elapsed_ms, quality and optional recorded_at + * timestamp + */ +class ProtectionEquipmentEventInfo : public Information { +protected: + EventState state; + LimitedUInt16 elapsed_ms; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const EventState state, const LimitedUInt16 elapsed_ms = LimitedUInt16{0}, + const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared( + state, elapsed_ms, quality, recorded_at, false); + }; + + ProtectionEquipmentEventInfo( + const EventState state, const LimitedUInt16 elapsed_ms, + const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), state(state), + elapsed_ms(elapsed_ms), quality(quality){}; + + [[nodiscard]] EventState getState() const { return state; } + + [[nodiscard]] LimitedUInt16 getElapsed_ms() const { return elapsed_ms; } + + [[nodiscard]] static std::string name() { return "ProtectionEventInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief start events info with relay_duration_ms, quality and optional + * recorded_at timestamp + */ +class ProtectionEquipmentStartEventsInfo : public Information { +protected: + StartEvents events; + LimitedUInt16 relay_duration_ms; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr + create(const StartEvents events, + const LimitedUInt16 relay_duration_ms = LimitedUInt16{0}, + const Quality quality = Quality::None, + const std::optional + recorded_at = std::nullopt) { + return std::make_shared( + events, relay_duration_ms, quality, recorded_at, false); + }; + + ProtectionEquipmentStartEventsInfo( + const StartEvents events, const LimitedUInt16 relay_duration_ms, + const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), events(events), + relay_duration_ms(relay_duration_ms), quality(quality){}; + + [[nodiscard]] StartEvents getEvents() const { return events; } + + [[nodiscard]] LimitedUInt16 getRelayDuration_ms() const { + return relay_duration_ms; + } + + [[nodiscard]] static std::string name() { return "ProtectionStartInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief output circuits info with replay_operating_ms, quality and optional + * recorded_at timestamp + */ +class ProtectionEquipmentOutputCircuitInfo : public Information { +protected: + OutputCircuits circuits; + LimitedUInt16 relay_operating_ms; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr + create(const OutputCircuits circuits, + const LimitedUInt16 relay_operating_ms = LimitedUInt16{0}, + const Quality quality = Quality::None, + const std::optional + recorded_at = std::nullopt) { + return std::make_shared( + circuits, relay_operating_ms, quality, recorded_at, false); + }; + + ProtectionEquipmentOutputCircuitInfo( + const OutputCircuits circuits, const LimitedUInt16 relay_operating_ms, + const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), circuits(circuits), + relay_operating_ms(relay_operating_ms), quality(quality){}; + + [[nodiscard]] OutputCircuits getCircuits() const { return circuits; } + + [[nodiscard]] LimitedUInt16 getRelayOperating_ms() const { + return relay_operating_ms; + } + + [[nodiscard]] static std::string name() { return "ProtectionCircuitInfo"; } + + [[nodiscard]] std::string toString() const override; +}; + +/** + * @brief 16 packed bool values with change indicator, quality and optional + * recorded_at timestamp + */ +class StatusWithChangeDetection : public Information { +protected: + FieldSet16 status; + FieldSet16 changed; + Quality quality; + + [[nodiscard]] InfoValue getValueImpl() const override; + void setValueImpl(InfoValue val) override; + + [[nodiscard]] InfoQuality getQualityImpl() const override; + void setQualityImpl(InfoQuality val) override; + +public: + [[nodiscard]] static std::shared_ptr create( + const FieldSet16 status, const FieldSet16 changed = FieldSet16{}, + const Quality quality = Quality::None, + const std::optional recorded_at = + std::nullopt) { + return std::make_shared(status, changed, quality, + recorded_at, false); + }; + + StatusWithChangeDetection( + const FieldSet16 status, const FieldSet16 changed, const Quality quality, + const std::optional recorded_at, + bool readonly) + : Information(recorded_at, readonly), status(status), changed(changed), + quality(quality){}; + + [[nodiscard]] FieldSet16 getStatus() const { return status; } + + [[nodiscard]] FieldSet16 getChanged() const { return changed; } + + [[nodiscard]] static std::string name() { return "StatusAndChanged"; } + + [[nodiscard]] std::string toString() const override; +}; + +}; // namespace Object +#endif // C104_OBJECT_INFORMATION_H diff --git a/src/object/Station.cpp b/src/object/Station.cpp index 486ff34..ab733dd 100644 --- a/src/object/Station.cpp +++ b/src/object/Station.cpp @@ -30,6 +30,7 @@ */ #include "object/Station.h" +#include "Client.h" #include "Server.h" #include "remote/Connection.h" @@ -94,13 +95,12 @@ Station::getPoint(const std::uint_fast32_t informationObjectAddress) { return {nullptr}; } -std::shared_ptr -Station::addPoint(const std::uint_fast32_t informationObjectAddress, - const IEC60870_5_TypeID type, - const std::uint_fast32_t reportInterval_ms, - const std::uint_fast32_t relatedInformationObjectAddress, - const bool relatedInformationObjectAutoReturn, - const CommandTransmissionMode commandMode) { +std::shared_ptr Station::addPoint( + const std::uint_fast32_t informationObjectAddress, + const IEC60870_5_TypeID type, const std::uint_fast16_t reportInterval_ms, + const std::optional relatedInformationObjectAddress, + const bool relatedInformationObjectAutoReturn, + const CommandTransmissionMode commandMode) { if (getPoint(informationObjectAddress)) { return {nullptr}; } @@ -109,14 +109,21 @@ Station::addPoint(const std::uint_fast32_t informationObjectAddress, "add_point] " + std::string(TypeID_toString(type)) + " | IOA " + std::to_string(informationObjectAddress)); + // forward tickRate_ms + uint_fast16_t tickRate_ms = 0; + if (auto sv = getServer()) { + tickRate_ms = sv->getTickRate_ms(); + } else if (auto co = getConnection()) { + if (auto cl = co->getClient()) { + tickRate_ms = cl->getTickRate_ms(); + } + } + std::scoped_lock const lock(points_mutex); - auto point = - DataPoint::create(informationObjectAddress, type, shared_from_this(), - reportInterval_ms, relatedInformationObjectAddress, - relatedInformationObjectAutoReturn, commandMode); - // auto point = new DataPoint(informationObjectAddress, type, this, - // reportInterval_ms, relatedInformationObjectAddress, - // relatedInformationObjectAutoReturn); + auto point = DataPoint::create( + informationObjectAddress, type, shared_from_this(), reportInterval_ms, + relatedInformationObjectAddress, relatedInformationObjectAutoReturn, + commandMode, tickRate_ms); points.push_back(point); return point; diff --git a/src/object/Station.h b/src/object/Station.h index aa49402..0709815 100644 --- a/src/object/Station.h +++ b/src/object/Station.h @@ -66,13 +66,13 @@ class Station : public std::enable_shared_from_this { std::shared_ptr st_connection); /// @brief unique common address of this station - std::uint_fast16_t commonAddress{0}; + const std::uint_fast16_t commonAddress{0}; /// @brief server object reference (only local station) - std::weak_ptr server; + const std::weak_ptr server; /// @brief remote connection object reference (only remote station) - std::weak_ptr connection; + const std::weak_ptr connection; /// @brief child DataPoint objects (owned by this Station) DataPointVector points{}; @@ -117,7 +117,8 @@ class Station : public std::enable_shared_from_this { * @param informationObjectAddress information object address * @param type iec60870-5-104 information type * @param reportInterval_ms auto reporting interval - * @param relatedInformationObjectAddress related information object address + * @param relatedInformationObjectAddress related information object address, + * if any * @param relatedInformationObjectAutoReturn auto transmit related point on * command * @param commandMode command transmission mode (direct or select-and-execute) @@ -125,24 +126,27 @@ class Station : public std::enable_shared_from_this { */ std::shared_ptr addPoint(std::uint_fast32_t informationObjectAddress, IEC60870_5_TypeID type, - std::uint_fast32_t reportInterval_ms = 0, - std::uint_fast32_t relatedInformationObjectAddress = 0, + std::uint_fast16_t reportInterval_ms = 0, + std::optional relatedInformationObjectAddress = + std::nullopt, bool relatedInformationObjectAutoReturn = false, CommandTransmissionMode commandMode = DIRECT_COMMAND); bool isLocal(); public: - inline friend std::ostream &operator<<(std::ostream &os, Station &s) { - os << std::endl - << "+------------------------------+" << '\n' - << "| DUMP Asset/Station |" << '\n' - << "+------------------------------+" << '\n' - << "|" << std::setw(19) << "ASDU/CA: " << std::setw(10) - << std::to_string(s.commonAddress) << " |" << '\n' - << "|------------------------------+" << std::endl; - return os; - } + std::string toString() const { + size_t len = 0; + { + std::scoped_lock const lock(points_mutex); + len = points.size(); + } + std::ostringstream oss; + oss << "<104.Station common_address=" << std::to_string(commonAddress) + << ", #points=" << std::to_string(len) << " at " << std::hex + << std::showbase << reinterpret_cast(this) << ">"; + return oss.str(); + }; }; /** diff --git a/src/python.cpp b/src/python.cpp index b32dc06..03e617d 100644 --- a/src/python.cpp +++ b/src/python.cpp @@ -35,30 +35,122 @@ #include "Client.h" #include "Server.h" +#include +#include #include + #ifdef VERSION_INFO #define PY_MODULE(name, var) PYBIND11_MODULE(name, var) #else +#define VERSION_INFO "embedded" #include #define PY_MODULE(name, var) PYBIND11_EMBEDDED_MODULE(name, var) #endif -#define STRINGIFY(x) #x -#define MACRO_STRINGIFY(x) STRINGIFY(x) - using namespace pybind11::literals; +// set UNBUFFERED mode for correct order of stdout flush between python and c++ +struct EnvironmentInitializer { + EnvironmentInitializer() { +#if defined(_WIN32) || defined(_WIN64) + _putenv("PYTHONUNBUFFERED=1"); +#else + // Use setenv on Linux + setenv("PYTHONUNBUFFERED", "1", 1); +#endif + } +}; + +// Initialize the environment variable before main() is called +static EnvironmentInitializer initializer; + // @todo Ubuntu 18 x64, Ubuntu 20 x64, arm7v32, later: arm aarch64 -PyObject * +// Bind Number with Template +template +void bind_Number(py::module &m, const std::string &name, + const bool with_float = false) { + auto py_number = + py::class_>(m, name.c_str()) + .def(py::init()) + // Overloading operators with different types + .def(py::self + int()) + .def(py::self - int()) + .def(py::self * int()) + .def(py::self / int()) + .def(py::self += int()) + .def(py::self -= int()) + .def(py::self *= int()) + .def(py::self /= int()) + .def("__int__", [](T &a) { return static_cast(a.get()); }) + .def("__float__", [](T &a) { return static_cast(a.get()); }) + .def("__str__", [name](T &a) { return std::to_string(a.get()); }) + .def("__repr__", [name](const T &a) { + return ""; + }); + + if (with_float) { + py_number.def(py::init()) + .def_property_readonly("min", &T::getMin, + "float: minimum value (read-only)") + .def_property_readonly("max", &T::getMax, + "float: maximum value (read-only)") + .def(py::self + float()) + .def(py::self - float()) + .def(py::self * float()) + .def(py::self / float()) + .def(py::self += float()) + .def(py::self -= float()) + .def(py::self *= float()) + .def(py::self /= float()); + } else { + py_number + .def_property_readonly("min", &T::getMin, + "int: minimum value (read-only)") + .def_property_readonly("max", &T::getMax, + "int: maximum value (read-only)"); + } +} + +template +void bind_BitFlags_ops(py::class_ &py_bit_enum, std::string (*fn)(const T &), + bool is_quality = false) { + py_bit_enum.def(py::self & py::self) + .def(py::self | py::self) + .def(py::self ^ py::self) + .def(py::self == py::self) + .def(py::self != py::self) + .def(py::self &= py::self) + .def(py::self |= py::self) + .def(py::self ^= py::self) + .def("__contains__", + [](const T &mode, const T &flag) { return test(mode, flag); }) + .def( + "is_any", [](const T &mode) { return is_any(mode); }, + "test if there are any flags set"); + + if (is_quality) { + py_bit_enum.def( + "is_good", [](const T &mode) { return is_none(mode); }, + "test if there are no flags set"); + } else { + py_bit_enum.def( + "is_none", [](const T &mode) { return is_none(mode); }, + "test if there are no flags set"); + } + + py_bit_enum.attr("__str__") = + py::cpp_function(fn, py::name("__str__"), py::is_method(py_bit_enum)); + py_bit_enum.attr("__repr__") = + py::cpp_function(fn, py::name("__repr__"), py::is_method(py_bit_enum)); +} + +py::bytes IncomingMessage_getRawBytes(Remote::Message::IncomingMessage *message) { unsigned char *msg = message->getRawBytes(); unsigned char msgSize = 2 + msg[1]; - PyObject *pymemview = - PyMemoryView_FromMemory((char *)msg, msgSize, PyBUF_READ); - - return PyBytes_FromObject(pymemview); + return {reinterpret_cast(msg), msgSize}; } std::string explain_bytes(const py::bytes &obj) { @@ -83,6 +175,30 @@ py::dict explain_bytes_dict(const py::bytes &obj) { (unsigned char)buffer->len); } +py::object convert_timestamp_to_datetime(const uint64_t timestamp_ms) { + // Convert milliseconds to seconds and microseconds + std::time_t seconds = timestamp_ms / 1000; + std::size_t milliseconds = timestamp_ms % 1000; + + // Convert the time_t to tm structure + std::tm *time_info = std::gmtime(&seconds); + + // Import datetime module + py::module_ datetime = py::module_::import("datetime"); + py::object datetime_class = datetime.attr("datetime"); + + // Create datetime object + return datetime_class( + time_info->tm_year + 1900, // Year + time_info->tm_mon + 1, // Month (tm_mon is in range [0, 11]) + time_info->tm_mday, // Day + time_info->tm_hour, // Hour + time_info->tm_min, // Minute + time_info->tm_sec, // Second + milliseconds * 1000 // Microsecond + ); +} + PY_MODULE(c104, m) { #ifdef _WIN32 system("chcp 65001 > nul"); @@ -92,21 +208,6 @@ PY_MODULE(c104, m) { options.disable_function_signatures(); options.disable_enum_members_docstring(); - py::enum_( - m, "Data", - "This enum contains all available information types for a datapoint") - .value("SINGLE", SINGLE) - .value("DOUBLE", DOUBLE) - .value("STEP", STEP) - .value("BITS", BITS) - .value("NORMALIZED", NORMALIZED) - .value("SCALED", SCALED) - .value("SHORT", SHORT) - .value("INTEGRATED", INTEGRATED) - .value("NORMALIZED_PARAMETER", NORMALIZED_PARAMETER) - .value("SCALED_PARAMETER", SCALED_PARAMETER) - .value("SHORT_PARAMETER", SHORT_PARAMETER); - py::enum_(m, "Type", "This enum contains all valid IEC60870 message " "types to interpret or create points.") @@ -254,7 +355,7 @@ PY_MODULE(c104, m) { .value("NONE", CS101_QualifierOfCommand::NONE) .value("SHORT_PULSE", CS101_QualifierOfCommand::SHORT_PULSE) .value("LONG_PULSE", CS101_QualifierOfCommand::LONG_PULSE) - .value("CONTINUOUS", CS101_QualifierOfCommand::CONTINUOUS); + .value("PERSISTENT", CS101_QualifierOfCommand::PERSISTENT); py::enum_( m, "Umc", @@ -271,23 +372,37 @@ PY_MODULE(c104, m) { .value("UNIMPLEMENTED_GROUP", UNIMPLEMENTED_GROUP); py::enum_( - m, "Init", "This enum contains all connection init command options.") - .value("ALL", INIT_ALL) - .value("INTERROGATION", INIT_INTERROGATION) - .value("CLOCK_SYNC", INIT_CLOCK_SYNC) - .value("NONE", INIT_NONE); + m, "Init", + "This enum contains all connection init command options. Everytime the " + "connection is established the client will behave as follows:") + .value("ALL", INIT_ALL, + "Unmute the connection, send an interrogation command and after " + "that send a clock synchronization command") + .value("INTERROGATION", INIT_INTERROGATION, + "Unmute the connection and send an interrogation command") + .value("CLOCK_SYNC", INIT_CLOCK_SYNC, + "Unmute the connection and send a clock synchronization command") + .value("NONE", INIT_NONE, + "Unmute the connection, but without triggering a command") + .value("MUTED", INIT_MUTED, + "Act as a redundancy client without active communication"); py::enum_(m, "ConnectionState", "This enum contains all link states for " "connection state machine behaviour.") - .value("CLOSED", CLOSED) - .value("CLOSED_AWAIT_OPEN", CLOSED_AWAIT_OPEN) - .value("CLOSED_AWAIT_RECONNECT", CLOSED_AWAIT_RECONNECT) - .value("OPEN_MUTED", OPEN_MUTED) - .value("OPEN_AWAIT_INTERROGATION", OPEN_AWAIT_INTERROGATION) - .value("OPEN_AWAIT_CLOCK_SYNC", OPEN_AWAIT_CLOCK_SYNC) - .value("OPEN", OPEN) - .value("OPEN_AWAIT_CLOSED", OPEN_AWAIT_CLOSED); + .value("CLOSED", CLOSED, "The connection is closed") + .value("CLOSED_AWAIT_OPEN", CLOSED_AWAIT_OPEN, + "The connection is dialing") + .value("CLOSED_AWAIT_RECONNECT", CLOSED_AWAIT_RECONNECT, + "The connection will retry dialing soon") + .value("OPEN", OPEN, + "The connection is established and active/unmuted, with no init " + "commands outstanding") + .value("OPEN_MUTED", OPEN_MUTED, + "The connection is established and inactive/muted, with no init " + "commands outstanding") + .value("OPEN_AWAIT_CLOSED", OPEN_AWAIT_CLOSED, + "The connection is about to close soon"); py::enum_( m, "ResponseState", @@ -302,8 +417,13 @@ PY_MODULE(c104, m) { m, "CommandMode", "This enum contains all command transmission modes a client" "may use to send commands.") - .value("DIRECT", DIRECT_COMMAND) - .value("SELECT_AND_EXECUTE", SELECT_AND_EXECUTE_COMMAND); + .value("DIRECT", DIRECT_COMMAND, + "The value can be set without any selection procedure") + .value("SELECT_AND_EXECUTE", SELECT_AND_EXECUTE_COMMAND, + "The client has to send a select command first to get exclusive " + "access to the points value and can then send an updated value " + "command. The selection automatically ends by receiving the value " + "command."); py::enum_( m, "Qoi", @@ -327,6 +447,14 @@ PY_MODULE(c104, m) { .value("GROUP_15", QOI_GROUP_15) .value("GROUP_16", QOI_GROUP_16); + py::enum_( + m, "Coi", + "This enum contains all valid IEC60870 cause of initialization values.") + .value("LOCAL_POWER_ON", CS101_CauseOfInitialization::LOCAL_POWER_ON) + .value("LOCAL_MANUAL_RESET", + CS101_CauseOfInitialization::LOCAL_MANUAL_RESET) + .value("REMOTE_RESET", CS101_CauseOfInitialization::REMOTE_RESET); + py::enum_( m, "Step", "This enum contains all valid IEC60870 step command values to interpret " @@ -375,133 +503,118 @@ PY_MODULE(c104, m) { .value("Callback", Debug::Callback) .value("Gil", Debug::Gil) .value("All", Debug::All) - .def(py::init([]() { return Debug::None; })) - .def("__str__", &Debug_toString, "convert to human readable string") - .def("__repr__", &Debug_toString, "convert to human readable string") - .def( - "__and__", [](const Debug &a, Debug b) { return a & b; }, - py::is_operator()) - .def( - "__or__", [](const Debug &a, Debug b) { return a | b; }, - py::is_operator()) - .def( - "__xor__", [](const Debug &a, Debug b) { return a ^ b; }, - py::is_operator()) - .def( - "__invert__", [](const Debug &a) { return ~a; }, - py::is_operator()) - .def( - "__iand__", - [](Debug &a, Debug b) { - a &= b; - return a; - }, - py::is_operator()) - .def( - "__ior__", - [](Debug &a, Debug b) { - a |= b; - return a; - }, - py::is_operator()) - .def( - "__ixor__", - [](Debug &a, Debug b) { - a ^= b; - return a; - }, - py::is_operator()) - .def( - "__contains__", - [](const Debug &mode, const Debug &flag) { - return test(mode, flag); - }, - py::is_operator()) - .def( - "is_any", [](const Debug &mode) { return is_any(mode); }, - "test if any debug mode is enabled") - .def( - "is_none", [](const Debug &mode) { return is_none(mode); }, - "test if no debug mode is enabled"); - - py_debug.attr("__str__") = py::cpp_function( - &Debug_toString, py::name("__str__"), py::is_method(py_debug)); - py_debug.attr("__repr__") = py::cpp_function( - &Debug_toString, py::name("__repr__"), py::is_method(py_debug)); + .def(py::init([]() { return Debug::None; })); + bind_BitFlags_ops(py_debug, &Debug_toString); auto py_quality = py::enum_(m, "Quality", "This enum contains all quality issue bits to " "interpret and manipulate measurement quality.") .value("Overflow", Quality::Overflow) - .value("Reserved", Quality::Reserved) + // .value("Reserved", Quality::Reserved) .value("ElapsedTimeInvalid", Quality::ElapsedTimeInvalid) .value("Blocked", Quality::Blocked) .value("Substituted", Quality::Substituted) .value("NonTopical", Quality::NonTopical) .value("Invalid", Quality::Invalid) - .def(py::init([]() { return Quality::None; })) - .def("__str__", &Quality_toString) - .def("__repr__", &Quality_toString) - .def( - "__and__", [](const Quality &a, Quality b) { return a & b; }, - py::is_operator()) - .def( - "__rand__", [](const Quality &a, Quality b) { return a & b; }, - py::is_operator()) - .def( - "__or__", [](const Quality &a, Quality b) { return a | b; }, - py::is_operator()) - .def( - "__ror__", [](const Quality &a, Quality b) { return a | b; }, - py::is_operator()) - .def( - "__xor__", [](const Quality &a, Quality b) { return a ^ b; }, - py::is_operator()) - .def( - "__rxor__", [](const Quality &a, Quality b) { return a ^ b; }, - py::is_operator()) - .def( - "__invert__", [](const Quality &a) { return ~a; }, - py::is_operator()) - .def( - "__iand__", - [](Quality &a, Quality b) { - a &= b; - return a; - }, - py::is_operator()) - .def( - "__ior__", - [](Quality &a, Quality b) { - a |= b; - return a; - }, - py::is_operator()) - .def( - "__ixor__", - [](Quality &a, Quality b) { - a ^= b; - return a; - }, - py::is_operator()) - .def( - "__contains__", - [](const Quality &mode, const Quality &flag) { - return test(mode, flag); - }, - py::is_operator()) - .def( - "is_any", [](const Quality &mode) { return is_any(mode); }, - "test if there are any limitations in terms of quality") - .def( - "is_good", [](const Quality &mode) { return is_none(mode); }, - "test if there are no limitations in terms of quality"); - - py_quality.attr("__str__") = py::cpp_function( - &Quality_toString, py::name("__str__"), py::is_method(py_quality)); - py_quality.attr("__repr__") = py::cpp_function( - &Quality_toString, py::name("__repr__"), py::is_method(py_quality)); + .def(py::init([]() { return Quality::None; })); + bind_BitFlags_ops(py_quality, &Quality_toString, true); + + auto py_bcrquality = + py::enum_( + m, "BinaryCounterQuality", + "This enum contains all binary counter quality issue bits to " + "interpret and manipulate counter quality.") + .value("Adjusted", BinaryCounterQuality::Adjusted) + .value("Carry", BinaryCounterQuality::Carry) + .value("Invalid", BinaryCounterQuality::Invalid) + .def(py::init([]() { return BinaryCounterQuality::None; })); + bind_BitFlags_ops(py_bcrquality, &BinaryCounterQuality_toString, true); + + auto py_start_events = + py::enum_( + m, "StartEvents", + "This enum contains all StartEvents issue bits to " + "interpret and manipulate protection equipment messages.") + .value("General", StartEvents::General) + .value("PhaseL1", StartEvents::PhaseL1) + .value("PhaseL2", StartEvents::PhaseL2) + .value("PhaseL3", StartEvents::PhaseL3) + .value("InEarthCurrent", StartEvents::InEarthCurrent) + .value("ReverseDirection", StartEvents::ReverseDirection) + .def(py::init([]() { return StartEvents::None; })); + bind_BitFlags_ops(py_start_events, &StartEvents_toString); + + auto py_output_circuit_info = + py::enum_( + m, "OutputCircuits", + "This enum contains all Output Circuit bits to " + "interpret and manipulate protection equipment messages.") + .value("General", OutputCircuits::General) + .value("PhaseL1", OutputCircuits::PhaseL1) + .value("PhaseL2", OutputCircuits::PhaseL2) + .value("PhaseL3", OutputCircuits::PhaseL3) + .def(py::init([]() { return OutputCircuits::None; })); + bind_BitFlags_ops(py_output_circuit_info, &OutputCircuits_toString); + + auto py_field_set = + py::enum_( + m, "PackedSingle", + "This enum contains all State bits to " + "interpret and manipulate status with change detection messages.") + .value("I0", FieldSet16::I0) + .value("I1", FieldSet16::I1) + .value("I2", FieldSet16::I2) + .value("I3", FieldSet16::I3) + .value("I4", FieldSet16::I4) + .value("I5", FieldSet16::I5) + .value("I6", FieldSet16::I6) + .value("I7", FieldSet16::I7) + .value("I8", FieldSet16::I8) + .value("I9", FieldSet16::I9) + .value("I10", FieldSet16::I10) + .value("I11", FieldSet16::I11) + .value("I12", FieldSet16::I12) + .value("I13", FieldSet16::I13) + .value("I14", FieldSet16::I14) + .value("I15", FieldSet16::I15) + .def(py::init([]() { return FieldSet16::None; })); + bind_BitFlags_ops(py_field_set, &FieldSet16_toString); + + bind_Number(m, "UInt5"); + bind_Number(m, "UInt7"); + bind_Number(m, "UInt16"); + bind_Number(m, "Int7"); + bind_Number(m, "Int16"); + bind_Number(m, "NormalizedFloat", true); + + py::class_(m, "Byte32") + .def(py::init()) + .def(py::init([](const py::bytes &byte_obj) { + py::buffer_info info(py::buffer(byte_obj).request()); + + if (info.size > sizeof(uint32_t)) { + throw std::runtime_error( + "Invalid size of bytes object. Expected 4 bytes, got " + + std::to_string(info.size) + "."); + } + uint32_t value = 0; + + // Copy only the available bytes + std::memcpy(&value, info.ptr, info.size); + + return Byte32(value); + })) + .def("__bytes__", + [](const Byte32 &b) { + uint32_t value = b.get(); + return py::bytes(reinterpret_cast(&value), + sizeof(value)); + }) + .def("__str__", &Byte32_toString) + .def("__repr__", [](const Byte32 &b) { + return ""; + }); py::class_>( @@ -613,9 +726,9 @@ PY_MODULE(c104, m) { Parameters ---------- - min: :ref:`c104.TlsVersion` + min: c104.TlsVersion minimum required TLS version for communication - max: :ref:`c104.TlsVersion` + max: c104.TlsVersion maximum allowed TLS version for communication Returns @@ -628,128 +741,8 @@ PY_MODULE(c104, m) { >>> tls.set_version(min=c104.TLSVersion.TLS_1_2, max=c104.TLSVersion.TLS_1_2) )def", "min"_a = TLS_VERSION_NOT_SELECTED, - "max"_a = TLS_VERSION_NOT_SELECTED); - - // todo remove deprecated in 2.x - m.def( - "add_client", - [](const std::uint_fast32_t tick_rate_ms, - const std::uint_fast32_t timeout_ms, - std::shared_ptr transport_security) - -> std::shared_ptr { - std::cerr << "[c104.add_client] deprecated: simple create a new " - "c104.Client() object with relevant arguments" - << std::endl; - return Client::create(tick_rate_ms, timeout_ms, - std::move(transport_security)); - }, - R"def( - add_client(tick_rate_ms: int = 1000, command_timeout_ms: int = 1000, transport_security: Optional[c104.TransportSecurity] = None) -> None - - create a new 104er client - - Parameters - ---------- - tick_rate_ms: int - client thread update interval - command_timeout_ms: int - time to wait for a command response - transport_security: :ref:`c104.TransportSecurity` - TLS configuration object - - Warning - ------- - Deprecated: Use the default constructor c104.Client(...) instead. Will be removed in 2.x -)def", - "tick_rate_ms"_a = 1000, "command_timeout_ms"_a = 1000, - "transport_security"_a = nullptr); - // todo remove deprecated in 2.x - m.def( - "remove_client", - [](std::shared_ptr instance) { - std::cerr - << "[c104.remove_client] deprecated: simple discard the variable" - << std::endl; - }, - R"def( - remove_client(instance: c104.Client) -> None - - destroy and free a 104er client - - Parameters - ---------- - instance: :ref:`c104.Client` - client instance - - Warning - ------- - Deprecated: Simple remove all references to the instance instead. Will be removed in 2.x -)def", - "instance"_a); - - // todo remove deprecated in 2.x - m.def( - "add_server", - [](const std::string &bind_ip, const std::uint_fast16_t tcp_port, - const std::uint_fast32_t tick_rate_ms, - const uint_fast8_t max_open_connections, - std::shared_ptr transport_security) - -> std::shared_ptr { - std::cerr << "[c104.add_server] deprecated: simple create a new " - "c104.Server() object with relevant arguments" - << std::endl; - return Server::create(bind_ip, tcp_port, tick_rate_ms, - max_open_connections, - std::move(transport_security)); - }, - R"def( - add_server(self: c104.Server, ip: str = "0.0.0.0", port: int = 2404, tick_rate_ms: int = 1000, max_connections: int = 0, transport_security: Optional[c104.TransportSecurity] = None) -> None - - create a new 104er server - - Parameters - ------- - ip: str - listening server ip address - port:int - listening server port - tick_rate_ms: int - server thread update interval - max_connections: int - maximum number of clients allowed to connect - transport_security: :ref:`c104.TransportSecurity` - TLS configuration object - - Warning - ------- - Deprecated: Use the default constructor c104.Server(...) instead. Will be removed in 2.x -)def", - "ip"_a = "0.0.0.0", "port"_a = IEC_60870_5_104_DEFAULT_PORT, - "tick_rate_ms"_a = 1000, "max_connections"_a = 0, - "transport_security"_a = nullptr); - // todo remove deprecated in 2.x - m.def( - "remove_server", - [](std::shared_ptr instance) -> void { - std::cerr - << "[c104.remove_server] deprecated: simple discard the variable" - << std::endl; - }, - R"def( - remove_server(instance: c104.Server) -> None - - destroy and free a 104er server - - Parameters - ---------- - instance: :ref:`c104.Server` - server instance - - Warning - ------- - Deprecated: Simple remove all references to the instance instead. Will be removed in 2.x -)def", - "instance"_a); + "max"_a = TLS_VERSION_NOT_SELECTED) + .def("__repr__", &Remote::TransportSecurity::toString); m.def("explain_bytes", &explain_bytes, R"def( explain_bytes(apdu: bytes) -> str @@ -805,7 +798,7 @@ PY_MODULE(c104, m) { Parameters ---------- - mode: :ref:`c104.Debug` + mode: c104.Debug debug mode bitset Example @@ -820,7 +813,7 @@ PY_MODULE(c104, m) { Returns ---------- - :ref:`c104.Debug` + c104.Debug debug mode bitset Example @@ -835,7 +828,7 @@ PY_MODULE(c104, m) { Parameters ---------- - mode: :ref:`c104.Debug` + mode: c104.Debug debug mode bitset Example @@ -852,7 +845,7 @@ PY_MODULE(c104, m) { Parameters ---------- - mode: :ref:`c104.Debug` + mode: c104.Debug debug mode bitset Example @@ -868,7 +861,7 @@ PY_MODULE(c104, m) { "This class represents a local client and provides access to meta " "information and connected remote servers") .def(py::init(&Client::create), R"def( - __init__(self: c104.Client, tick_rate_ms: int = 1000, command_timeout_ms: int = 1000, transport_security: Optional[c104.TransportSecurity] = None) -> None + __init__(self: c104.Client, tick_rate_ms: int = 100, command_timeout_ms: int = 100, transport_security: Optional[c104.TransportSecurity] = None) -> None create a new 104er client @@ -878,27 +871,41 @@ PY_MODULE(c104, m) { client thread update interval command_timeout_ms: int time to wait for a command response - transport_security: :ref:`c104.TransportSecurity` + transport_security: c104.TransportSecurity TLS configuration object Example ------- - >>> my_client = c104.Client(tick_rate_ms=1000, command_timeout_ms=1000) + >>> my_client = c104.Client(tick_rate_ms=100, command_timeout_ms=100) )def", - "tick_rate_ms"_a = 1000, "command_timeout_ms"_a = 1000, + "tick_rate_ms"_a = 100, "command_timeout_ms"_a = 100, "transport_security"_a = nullptr) + .def_property_readonly( + "tick_rate_ms", &Client::getTickRate_ms, + "int: the clients tick rate in milliseconds (read-only)") .def_property_readonly("is_running", &Client::isRunning, - "bool: test if client is running (read-only)", - py::return_value_policy::copy) + "bool: test if client is running (read-only)") .def_property_readonly("has_connections", &Client::hasConnections, "bool: test if client has at least one remote " - "server connection (read-only)", - py::return_value_policy::copy) + "server connection (read-only)") + .def_property_readonly( + "has_open_connections", &Client::hasOpenConnections, + "bool: test if client has open connections to servers (read-only)") + .def_property_readonly( + "open_connection_count", &Client::getOpenConnectionCount, + "int: get number of open connections to servers (read-only)") + .def_property_readonly("has_active_connections", + &Client::hasActiveConnections, + "bool: test if client has active (open and not " + "muted) connections to servers (read-only)") + .def_property_readonly("active_connection_count", + &Client::getActiveConnectionCount, + "int: get number of active (open and not muted) " + "connections to servers (read-only)") .def_property_readonly( "connections", &Client::getConnections, - "List[:ref:`c104.Connection`]: list of all remote terminal unit " - "(server) Connection objects (read-only)", - py::return_value_policy::copy) + "list[c104.Connection]: list of all remote terminal unit " + "(server) Connection objects (read-only)") .def_property("originator_address", &Client::getOriginatorAddress, &Client::setOriginatorAddress, "int: primary originator address of this client (0-255)", @@ -932,12 +939,12 @@ PY_MODULE(c104, m) { remote terminal units ip address port: int remote terminal units port - init: :ref:`c104.Init` + init: c104.Init communication initiation commands Returns ------- - :ref:`c104.Connection` + c104.Connection connection object, if added, else None Raises @@ -964,7 +971,7 @@ PY_MODULE(c104, m) { Returns ------- - :ref:`c104.Connection` + c104.Connection connection object, if found else None Example @@ -980,11 +987,11 @@ PY_MODULE(c104, m) { Parameters ---------- common_address: int - common address (value between 0-65535) + common address (value between 1 and 65534) Returns ------- - :ref:`c104.Connection` + c104.Connection connection object, if found else None Example @@ -1011,7 +1018,7 @@ PY_MODULE(c104, m) { >>> my_client.disconnect_all() )def") .def("on_new_station", &Client::setOnNewStationCallback, R"def( - on_new_station(self: c104.Client, callable: Callable[[c104.Client, c104.Connection, int], None]) -> None + on_new_station(self: c104.Client, callable: collections.abc.Callable[[c104.Client, c104.Connection, int], None]) -> None set python callback that will be executed on incoming message from unknown station @@ -1019,12 +1026,12 @@ PY_MODULE(c104, m) { Parameters ---------- - client: :ref:`c104.Client` + client: c104.Client client instance - connection: :ref:`c104.Connection` + connection: c104.Connection connection reporting station common_address: int - station common address (value between 0-65535) + station common address (value between 1 and 65534) Returns ------- @@ -1045,7 +1052,7 @@ PY_MODULE(c104, m) { )def", "callable"_a) .def("on_new_point", &Client::setOnNewPointCallback, R"def( - on_new_point(self: c104.Client, callable: Callable[[c104.Client, c104.Station, int, c104.Type], None]) -> None + on_new_point(self: c104.Client, callable: collections.abc.Callable[[c104.Client, c104.Station, int, c104.Type], None]) -> None set python callback that will be executed on incoming message from unknown point @@ -1053,13 +1060,13 @@ PY_MODULE(c104, m) { Parameters ---------- - client: :ref:`c104.Client` + client: c104.Client client instance - station: :ref:`c104.Station` + station: c104.Station station reporting point io_address: int - point information object address (value between 0 and 16777216) - point_type: :ref:`c104.Type` + point information object address (value between 0 and 16777215) + point_type: c104.Type point information type Returns @@ -1079,14 +1086,15 @@ PY_MODULE(c104, m) { >>> >>> my_client.on_new_point(callable=cl_on_new_point) )def", - "callable"_a); + "callable"_a) + .def("__repr__", &Client::toString); py::class_>( m, "Server", "This class represents a local server and provides access to meta " "information and containing stations") .def(py::init(&Server::create), R"def( - __init__(self: c104.Server, ip: str = "0.0.0.0", port: int = 2404, tick_rate_ms: int = 1000, max_connections: int = 0, transport_security: Optional[c104.TransportSecurity] = None) -> None + __init__(self: c104.Server, ip: str = "0.0.0.0", port: int = 2404, tick_rate_ms: int = 100, select_timeout_ms = 100, max_connections: int = 0, transport_security: Optional[c104.TransportSecurity] = None) -> None create a new 104er server @@ -1098,55 +1106,51 @@ PY_MODULE(c104, m) { listening server port tick_rate_ms: int server thread update interval + select_timeout_ms: int + execution for points in SELECT_AND_EXECUTE mode must arrive within this interval to succeed max_connections: int maximum number of clients allowed to connect - transport_security: :ref:`c104.TransportSecurity` + transport_security: c104.TransportSecurity TLS configuration object Example ------- - >>> my_server = c104.Server(ip="0.0.0.0", port=2404, tick_rate_ms=1000, max_connections=0) + >>> my_server = c104.Server(ip="0.0.0.0", port=2404, tick_rate_ms=100, select_timeout_ms=100, max_connections=0) )def", "ip"_a = "0.0.0.0", "port"_a = IEC_60870_5_104_DEFAULT_PORT, - "tick_rate_ms"_a = 1000, "max_connections"_a = 0, - "transport_security"_a = nullptr) + "tick_rate_ms"_a = 100, "select_timeout_ms"_a = 100, + "max_connections"_a = 0, "transport_security"_a = nullptr) + .def_property_readonly( + "tick_rate_ms", &Server::getTickRate_ms, + "int: the servers tick rate in milliseconds (read-only)") .def_property_readonly("ip", &Server::getIP, "str: ip address the server will accept " - "connections on, \"0.0.0.0\" = any (read-only)", - py::return_value_policy::copy) + "connections on, \"0.0.0.0\" = any (read-only)") .def_property_readonly( "port", &Server::getPort, - "int: port number the server will accept connections on (read-only)", - py::return_value_policy::copy) + "int: port number the server will accept connections on (read-only)") .def_property_readonly("is_running", &Server::isRunning, - "bool: test if server is running (read-only)", - py::return_value_policy::copy) + "bool: test if server is running (read-only)") .def_property_readonly( "has_open_connections", &Server::hasOpenConnections, - "bool: test if Server has open connections to clients (read-only)", - py::return_value_policy::copy) + "bool: test if server has open connections to clients (read-only)") .def_property_readonly( "open_connection_count", &Server::getOpenConnectionCount, - "int: get number of open connections to clients (read-only)", - py::return_value_policy::copy) + "int: get number of open connections to clients (read-only)") .def_property_readonly("has_active_connections", &Server::hasActiveConnections, - "bool: test if Server has active (open and not " - "muted) connections to clients (read-only)", - py::return_value_policy::copy) + "bool: test if server has active (open and not " + "muted) connections to clients (read-only)") .def_property_readonly("active_connection_count", &Server::getActiveConnectionCount, "int: get number of active (open and not muted) " - "connections to clients (read-only)", - py::return_value_policy::copy) + "connections to clients (read-only)") .def_property_readonly( "has_stations", &Server::hasStations, - "bool: test if local server has at least one station (read-only)", - py::return_value_policy::copy) + "bool: test if server has at least one station (read-only)") .def_property_readonly("stations", &Server::getStations, - "List[:ref:`c104.Station`]: list of all local " - "Station objects (read-only)", - py::return_value_policy::copy) + "list[c104.Station]: list of all local " + "Station objects (read-only)") .def_property("max_connections", &Server::getMaxOpenConnections, &Server::setMaxOpenConnections, "int: maximum number of open connections, 0 = no limit", @@ -1182,11 +1186,11 @@ PY_MODULE(c104, m) { Parameters ---------- common_address: int - station common address (value between 0 and 65535) + station common address (value between 1 and 65534) Returns ------- - :ref:`c104.Station` + c104.Station station object, if station was added, else None Example @@ -1202,11 +1206,11 @@ PY_MODULE(c104, m) { Parameters ---------- common_address: int - station common address (value between 0 and 65535) + station common address (value between 1 and 65534) Returns ------- - :ref:`c104.Station` + c104.Station station object, if found, else None Example @@ -1215,7 +1219,7 @@ PY_MODULE(c104, m) { )def", "common_address"_a) .def("on_receive_raw", &Server::setOnReceiveRawCallback, R"def( - on_receive_raw(self: c104.Server, callable: Callable[[c104.Server, bytes], None]) -> None + on_receive_raw(self: c104.Server, callable: collections.abc.Callable[[c104.Server, bytes], None]) -> None set python callback that will be executed on incoming message @@ -1223,7 +1227,7 @@ PY_MODULE(c104, m) { Parameters ---------- - server: :ref:`c104.Server` + server: c104.Server server instance data: bytes raw message bytes @@ -1246,7 +1250,7 @@ PY_MODULE(c104, m) { )def", "callable"_a) .def("on_send_raw", &Server::setOnSendRawCallback, R"def( - on_send_raw(self: c104.Server, callable: Callable[[c104.Server, bytes], None]) -> None + on_send_raw(self: c104.Server, callable: collections.abc.Callable[[c104.Server, bytes], None]) -> None set python callback that will be executed on outgoing message @@ -1254,7 +1258,7 @@ PY_MODULE(c104, m) { Parameters ---------- - server: :ref:`c104.Server` + server: c104.Server server instance data: bytes raw message bytes @@ -1277,7 +1281,7 @@ PY_MODULE(c104, m) { )def", "callable"_a) .def("on_connect", &Server::setOnConnectCallback, R"def( - on_connect(self: c104.Server, callable: Callable[[c104.Server, ip], bool]) -> None + on_connect(self: c104.Server, callable: collections.abc.Callable[[c104.Server, ip], bool]) -> None set python callback that will be executed on incoming connection requests @@ -1285,7 +1289,7 @@ PY_MODULE(c104, m) { Parameters ---------- - server: :ref:`c104.Server` + server: c104.Server server instance ip: str client connection request ip @@ -1310,7 +1314,7 @@ PY_MODULE(c104, m) { )def", "callable"_a) .def("on_clock_sync", &Server::setOnClockSyncCallback, R"def( - on_clock_sync(self: c104.Server, callable: Callable[[c104.Server, str, datetime.datetime], c104.ResponseState]) -> None + on_clock_sync(self: c104.Server, callable: collections.abc.Callable[[c104.Server, str, datetime.datetime], c104.ResponseState]) -> None set python callback that will be executed on incoming clock sync command @@ -1318,7 +1322,7 @@ PY_MODULE(c104, m) { Parameters ---------- - server: :ref:`c104.Server` + server: c104.Server server instance ip: str client connection request ip @@ -1327,7 +1331,7 @@ PY_MODULE(c104, m) { Returns ------- - :ref:`c104.ResponseState` + c104.ResponseState success or failure of clock sync command Raises @@ -1348,7 +1352,7 @@ PY_MODULE(c104, m) { "callable"_a) .def("on_unexpected_message", &Server::setOnUnexpectedMessageCallback, R"def( - on_unexpected_message(self: c104.Server, callable: Callable[[c104.Server, c104.IncomingMessage, c104.Umc], None]) -> None + on_unexpected_message(self: c104.Server, callable: collections.abc.Callable[[c104.Server, c104.IncomingMessage, c104.Umc], None]) -> None set python callback that will be executed on unexpected incoming messages @@ -1356,11 +1360,11 @@ PY_MODULE(c104, m) { Parameters ---------- - server: :ref:`c104.Server` + server: c104.Server server instance - message: :ref:`c104.IncomingMessage` + message: c104.IncomingMessage incoming message - cause: :ref:`c104.Umc` + cause: c104.Umc unexpected message cause Returns @@ -1379,7 +1383,8 @@ PY_MODULE(c104, m) { >>> >>> my_server.on_unexpected_message(callable=sv_on_unexpected_message) )def", - "callable"_a); + "callable"_a) + .def("__repr__", &Server::toString); py::class_>( m, "Connection", @@ -1387,31 +1392,35 @@ PY_MODULE(c104, m) { "provides access to meta information and containing stations") .def_property_readonly( "ip", &Remote::Connection::getIP, - "str: remote terminal units (server) ip (read-only)", - py::return_value_policy::copy) + "str: remote terminal units (server) ip (read-only)") .def_property_readonly( "port", &Remote::Connection::getPort, - "int: remote terminal units (server) port (read-only)", - py::return_value_policy::copy) + "int: remote terminal units (server) port (read-only)") + .def_property_readonly( + "state", &Remote::Connection::getState, + "c104.ConnectionState: current connection state (read-only)") .def_property_readonly( "has_stations", &Remote::Connection::hasStations, - "bool: test if remote server has at least one station (read-only)", - py::return_value_policy::copy) + "bool: test if remote server has at least one station (read-only)") .def_property_readonly( "stations", &Remote::Connection::getStations, - "List[:ref:`c104.Station`] list of all Station objects (read-only)", - py::return_value_policy::copy) + "list[c104.Station] list of all Station objects (read-only)") .def_property_readonly("is_connected", &Remote::Connection::isOpen, - "bool: test if connection is opened (read-only)", - py::return_value_policy::copy) + "bool: test if connection is opened (read-only)") .def_property_readonly("is_muted", &Remote::Connection::isMuted, - "bool: test if connection is muted (read-only)", - py::return_value_policy::copy) + "bool: test if connection is muted (read-only)") .def_property( "originator_address", &Remote::Connection::getOriginatorAddress, &Remote::Connection::setOriginatorAddress, - "int: primary originator address of this connection (0-255)", - py::return_value_policy::copy) + "int: primary originator address of this connection (0-255)") + .def_property_readonly("connected_at", + &Remote::Connection::getConnectedAt, + "typing.Optional[datetime.datetime]: datetime of " + "disconnect, if connection is closed (read-only)") + .def_property_readonly( + "disconnected_at", &Remote::Connection::getDisconnectedAt, + "typing.Optional[datetime.datetime]: test if connection " + "is muted (read-only)") .def("connect", &Remote::Connection::connect, R"def( connect(self: c104.Connection) -> None @@ -1463,17 +1472,17 @@ PY_MODULE(c104, m) { )def", py::return_value_policy::copy) .def("interrogation", &Remote::Connection::interrogation, R"def( - interrogation(self: c104.Connection, common_address: int, cause: c104.Cot = c104.Cot.ACTIVATION, qualifier: c104.Qoi = c104.Qoi.Station, wait_for_response: bool = True) -> bool + interrogation(self: c104.Connection, common_address: int, cause: c104.Cot = c104.Cot.ACTIVATION, qualifier: c104.Qoi = c104.Qoi.STATION, wait_for_response: bool = True) -> bool send an interrogation command to the remote terminal unit (server) Parameters ---------- common_address: int - station common address (value between 0 and 65535) - cause: :ref:`c104.Cot` + station common address (The valid range is 0 to 65535. Using the values 0 or 65535 sends the command to all stations, acting as a wildcard.) + cause: c104.Cot cause of transmission - qualifier: :ref:`c104.Qoi` + qualifier: c104.Qoi qualifier of interrogation wait_for_response: bool block call until command success or failure reponse received? @@ -1490,7 +1499,7 @@ PY_MODULE(c104, m) { Example ------- - >>> if not my_connection.interrogation(common_address=47, cause=c104.Cot.ACTIVATION, qualifier=c104.Qoi.Station): + >>> if not my_connection.interrogation(common_address=47, cause=c104.Cot.ACTIVATION, qualifier=c104.Qoi.STATION): >>> raise ValueError("Cannot send interrogation command") )def", "common_address"_a, "cause"_a = CS101_COT_ACTIVATION, @@ -1498,17 +1507,17 @@ PY_MODULE(c104, m) { py::return_value_policy::copy) .def("counter_interrogation", &Remote::Connection::counterInterrogation, R"def( - counter_interrogation(self: c104.Connection, common_address: int, cause: c104.Cot = c104.Cot.ACTIVATION, qualifier: c104.Qoi = c104.Qoi.Station, wait_for_response: bool = True) -> bool + counter_interrogation(self: c104.Connection, common_address: int, cause: c104.Cot = c104.Cot.ACTIVATION, qualifier: c104.Qoi = c104.Qoi.STATION, wait_for_response: bool = True) -> bool send a counter interrogation command to the remote terminal unit (server) Parameters ---------- common_address: int - station common address (value between 0 and 65535) - cause: :ref:`c104.Cot` + station common address (The valid range is 0 to 65535. Using the values 0 or 65535 sends the command to all stations, acting as a wildcard.) + cause: c104.Cot cause of transmission - qualifier: :ref:`c104.Qoi` + qualifier: c104.Qoi qualifier of interrogation wait_for_response: bool block call until command success or failure reponse received? @@ -1525,7 +1534,7 @@ PY_MODULE(c104, m) { Example ------- - >>> if not my_connection.counter_interrogation(common_address=47, cause=c104.Cot.ACTIVATION, qualifier=c104.Qoi.Station): + >>> if not my_connection.counter_interrogation(common_address=47, cause=c104.Cot.ACTIVATION, qualifier=c104.Qoi.STATION): >>> raise ValueError("Cannot send counter interrogation command") )def", "common_address"_a, "cause"_a = CS101_COT_ACTIVATION, @@ -1540,7 +1549,7 @@ PY_MODULE(c104, m) { Parameters ---------- common_address: int - station common address (value between 0 and 65535) + station common address (The valid range is 0 to 65535. Using the values 0 or 65535 sends the command to all stations, acting as a wildcard.) wait_for_response: bool block call until command success or failure reponse received? @@ -1565,7 +1574,7 @@ PY_MODULE(c104, m) { Parameters ---------- common_address: int - station common address (value between 0 and 65535) + station common address (The valid range is 0 to 65535. Using the values 0 or 65535 sends the command to all stations, acting as a wildcard.) with_time: bool send with or without timestamp wait_for_response: bool @@ -1591,11 +1600,11 @@ PY_MODULE(c104, m) { Parameters ---------- common_address: int - station common address (value between 0 and 65535) + station common address (value between 1 and 65534) Returns ------- - :ref:`c104.Station` + c104.Station station object, if found, else None Example @@ -1611,11 +1620,11 @@ PY_MODULE(c104, m) { Parameters ---------- common_address: int - station common address (value between 0 and 65535) + station common address (value between 1 and 65534) Returns ------- - :ref:`c104.Station` + c104.Station station object, if station was added, else None Example @@ -1625,7 +1634,7 @@ PY_MODULE(c104, m) { "common_address"_a) .def("on_receive_raw", &Remote::Connection::setOnReceiveRawCallback, R"def( - on_receive_raw(self: c104.Connection, callable: Callable[[c104.Connection, bytes], None]) -> None + on_receive_raw(self: c104.Connection, callable: collections.abc.Callable[[c104.Connection, bytes], None]) -> None set python callback that will be executed on incoming message @@ -1633,7 +1642,7 @@ PY_MODULE(c104, m) { Parameters ---------- - connection: :ref:`c104.Connection` + connection: c104.Connection connection instance data: bytes raw message bytes @@ -1656,7 +1665,7 @@ PY_MODULE(c104, m) { )def", "callable"_a) .def("on_send_raw", &Remote::Connection::setOnSendRawCallback, R"def( - on_send_raw(self: c104.Connection, callable: Callable[[c104.Connection, bytes], None]) -> None + on_send_raw(self: c104.Connection, callable: collections.abc.Callable[[c104.Connection, bytes], None]) -> None set python callback that will be executed on outgoing message @@ -1664,7 +1673,7 @@ PY_MODULE(c104, m) { Parameters ---------- - connection: :ref:`c104.Connection` + connection: c104.Connection connection instance data: bytes raw message bytes @@ -1688,7 +1697,7 @@ PY_MODULE(c104, m) { "callable"_a) .def("on_state_change", &Remote::Connection::setOnStateChangeCallback, R"def( - on_state_change(self: c104.Connection, callable: Callable[[c104.Connection, c104.ConnectionState], None]) -> None + on_state_change(self: c104.Connection, callable: collections.abc.Callable[[c104.Connection, c104.ConnectionState], None]) -> None set python callback that will be executed on connection state changes @@ -1696,9 +1705,9 @@ PY_MODULE(c104, m) { Parameters ---------- - connection: :ref:`c104.Connection` + connection: c104.Connection connection instance - state: :ref:`c104.ConnectionState` + state: c104.ConnectionState latest connection state Returns @@ -1717,34 +1726,33 @@ PY_MODULE(c104, m) { >>> >>> my_connection.on_state_change(callable=con_on_state_change) )def", - "callable"_a); + "callable"_a) + .def("__repr__", &Remote::Connection::toString); + ; py::class_>( m, "Station", "This class represents local or remote stations and provides access to " "meta information and containing points") .def_property_readonly("server", &Object::Station::getServer, - "Optional[:ref:`c104.Server`]: parent Server of " + "typing.Optional[c104.Server]: parent Server of " "local station (read-only)") .def_property_readonly("connection", &Object::Station::getConnection, - "Optional[:ref:`c104.Connection`]: parent " + "typing.Optional[c104.Connection]: parent " "Connection of non-local station (read-only)") .def_property_readonly( "common_address", &Object::Station::getCommonAddress, - "int: common address of this station (0-65535) (read-only)", + "int: common address of this station (1-65534) (read-only)", py::return_value_policy::copy) .def_property_readonly("is_local", &Object::Station::isLocal, "bool: test if station is local (has sever) or " - "remote (has connection) one (read-only)", - py::return_value_policy::copy) + "remote (has connection) one (read-only)") .def_property_readonly( "has_points", &Object::Station::hasPoints, - "bool: test if station has at least one point (read-only)", - py::return_value_policy::copy) + "bool: test if station has at least one point (read-only)") .def_property_readonly( "points", &Object::Station::getPoints, - "List[:ref:`c104.Point`] list of all Point objects (read-only)", - py::return_value_policy::copy) + "list[c104.Point] list of all Point objects (read-only)") .def("get_point", &Object::Station::getPoint, R"def( get_point(self: c104.Station, io_address: int) -> Optional[c104.Point] @@ -1753,11 +1761,11 @@ PY_MODULE(c104, m) { Parameters ---------- io_address: int - point information object address (value between 0 and 16777216) + point information object address (value between 0 and 16777215) Returns ------- - :ref:`c104.Point` + c104.Point point object, if found, else None Example @@ -1766,28 +1774,28 @@ PY_MODULE(c104, m) { )def", "io_address"_a) .def("add_point", &Object::Station::addPoint, R"def( - add_point(self: c104.Station, io_address: int, type: c104.Type, report_ms: int = 0, related_io_address: int = 0, related_io_autoreturn: bool = False, command_mode: c104.CommandMode = c104.CommandMode.DIRECT) -> Optional[c104.Point] + add_point(self: c104.Station, io_address: int, type: c104.Type, report_ms: int = 0, related_io_address: Optional[int] = None, related_io_autoreturn: bool = False, command_mode: c104.CommandMode = c104.CommandMode.DIRECT) -> Optional[c104.Point] add a new point to this station and return the new point object Parameters ---------- io_address: int - point information object address (value between 0 and 16777216) - type: :ref:`c104.Type` + point information object address (value between 0 and 16777215) + type: c104.Type point information type report_ms: int - automatic reporting interval in milliseconds (monitoring points server-sided only) - related_io_address: int + automatic reporting interval in milliseconds (monitoring points server-sided only), 0 = disabled + related_io_address: typing.Optional[int] related monitoring point identified by information object address, that should be auto transmitted on incoming client command (for control points server-sided only) related_io_autoreturn: bool automatic reporting interval in milliseconds (for control points server-sided only) - command_mode: :ref:`c104.CommandMode` + command_mode: c104.CommandMode command transmission mode (direct or select-and-execute) Returns ------- - :ref:`c104.Station` + c104.Station station object, if station was added, else None Raises @@ -1808,8 +1816,9 @@ PY_MODULE(c104, m) { >>> point_3 = sv_station_1.add_point(io_address=12, type=c104.Type.C_SE_NC_1, report_ms=0, related_io_address=point_2.io_address, related_io_autoreturn=True, command_mode=c104.CommandMode.SELECT_AND_EXECUTE) )def", "io_address"_a, "type"_a, "report_ms"_a = 0, - "related_io_address"_a = 0, "related_io_autoreturn"_a = false, - "command_mode"_a = DIRECT_COMMAND); + "related_io_address"_a = std::nullopt, + "related_io_autoreturn"_a = false, "command_mode"_a = DIRECT_COMMAND) + .def("__repr__", &Object::Station::toString); py::class_>( m, "Point", @@ -1817,79 +1826,71 @@ PY_MODULE(c104, m) { "and provides access to structured properties of points") .def_property_readonly( "station", &Object::DataPoint::getStation, - "Optional[:ref:`c104.Station`]: parent Station object (read-only)") + "typing.Optional[c104.Station]: parent Station object (read-only)") .def_property_readonly("io_address", &Object::DataPoint::getInformationObjectAddress, - "int: information object address (read-only)", - py::return_value_policy::copy) + "int: information object address (read-only)") .def_property_readonly("type", &Object::DataPoint::getType, - ":ref:`c104.Type`: iec60870 data Type (read-only)", - py::return_value_policy::copy) - .def_property("quality", &Object::DataPoint::getQuality, - &Object::DataPoint::setQuality, - ":ref:`c104.Quality`: Quality bitset object", - py::return_value_policy::copy) + "c104.Type: iec60870 data Type (read-only)") .def_property("related_io_address", &Object::DataPoint::getRelatedInformationObjectAddress, &Object::DataPoint::setRelatedInformationObjectAddress, - "int: io_address of a related monitoring point", - py::return_value_policy::copy) + "typing.Optional[int]: io_address of a related monitoring " + "point or None") .def_property( "related_io_autoreturn", &Object::DataPoint::getRelatedInformationObjectAutoReturn, &Object::DataPoint::setRelatedInformationObjectAutoReturn, - "bool: toggle automatic return info remote response on or off", - py::return_value_policy::copy) + "bool: toggle automatic return info remote response on or off") .def_property("command_mode", &Object::DataPoint::getCommandMode, &Object::DataPoint::setCommandMode, "c104.CommandMode: set direct or select-and-execute " "command transmission mode", py::return_value_policy::copy) - .def_property( + .def_property_readonly( "selected_by", &Object::DataPoint::getSelectedByOriginatorAddress, - &Object::DataPoint::setSelectedByOriginatorAddress, - "int: originator address (1-255) = SELECTED, 0 = NOT SELECTED", - py::return_value_policy::copy) + "typing.Optional[int]: originator address (0-255) or None") .def_property("report_ms", &Object::DataPoint::getReportInterval_ms, &Object::DataPoint::setReportInterval_ms, "int: interval in milliseconds between periodic " - "transmission, 0 = no periodic transmission", - py::return_value_policy::copy) + "transmission, 0 = no periodic transmission") + .def_property_readonly("timer_ms", + &Object::DataPoint::getTimerInterval_ms, + "int: interval in milliseconds between timer " + "callbacks, 0 = no periodic transmission") + .def_property("info", &Object::DataPoint::getInfo, + &Object::DataPoint::setInfo, + "ref:`c104.Information`: information object", + py::return_value_policy::automatic) + .def_property( + "value", &Object::DataPoint::getValue, &Object::DataPoint::setValue, + "typing.Union[None, bool, c104.Double, c104.Step, c104.Int7, " + "c104.Int16, int, c104.Byte32, c104.NormalizedFloat, float, " + "c104.EventState, c104.StartEvents, c104.OutputCircuits, " + "c104.PackedSingle]: value (this is just a shortcut to " + "point.info.value)", + py::return_value_policy::copy) .def_property( - "value", &Object::DataPoint::getValue, - [](Object::DataPoint &d1, const py::object &o) { - d1.setValue(py::cast(o)); - }, - "float: value", py::return_value_policy::copy) - .def_property_readonly( - "value_uint32", &Object::DataPoint::getValueAsUInt32, - "int: value formatted as unsigned integer (read-only)", + "quality", &Object::DataPoint::getQuality, + &Object::DataPoint::setQuality, + "typing.Union[None, c104.Quality, c104.BinaryCounterQuality]: " + "Quality info object (this is just a shortcut to " + "point.info.quality)", py::return_value_policy::copy) .def_property_readonly( - "value_int32", &Object::DataPoint::getValueAsInt32, - "int: value formatted as signed integer (read-only)", + "processed_at", &Object::DataPoint::getProcessedAt, + "datetime.datetime: timestamp with milliseconds of last local " + "information processing " + "(read-only)", py::return_value_policy::copy) - .def_property_readonly("value_float", &Object::DataPoint::getValueAsFloat, - "float: value formatted as float (read-only)", + .def_property_readonly("recorded_at", &Object::DataPoint::getRecordedAt, + "typing.Optional[int]: timestamp with " + "milliseconds transported with the " + "value " + "itself or None (read-only)", py::return_value_policy::copy) - .def_property_readonly( // todo convert to datetime.datetime - "updated_at_ms", &Object::DataPoint::getUpdatedAt_ms, - "int: timestamp in milliseconds of last value update (read-only)", - py::return_value_policy::copy) - .def_property_readonly( // todo convert to datetime.datetime - "reported_at_ms", &Object::DataPoint::getReportedAt_ms, - "int: timestamp in milliseconds of last transmission (read-only)", - py::return_value_policy::copy) - .def_property_readonly( // todo convert to datetime.datetime - "received_at_ms", &Object::DataPoint::getReceivedAt_ms, - "int: timestamp in milliseconds of last incoming message (read-only)", - py::return_value_policy::copy) - .def_property_readonly( // todo convert to datetime.datetime - "sent_at_ms", &Object::DataPoint::getSentAt_ms, - "int: timestamp in milliseconds of last outgoing message (read-only)", - py::return_value_policy::copy) .def("on_receive", &Object::DataPoint::setOnReceiveCallback, R"def( - on_receive(self: c104.Point, callable: Callable[[c104.Point, dict, c104.IncomingMessage], c104.ResponseState]) -> None + on_receive(self: c104.Point, callable: collections.abc.Callable[[c104.Point, dict, c104.IncomingMessage], c104.ResponseState]) -> None set python callback that will be executed on every incoming message this can be either a command or an monitoring message @@ -1898,16 +1899,16 @@ PY_MODULE(c104, m) { Parameters ---------- - point: :ref:`c104.Point` + point: c104.Point point instance - previous_state: dict - dictionary containing the state of the point before the command took effect :code:`{"value": float, "quality": :ref:`c104.Quality`, updatedAt_ms: int}` + previous_info: c104.Information + Information object containing the state of the point before the command took effect message: c104.IncomingMessage new command message Returns ------- - :ref:`c104.ResponseState` + c104.ResponseState send command SUCCESS or FAILURE response Raises @@ -1917,8 +1918,8 @@ PY_MODULE(c104, m) { Example ------- - >>> def on_setpoint_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - >>> print("SV] {0} SETPOINT COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality)) + >>> def on_setpoint_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + >>> print("SV] {0} SETPOINT COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_info, message.cot, point.quality)) >>> if point.quality.is_good(): >>> if point.related_io_address: >>> print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) @@ -1938,7 +1939,7 @@ PY_MODULE(c104, m) { )def", "callable"_a) .def("on_before_read", &Object::DataPoint::setOnBeforeReadCallback, R"def( - on_before_read(self: c104.Point, callable: Callable[[c104.Point], None]) -> None + on_before_read(self: c104.Point, callable: collections.abc.Callable[[c104.Point], None]) -> None set python callback that will be called on incoming interrogation or read commands to support polling @@ -1946,7 +1947,7 @@ PY_MODULE(c104, m) { Parameters ---------- - point: :ref:`c104.Point` + point: c104.Point point instance Returns @@ -1970,7 +1971,7 @@ PY_MODULE(c104, m) { "callable"_a) .def("on_before_auto_transmit", &Object::DataPoint::setOnBeforeAutoTransmitCallback, R"def( - on_before_auto_transmit(self: c104.Point, callable: Callable[[c104.Point], None]) -> None + on_before_auto_transmit(self: c104.Point, callable: collections.abc.Callable[[c104.Point], None]) -> None set python callback that will be called before server reports a measured value interval-based @@ -1978,7 +1979,7 @@ PY_MODULE(c104, m) { Parameters ---------- - point: :ref:`c104.Point` + point: c104.Point point instance Returns @@ -1998,14 +1999,47 @@ PY_MODULE(c104, m) { Example ------- - >>> def on_before_read_steppoint(point: c104.Point) -> None: - >>> print("SV] {0} READ COMMAND on IOA: {1}".format(point.type, point.io_address)) - >>> point.value = random.randint(-64,63) # import random + >>> def on_before_auto_transmit_step(point: c104.Point) -> None: + >>> print("SV] {0} PERIODIC TRANSMIT on IOA: {1}".format(point.type, point.io_address)) + >>> point.value = c104.Int7(random.randint(-64,63)) # import random >>> >>> step_point = sv_station_2.add_point(io_address=31, type=c104.Type.M_ST_TB_1, report_ms=2000) - >>> step_point.on_before_auto_transmit(callable=on_before_read_steppoint) + >>> step_point.on_before_auto_transmit(callable=on_before_auto_transmit_step) )def", "callable"_a) + .def("on_timer", &Object::DataPoint::setOnTimerCallback, R"def( + on_timer(self: c104.Point, callable: collections.abc.Callable[[c104.Point], None], int) -> None + + set python callback that will be called in a fixed delay (timer_ms) + + **Callable signature** + + Parameters + ---------- + point: c104.Point + point instance + interval_ms: int + fixed delay between timer callback execution, default: 0, min: 50 + + Returns + ------- + None + + Raises + ------ + ValueError + If callable signature does not match exactly + + Example + ------- + >>> def on_timer(point: c104.Point) -> None: + >>> print("SV] {0} TIMER on IOA: {1}".format(point.type, point.io_address)) + >>> point.value = random.randint(-64,63) # import random + >>> + >>> nv_point = sv_station_2.add_point(io_address=31, type=c104.Type.M_ME_TD_1) + >>> nv_point.on_timer(callable=on_timer, interval_ms=1000) +)def", + "callable"_a, "interval_ms"_a = 0) .def("read", &Object::DataPoint::read, R"def( read(self: c104.Point) -> bool @@ -2027,37 +2061,8 @@ PY_MODULE(c104, m) { >>> print("read command successful") )def", py::return_value_policy::copy) - .def( - "set", - [](Object::DataPoint &d1, const py::object &o, const Quality &q, - const std::uint_fast64_t &t) { - d1.setValueEx(py::cast(o), q, t); - }, - R"def( - set(self: c104.Point, value: typing.Union[float, c104.Step, c104.Double], quality: c104.Quality, timestamp_ms: int) -> None - - set value, quality and timestamp - - Parameters - ---------- - value: float, c104.Step, c104.Double - point value - quality: :ref:`c104.Quality` - quality restrictions if any, default: c104.Quality.None - timestamp_ms: int - modification timestamp in milliseconds, default: current utc timestamp - - Returns - ------- - None - - Example - ------- - >>> sv_measurement_point.set(value=-1234.56, quality=c104.Quality.Invalid, timestamp_ms=int(time.time() * 1000)) -)def", - "value"_a, "quality"_a = Quality::None, "timestamp_ms"_a = 0) .def("transmit", &Object::DataPoint::transmit, R"def( - transmit(self: c104.Point, cause: c104.Cot = c104.Cot.SPONTANEOUS, qualifier: c104.Qoc = c104.Qoc.NONE) -> bool + transmit(self: c104.Point, cause: c104.Cot) -> bool **Server-side point** report a measurement value to connected clients @@ -2067,20 +2072,13 @@ PY_MODULE(c104, m) { Parameters ---------- - cause: :ref:`c104.Cot` + cause: c104.Cot cause of the transmission - qualifier: :ref:`c104.Qoc` - command duration parametrization (only for following command points: single, double and regulation step) Raises ------ ValueError If parent station, server or connection reference is invalid - If qualifier is set for points in monitoring direction - - Warning - ------- - It is recommended to specify a cause and not to use UNKNOWN_COT. Returns ------- @@ -2090,11 +2088,761 @@ PY_MODULE(c104, m) { Example ------- >>> sv_measurement_point.transmit(cause=c104.Cot.SPONTANEOUS) - >>> cl_single_command_point.transmit(qualifier=c104.Qoc.SHORT_PULSE) + >>> cl_single_command_point.transmit(cause=c104.Cot.ACTIVATION) +)def", + "cause"_a, py::return_value_policy::copy) + .def("__repr__", &Object::DataPoint::toString); + + py::class_>( + m, "Information", + "This class represents all specialized kind of information a specific " + "point may have") + .def_property_readonly( + "value", &Object::Information::getValue, + "typing.Union[None, bool, c104.Double, c104.Step, c104.Int7, " + "c104.Int16, int, c104.Byte32, c104.NormalizedFloat, float, " + "c104.EventState, c104.StartEvents, c104.OutputCircuits, " + "c104.PackedSingle]: mapped value property (read-only). The " + "setter is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::Information::getQuality, + "typing.Union[None, c104.Quality, c104.BinaryCounterQuality]: " + "quality information (read-only). The " + "setter is available via point.quality=xyz") + .def_property_readonly( + "processed_at", &Object::Information::getProcessedAt, + "datetime.datetime: timestamp with milliseconds of last local " + "information processing " + "(read-only)") + .def_property_readonly("recorded_at", &Object::Information::getRecordedAt, + "typing.Optional[int]: timestamp with " + "milliseconds transported with the " + "value " + "itself or None (read-only)") + .def("__repr__", &Object::Information::toString); + ; + + py::class_>( + m, "SingleInfo", + "This class represents all specific single point information") + .def(py::init(&Object::SingleInfo::create), R"def( + __init__(self: c104.SingleInfo, on: bool, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new single info + + Parameters + ------- + on: bool + Single status value + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> single_info = c104.SingleInfo(on=True, quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) )def", - "cause"_a = CS101_COT_UNKNOWN_COT, - "qualifier"_a = CS101_QualifierOfCommand::NONE, - py::return_value_policy::copy); + "on"_a, "quality"_a = Quality::None, "recorded_at"_a = py::none()) + .def_property_readonly("on", &Object::SingleInfo::isOn, + "bool: the value (read-only)") + .def_property_readonly("value", &Object::SingleInfo::getValue, + "bool: maps to property ``on`` (read-only). The " + "setter is available via point.value=xyz") + .def_property_readonly("quality", &Object::SingleInfo::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def("__repr__", &Object::SingleInfo::toString); + + py::class_>( + m, "SingleCmd", + "This class represents all specific single command information") + .def(py::init(&Object::SingleCmd::create), R"def( + __init__(self: c104.SingleCmd, on: bool, qualifier: c104.Qoc = c104.QoC.NONE, recorded_at: Optional[datetime.datetime] = None) -> None + + create a new single command + + Parameters + ------- + on: bool + Single command value + qualifier: c104.Qoc + Qualifier of command + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> single_cmd = c104.SingleCmd(on=True, qualifier=c104.Qoc.SHORT_PULSE, recorded_at=datetime.datetime.utcnow()) +)def", + "on"_a, "qualifier"_a = CS101_QualifierOfCommand::NONE, + "recorded_at"_a = py::none()) + .def_property_readonly("on", &Object::SingleCmd::isOn, + "bool: the value (read-only)") + .def_property_readonly("value", &Object::SingleCmd::getValue, + "bool: maps to property ``on`` (read-only). The " + "setter is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::SingleCmd::getQuality, + "None: This information does not contain quality information.") + .def_property_readonly( + "qualifier", &Object::SingleCmd::getQualifier, + "c104.Qoc: the command qualifier information (read-only)") + .def("__repr__", &Object::SingleCmd::toString); + + py::class_>( + m, "DoubleInfo", + "This class represents all specific double point information") + .def(py::init(&Object::DoubleInfo::create), R"def( + __init__(self: c104.DoubleInfo, state: c104.Double, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new double info + + Parameters + ------- + state: c104.Double + Double point status value + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> double_info = c104.DoubleInfo(state=c104.Double.ON, quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "state"_a, "quality"_a = Quality::None, "recorded_at"_a = py::none()) + .def_property_readonly("state", &Object::DoubleInfo::getState, + "c104.Double: the value (read-only)") + .def_property_readonly( + "value", &Object::DoubleInfo::getValue, + "c104.Double: maps to property ``state`` (read-only). The setter is " + "available via point.value=xyz") + .def_property_readonly("quality", &Object::DoubleInfo::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def("__repr__", &Object::DoubleInfo::toString); + + py::class_>( + m, "DoubleCmd", + "This class represents all specific double command information") + .def(py::init(&Object::DoubleCmd::create), R"def( + __init__(self: c104.DoubleCmd, state: c104.Double, qualifier: c104.Qoc = c104.QoC.NONE, recorded_at: Optional[datetime.datetime] = None) -> None + + create a new double command + + Parameters + ------- + state: c104.Double + Double command value + qualifier: c104.Qoc + Qualifier of command + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> double_cmd = c104.DoubleCmd(state=c104.Double.ON, qualifier=c104.Qoc.SHORT_PULSE, recorded_at=datetime.datetime.utcnow()) +)def", + "state"_a, "qualifier"_a = CS101_QualifierOfCommand::NONE, + "recorded_at"_a = py::none()) + .def_property_readonly("state", &Object::DoubleCmd::getState, + "c104.Double: the value (read-only)") + .def_property_readonly( + "qualifier", &Object::DoubleCmd::getQualifier, + "c104.Qoc: the command qualifier information (read-only)") + .def_property_readonly( + "value", &Object::DoubleCmd::getValue, + "c104.Double: maps to property ``state`` (read-only). The setter is " + "available via point.value=xyz") + .def_property_readonly( + "quality", &Object::DoubleCmd::getQuality, + "None: This information does not contain quality information.") + .def("__repr__", &Object::DoubleCmd::toString); + + py::class_>( + m, "StepInfo", + "This class represents all specific step point information") + .def(py::init(&Object::StepInfo::create), R"def( + __init__(self: c104.StepInfo, position: c104.Int7, transient: bool, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new step info + + Parameters + ------- + position: c104.Int7 + Current transformer step position value + transient: bool + Indicator, if transformer is currently in step change procedure + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> step_info = c104.StepInfo(position=c104.Int7(2), transient=False, quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "state"_a, "transient"_a = false, "quality"_a = Quality::None, + "recorded_at"_a = py::none()) + .def_property_readonly("position", &Object::StepInfo::getPosition, + "c104.Int7: the value (read-only)") + .def_property_readonly("transient", &Object::StepInfo::isTransient, + "bool: if the position is transient (read-only)") + .def_property_readonly( + "value", &Object::StepInfo::getValue, + "c104.Int7: maps to property ``position`` (read-only). The setter is " + "available via point.value=xyz") + .def_property_readonly("quality", &Object::StepInfo::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def("__repr__", &Object::StepInfo::toString); + + py::class_>( + m, "StepCmd", + "This class represents all specific step command information") + .def(py::init(&Object::StepCmd::create), R"def( + __init__(self: c104.StepCmd, direction: c104.Step, qualifier: c104.Qoc = c104.QoC.NONE, recorded_at: Optional[datetime.datetime] = None) -> None + + create a new step command + + Parameters + ------- + direction: c104.Step + Step command direction value + qualifier: c104.Qoc + Qualifier of Command + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> step_cmd = c104.StepCmd(direction=c104.Step.HIGHER, qualifier=c104.Qoc.SHORT_PULSE, recorded_at=datetime.datetime.utcnow()) +)def", + "direction"_a, "qualifier"_a = CS101_QualifierOfCommand::NONE, + "recorded_at"_a = py::none()) + .def_property_readonly("direction", &Object::StepCmd::getStep, + "c104.Step: the value (read-only)") + .def_property_readonly( + "qualifier", &Object::StepCmd::getQualifier, + "c104.Qoc: the command qualifier information (read-only)") + .def_property_readonly( + "value", &Object::DoubleCmd::getValue, + "c104.Step: maps to property ``direction`` (read-only). The setter " + "is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::DoubleCmd::getQuality, + "None: This information does not contain quality information.") + .def("__repr__", &Object::StepCmd::toString); + + py::class_>( + m, "BinaryInfo", + "This class represents all specific binary point information") + .def(py::init(&Object::BinaryInfo::create), R"def( + __init__(self: c104.BinaryInfo, blob: c104.Byte32, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new binary info + + Parameters + ------- + blob: c104.Byte32 + Binary status value + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> binary_info = c104.BinaryInfo(blob=c104.Byte32(2345), quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "blob"_a, "quality"_a = Quality::None, "recorded_at"_a = py::none()) + .def_property_readonly("blob", &Object::BinaryInfo::getBlob, + "c104.Byte32: the value (read-only)") + .def_property_readonly( + "value", &Object::BinaryInfo::getValue, + "c104.Byte32: maps to property ``blob`` (read-only). The setter is " + "available via point.value=xyz") + .def_property_readonly("quality", &Object::BinaryInfo::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def("__repr__", &Object::BinaryInfo::toString); + + py::class_>( + m, "BinaryCmd", + "This class represents all specific binary command information") + .def(py::init(&Object::BinaryCmd::create), R"def( + __init__(self: c104.BinaryCmd, blob: c104.Byte32, recorded_at: Optional[datetime.datetime] = None) -> None + + create a new binary command + + Parameters + ------- + blob: c104.Byte32 + Binary command value + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> binary_cmd = c104.BinaryCmd(blob=c104.Byte32(1234), recorded_at=datetime.datetime.utcnow()) +)def", + "blob"_a, "recorded_at"_a = py::none()) + .def_property_readonly("blob", &Object::BinaryCmd::getBlob, + "c104.Byte32: the value (read-only)") + .def_property_readonly( + "value", &Object::BinaryCmd::getValue, + "c104.Byte32: maps to property ``direction`` (read-only). The setter " + "is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::BinaryCmd::getQuality, + "None: This information does not contain quality information.") + .def("__repr__", &Object::BinaryCmd::toString); + + py::class_>( + m, "NormalizedInfo", + "This class represents all specific normalized measurement point " + "information") + .def(py::init(&Object::NormalizedInfo::create), R"def( + __init__(self: c104.NormalizedInfo, actual: c104.NormalizedFloat, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new normalized measurement info + + Parameters + ------- + actual: c104.NormalizedFloat + Actual measurement value [-1.f, 1.f] + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> normalized_info = c104.NormalizedInfo(actual=c104.NormalizedFloat(23.45), quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "actual"_a, "quality"_a = Quality::None, + "recorded_at"_a = py::none()) + .def_property_readonly("actual", &Object::NormalizedInfo::getActual, + "c104.NormalizedFloat: the value (read-only)") + .def_property_readonly( + "value", &Object::NormalizedInfo::getValue, + "c104.NormalizedFloat: maps to property ``actual`` (read-only). The " + "setter is available via point.value=xyz") + .def_property_readonly("quality", &Object::NormalizedInfo::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def("__repr__", &Object::NormalizedInfo::toString); + + py::class_>( + m, "NormalizedCmd", + "This class represents all specific normalized set point command " + "information") + .def(py::init(&Object::NormalizedCmd::create), R"def( + __init__(self: c104.NormalizedCmd, target: c104.NormalizedFloat, qualifier: c104.UInt7 = c104.UInt7(0), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new normalized set point command + + Parameters + ------- + target: c104.NormalizedFloat + Target set-point value [-1.f, 1.f] + qualifier: c104.UInt7 + Qualifier of set-point command + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> normalized_cmd = c104.NormalizedCmd(target=c104.NormalizedFloat(23.45), qualifier=c104.UInt7(123), recorded_at=datetime.datetime.utcnow()) +)def", + "target"_a, "qualifier"_a = LimitedUInt7(0), + "recorded_at"_a = py::none()) + .def_property_readonly("target", &Object::NormalizedCmd::getTarget, + "c104.NormalizedFloat: the value (read-only)") + .def_property_readonly( + "qualifier", &Object::NormalizedCmd::getQualifier, + "c104.UInt7: the command qualifier information (read-only)") + .def_property_readonly( + "value", &Object::NormalizedCmd::getValue, + "c104.NormalizedFloat: maps to property ``target`` (read-only). The " + "setter is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::NormalizedCmd::getQuality, + "None: This information does not contain quality information.") + .def("__repr__", &Object::NormalizedCmd::toString); + + py::class_>( + m, "ScaledInfo", + "This class represents all specific scaled measurement point information") + .def(py::init(&Object::ScaledInfo::create), R"def( + __init__(self: c104.ScaledInfo, actual: c104.Int16, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new scaled measurement info + + Parameters + ------- + actual: c104.Int16 + Actual measurement value [-32768, 32767] + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> scaled_info = c104.ScaledInfo(actual=c104.Int16(-2345), quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "actual"_a, "quality"_a = Quality::None, + "recorded_at"_a = py::none()) + .def_property_readonly("actual", &Object::ScaledInfo::getActual, + "c104.Int16: the value (read-only)") + .def_property_readonly( + "value", &Object::ScaledInfo::getValue, + "c104.Int16: maps to property ``actual`` (read-only). The setter is " + "available via point.value=xyz") + .def_property_readonly("quality", &Object::ScaledInfo::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def("__repr__", &Object::ScaledInfo::toString); + + py::class_>( + m, "ScaledCmd", + "This class represents all specific scaled set point command information") + .def(py::init(&Object::ScaledCmd::create), R"def( + __init__(self: c104.ScaledCmd, target: c104.Int16, qualifier: c104.UInt7 = c104.UInt7(0), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new scaled set point command + + Parameters + ------- + target: c104.Int16 + Target set-point value [-32768, 32767] + qualifier: c104.UInt7 + Qualifier of set-point command + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> scaled_cmd = c104.ScaledCmd(target=c104.Int16(-2345), qualifier=c104.UInt7(123), recorded_at=datetime.datetime.utcnow()) +)def", + "target"_a, "qualifier"_a = LimitedUInt7(0), + "recorded_at"_a = py::none()) + .def_property_readonly("target", &Object::ScaledCmd::getTarget, + "c104.Int16: the value (read-only)") + .def_property_readonly( + "qualifier", &Object::ScaledCmd::getQualifier, + "c104.UInt7: the command qualifier information (read-only)") + .def_property_readonly( + "value", &Object::ScaledCmd::getValue, + "c104.Int16: maps to property ``target`` (read-only). The setter is " + "available via point.value=xyz") + .def_property_readonly( + "quality", &Object::ScaledCmd::getQuality, + "None: This information does not contain quality information.") + .def("__repr__", &Object::ScaledCmd::toString); + + py::class_>( + m, "ShortInfo", + "This class represents all specific short measurement point information") + .def(py::init(&Object::ShortInfo::create), R"def( + __init__(self: c104.ShortInfo, actual: float, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new short measurement info + + Parameters + ------- + actual: float + Actual measurement value in 32-bit precision + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> short_info = c104.ShortInfo(actual=23.45, quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "actual"_a, "quality"_a = Quality::None, + "recorded_at"_a = py::none()) + .def_property_readonly("actual", &Object::ShortInfo::getActual, + "float: the value (read-only)") + .def_property_readonly("value", &Object::ShortInfo::getValue, + "float: maps to property ``actual`` (read-only). " + "The setter is available via point.value=xyz") + .def_property_readonly("quality", &Object::ShortInfo::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def("__repr__", &Object::ShortInfo::toString); + + py::class_>( + m, "ShortCmd", + "This class represents all specific short set point command information") + .def(py::init(&Object::ShortCmd::create), R"def( + __init__(self: c104.ShortCmd, target: float, qualifier: c104.UInt7 = c104.UInt7(0), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new short set point command + + Parameters + ------- + target: float + Target set-point value in 32-bit precision + qualifier: c104.UInt7 + Qualifier of set-point command + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> short_cmd = c104.ShortCmd(target=-23.45, qualifier=c104.UInt7(123), recorded_at=datetime.datetime.utcnow()) +)def", + "target"_a, "qualifier"_a = LimitedUInt7(0), + "recorded_at"_a = py::none()) + .def_property_readonly("target", &Object::ShortCmd::getTarget, + "float: the value (read-only)") + .def_property_readonly( + "qualifier", &Object::ShortCmd::getQualifier, + "c104.UInt7: the command qualifier information (read-only)") + .def_property_readonly("value", &Object::ShortCmd::getValue, + "float: maps to property ``target`` (read-only). " + "The setter is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::ShortCmd::getQuality, + "None: This information does not contain quality information.") + .def("__repr__", &Object::ShortCmd::toString); + + py::class_>( + m, "BinaryCounterInfo", + "This class represents all specific integrated totals of binary counter " + "point information") + .def(py::init(&Object::BinaryCounterInfo::create), R"def( + __init__(self: c104.BinaryCounterInfo, counter: int, sequence: c104.UInt5, quality: c104.BinaryCounterQuality = c104.BinaryCounterQuality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new short measurement info + + Parameters + ------- + counter: int + Counter value + sequence: c104.UInt5 + Counter info sequence number + quality: c104.BinaryCounterQuality + Binary counter quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> counter_info = c104.BinaryCounterInfo(counter=2345, sequence=c104.UInt5(35), quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "counter"_a, "sequence"_a = LimitedUInt5(0), + "quality"_a = Quality::None, "recorded_at"_a = py::none()) + .def_property_readonly("counter", &Object::BinaryCounterInfo::getCounter, + "int: the value (read-only)") + .def_property_readonly( + "sequence", &Object::BinaryCounterInfo::getSequence, + "c104.UInt5: the counter sequence number (read-only)") + .def_property_readonly("value", &Object::BinaryCounterInfo::getValue, + "int: maps to property ``counter`` (read-only). " + "The setter is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::BinaryCounterInfo::getQuality, + "c104.BinaryCounterQuality: the quality (read-only). The setter is " + "available via point.quality=xyz") + .def("__repr__", &Object::BinaryCounterInfo::toString); + + py::class_>( + m, "ProtectionEventInfo", + "This class represents all specific protection equipment single event " + "point information") + .def(py::init(&Object::ProtectionEquipmentEventInfo::create), R"def( + __init__(self: c104.ProtectionEventInfo, state: c104.EventState, elapsed_ms: c104.UInt16, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new event info raised by protection equipment + + Parameters + ------- + state: c104.EventState + State of the event + elapsed_ms: c104.UInt16 + Time in milliseconds elapsed + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> single_event = c104.ProtectionEventInfo(state=c104.EventState.ON, elapsed_ms=c104.UInt16(35000), quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "state"_a, "elapsed_ms"_a = LimitedUInt16(0), + "quality"_a = Quality::None, "recorded_at"_a = py::none()) + .def_property_readonly("state", + &Object::ProtectionEquipmentEventInfo::getState, + "c104.EventState: the state (read-only)") + .def_property_readonly( + "value", &Object::ProtectionEquipmentEventInfo::getValue, + "c104.EventState: maps to property ``state`` (read-only). The setter " + "is available via point.value=xyz") + .def_property_readonly("quality", + &Object::ProtectionEquipmentEventInfo::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def_property_readonly( + "elapsed_ms", &Object::ProtectionEquipmentEventInfo::getElapsed_ms, + "int: the elapsed time in milliseconds (read-only)") + .def("__repr__", &Object::ProtectionEquipmentEventInfo::toString); + + py::class_>( + m, "ProtectionStartInfo", + "This class represents all specific protection equipment packed start " + "events point information") + .def(py::init(&Object::ProtectionEquipmentStartEventsInfo::create), + R"def( + __init__(self: c104.ProtectionStartInfo, events: c104.StartEvents, relay_duration_ms: c104.UInt16, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new packed event start info raised by protection equipment + + Parameters + ------- + events: c104.StartEvents + Set of start events + relay_duration_ms: c104.UInt16 + Time in milliseconds of relay duration + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> start_events = c104.ProtectionStartInfo(events=c104.StartEvents.ON, relay_duration_ms=c104.UInt16(35000), quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "events"_a, "relay_duration_ms"_a = LimitedUInt16(0), + "quality"_a = Quality::None, "recorded_at"_a = py::none()) + .def_property_readonly( + "events", &Object::ProtectionEquipmentStartEventsInfo::getEvents, + "c104.StartEvents: the started events (read-only)") + .def_property_readonly( + "relay_duration_ms", + &Object::ProtectionEquipmentStartEventsInfo::getRelayDuration_ms, + "int: the relay duration information (read-only)") + .def_property_readonly( + "value", &Object::ProtectionEquipmentStartEventsInfo::getValue, + "c104.StartEvents: maps to property ``events`` (read-only). The " + "setter is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::ProtectionEquipmentStartEventsInfo::getQuality, + "c104.Quality: the quality (read-only). The setter is available via " + "point.quality=xyz") + .def("__repr__", &Object::ProtectionEquipmentStartEventsInfo::toString); + + py::class_>( + m, "ProtectionCircuitInfo", + "This class represents all specific protection equipment output circuit " + "point information") + .def(py::init(&Object::ProtectionEquipmentOutputCircuitInfo::create), + R"def( + __init__(self: c104.ProtectionCircuitInfo, circuits: c104.OutputCircuits, relay_operating_ms: c104.UInt16, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new output circuits info raised by protection equipment + + Parameters + ------- + circuits: c104.OutputCircuits + Set of output circuits + relay_operating_ms: c104.UInt16 + Time in milliseconds of relay operation + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> output_circuits = c104.ProtectionCircuitInfo(events=c104.OutputCircuits.PhaseL1|c104.OutputCircuits.PhaseL2, relay_operating_ms=c104.UInt16(35000), quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "events"_a, "relay_duration_ms"_a = LimitedUInt16(0), + "quality"_a = Quality::None, "recorded_at"_a = py::none()) + .def_property_readonly( + "circuits", + &Object::ProtectionEquipmentOutputCircuitInfo::getCircuits, + "c104.OutputCircuits: the started events (read-only)") + .def_property_readonly( + "relay_operating_ms", + &Object::ProtectionEquipmentOutputCircuitInfo::getRelayOperating_ms, + "int: the relay operation duration information (read-only)") + .def_property_readonly( + "value", &Object::ProtectionEquipmentOutputCircuitInfo::getValue, + "c104.OutputCircuits: maps to property ``circuits`` (read-only). The " + "setter is available via point.value=xyz") + .def_property_readonly( + "quality", &Object::ProtectionEquipmentOutputCircuitInfo::getQuality, + "c104.Quality: the quality (read-only). The setter is available via " + "point.quality=xyz") + .def("__repr__", &Object::ProtectionEquipmentOutputCircuitInfo::toString); + + py::class_>( + m, "StatusAndChanged", + "This class represents all specific packed status point information with " + "change detection") + .def(py::init(&Object::StatusWithChangeDetection::create), R"def( + __init__(self: c104.StatusAndChanged, status: c104.PackedSingle, changed: c104.PackedSingle, quality: c104.Quality = c104.Quality(), recorded_at: Optional[datetime.datetime] = None) -> None + + create a new event info raised by protection equipment + + Parameters + ------- + status: c104.PackedSingle + Set of current single values + changed: c104.PackedSingle + Set of changed single values + quality: c104.Quality + Quality information + recorded_at: typing.Optional[datetime.datetime] + Timestamp contained in the protocol message, or None if the protocol message type does not contain a timestamp. + + Example + ------- + >>> status_and_changed = c104.StatusAndChanged(status=c104.PackedSingle.I0|c104.PackedSingle.I5, changed=c104.PackedSingle(15), quality=c104.Quality.Invalid, recorded_at=datetime.datetime.utcnow()) +)def", + "status"_a, "changed"_a = FieldSet16(0), "quality"_a = Quality::None, + "recorded_at"_a = py::none()) + .def_property_readonly( + "status", &Object::StatusWithChangeDetection::getStatus, + "c104.PackedSingle: the current status (read-only)") + .def_property_readonly( + "changed", &Object::StatusWithChangeDetection::getChanged, + "c104.PackedSingle: the changed information (read-only)") + .def_property_readonly( + "value", &Object::StatusWithChangeDetection::getValue, + "c104.PackedSingle: maps to property ``status`` (read-only). The " + "setter is available via point.value=xyz") + .def_property_readonly("quality", + &Object::StatusWithChangeDetection::getQuality, + "c104.Quality: the quality (read-only). The " + "setter is available via point.quality=xyz") + .def("__repr__", &Object::StatusWithChangeDetection::toString); py::class_>( @@ -2102,52 +2850,39 @@ PY_MODULE(c104, m) { "This class represents incoming messages and provides access to " "structured properties interpreted from incoming messages") .def_property_readonly("type", &Remote::Message::IncomingMessage::getType, - ":ref:`c104.Type`: iec60870 type (read-only)", + "c104.Type: iec60870 type (read-only)", py::return_value_policy::copy) .def_property_readonly( "common_address", &Remote::Message::IncomingMessage::getCommonAddress, - "int: common address (0-65535) (read-only)", + "int: common address (1-65534) (read-only)", py::return_value_policy::copy) .def_property_readonly( "originator_address", &Remote::Message::IncomingMessage::getOriginatorAddress, "int: originator address (0-255) (read-only)", py::return_value_policy::copy) - .def_property_readonly("io_address", - &Remote::Message::IncomingMessage::getIOA, - "int: information object address (read-only)", - py::return_value_policy::copy) .def_property_readonly( - "connection_string", - &Remote::Message::IncomingMessage::getConnectionString, - "str: connection string in format :code:`ip:port` (read-only)", + "io_address", &Remote::Message::IncomingMessage::getIOA, + "int: information object address (0-16777215) (read-only)", py::return_value_policy::copy) .def_property_readonly( "cot", &Remote::Message::IncomingMessage::getCauseOfTransmission, - ":ref:`c104.Cot`: cause of transmission (read-only)", - py::return_value_policy::copy) - .def_property_readonly( - "value", &Remote::Message::IncomingMessage::getValue, - "float: value (read-only)", py::return_value_policy::copy) - .def_property_readonly( - "quality", &Remote::Message::IncomingMessage::getQuality, - ":ref:`c104.Quality`: quality restrictions bitset object (read-only)", + "c104.Cot: cause of transmission (read-only)", py::return_value_policy::copy) + .def_property_readonly("info", &Remote::Message::IncomingMessage::getInfo, + "c104.Information: value (read-only)") .def_property_readonly("is_test", &Remote::Message::IncomingMessage::isTest, - "bool: test if test flag is set (read-only)", - py::return_value_policy::copy) + "bool: test if test flag is set (read-only)") .def_property_readonly("is_sequence", &Remote::Message::IncomingMessage::isSequence, - "bool: test if sequence flag is set (read-only)", - py::return_value_policy::copy) + "bool: test if sequence flag is set (read-only)") .def_property_readonly("is_negative", &Remote::Message::IncomingMessage::isNegative, - "bool: test if negative flag is set (read-only)", - py::return_value_policy::copy) + "bool: test if negative flag is set (read-only)") .def_property_readonly("raw", &IncomingMessage_getRawBytes, "bytes: asdu message bytes (read-only)", - py::return_value_policy::copy) + py::return_value_policy::take_ownership) .def_property_readonly( "raw_explain", &Remote::Message::IncomingMessage::getRawMessageString, "str: asdu message bytes explained (read-only)", @@ -2162,12 +2897,6 @@ PY_MODULE(c104, m) { "bool: test if message is a point command and has " "select flag set (read-only)", py::return_value_policy::copy) - .def_property_readonly( - "command_qualifier", - &Remote::Message::IncomingMessage::getCommandQualifier, - ":ref:`c104.Qoc`: duration parameter, only for single, double " - "and regulating step command messages (read-only)", - py::return_value_policy::copy) .def("first", &Remote::Message::IncomingMessage::first, R"def( first(self: c104.IncomingMessage) -> None @@ -2186,12 +2915,11 @@ PY_MODULE(c104, m) { ------- bool True, if another information element exists, otherwise False -)def"); +)def") + .def("__repr__", &Remote::Message::IncomingMessage::toString); + ; //*/ -#ifdef VERSION_INFO - m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); -#else - m.attr("__version__") = "dev"; -#endif + + m.attr("__version__") = VERSION_INFO; } diff --git a/src/remote/Connection.cpp b/src/remote/Connection.cpp index 27fa69a..b1f6b0a 100644 --- a/src/remote/Connection.cpp +++ b/src/remote/Connection.cpp @@ -43,16 +43,16 @@ using namespace std::chrono_literals; Connection::Connection( std::shared_ptr _client, const std::string &_ip, - const uint_fast16_t _port, const uint_fast32_t command_timeout_ms, + const uint_fast16_t _port, const uint_fast16_t command_timeout_ms, const ConnectionInit _init, std::shared_ptr transport_security, const uint_fast8_t originator_address) - : client(_client), ip(_ip), port(_port), + : client(_client), ip(_ip), port(_port), init(_init), commandTimeout_ms(command_timeout_ms) { Assert_IPv4(_ip); Assert_Port(_port); - if (transport_security.get() != nullptr) { + if (transport_security) { connection = CS104_Connection_createSecure(ip.c_str(), _port, transport_security->get()); } else { @@ -68,7 +68,7 @@ Connection::Connection( // timeout elapsed without confirmation the connection will // be closed. This is used by the sender to determine if // the receiver has failed to confirm a message. - param->t1 = std::max(1, (int)round(command_timeout_ms / 1000)); + param->t1 = 1; // Timeout to confirm messages (in s). This timeout is used by // the receiver to determine the time when the message // confirmation has to be sent. @@ -77,7 +77,6 @@ Connection::Connection( // connection CS104_Connection_setAPCIParameters(connection, param); - init.store(_init); if (originator_address > 0) { setOriginatorAddress(originator_address); } @@ -113,9 +112,13 @@ void Connection::setState(ConnectionState connectionState) { if (prev != connectionState) { state.store(connectionState); if (py_onStateChange.is_set()) { - DEBUG_PRINT(Debug::Connection, "CALLBACK on_state_change"); - Module::ScopedGilAcquire const scoped("Connection.on_state_change"); - py_onStateChange.call(shared_from_this(), connectionState); + if (auto c = getClient()) { + c->scheduleTask([this, connectionState]() { + DEBUG_PRINT(Debug::Connection, "CALLBACK on_state_change"); + Module::ScopedGilAcquire const scoped("Connection.on_state_change"); + this->py_onStateChange.call(shared_from_this(), connectionState); + }); + } } DEBUG_PRINT(Debug::Connection, "state] " + ConnectionState_toString(prev) + " -> " + @@ -149,51 +152,65 @@ std::shared_ptr Connection::getClient() const { void Connection::connect() { ConnectionState const current = state.load(); - if (CLOSED != current && CLOSED_AWAIT_RECONNECT != current) + if (current == OPEN || current == OPEN_MUTED || CLOSED_AWAIT_OPEN == current) return; - Module::ScopedGilRelease const scoped("Connection.connect"); - // bool success = false; + if (current == OPEN_AWAIT_CLOSED) { + DEBUG_PRINT(Debug::Connection, + "connect] Wait for closing before reconnecting to " + + getConnectionString()); + setState(CLOSED_AWAIT_OPEN); + return; + } + + DEBUG_PRINT(Debug::Connection, + "connect] Asynchronous connect to " + getConnectionString()); // connect setState(CLOSED_AWAIT_OPEN); - { - std::lock_guard const lock(connection_mutex); - // free connection thread if exists - CS104_Connection_close(connection); - // reconnect - CS104_Connection_connectAsync(connection); - } + std::lock_guard const lock(connection_mutex); - DEBUG_PRINT(Debug::Connection, - "connect] Asynchronous connect to " + getConnectionString()); + // free connection thread if exists + CS104_Connection_close(connection); + + // reconnect + CS104_Connection_connectAsync(connection); } void Connection::disconnect() { - { - // free connection thread - std::lock_guard const lock(connection_mutex); - CS104_Connection_close(connection); + ConnectionState const current = state.load(); + if (CLOSED == current || OPEN_AWAIT_CLOSED == current) { + return; } - if (isOpen()) { - setState(OPEN_AWAIT_CLOSED); - - // print + if (current == CLOSED_AWAIT_OPEN) { DEBUG_PRINT(Debug::Connection, - "disconnect] Disconnect from " + getConnectionString()); + "connect] Wait for opening before closing to " + + getConnectionString()); + setState(OPEN_AWAIT_CLOSED); + return; + } - // Thread_sleep(1000); - } else { + if (CLOSED_AWAIT_RECONNECT == current) { setState(CLOSED); + } else { + setState(OPEN_AWAIT_CLOSED); } + + // free connection thread + std::unique_lock lock(connection_mutex); + CS104_Connection_close(connection); + lock.unlock(); + + // print + DEBUG_PRINT(Debug::Connection, + "disconnect] Disconnect from " + getConnectionString()); } bool Connection::isOpen() const { ConnectionState const current = state.load(); - return OPEN_MUTED == current || OPEN_AWAIT_INTERROGATION == current || - OPEN_AWAIT_CLOCK_SYNC == current || OPEN == current; + return current == OPEN || current == OPEN_MUTED; } bool Connection::isMuted() const { return state == OPEN_MUTED; } @@ -228,101 +245,130 @@ bool Connection::unmute() { return true; } -bool Connection::setMuted(bool value) { - bool const result = OPEN_MUTED == state.load(); - - if (isOpen()) { +void Connection::setMuted(bool value) { + ConnectionState current = state.load(); + if (OPEN == current && value) { // print DEBUG_PRINT(Debug::Connection, - "set_muted] Muted: " + std::to_string(value) + - " for connection to " + getConnectionString()); - - if (value) { - setState(OPEN_MUTED); - } else { - switch (init) { - case INIT_ALL: - case INIT_INTERROGATION: - setState(OPEN_AWAIT_INTERROGATION); - break; - case INIT_CLOCK_SYNC: - setState(OPEN_AWAIT_CLOCK_SYNC); - default: - setState(OPEN); + "set_muted] Muted connection to " + getConnectionString()); + setState(OPEN_MUTED); + } else if (OPEN_MUTED == current && !value) { + // print + DEBUG_PRINT(Debug::Connection, + "set_muted] Unmuted connection to " + getConnectionString()); + + switch (init) { + case INIT_ALL: + if (auto c = getClient()) { + c->scheduleTask( + [this]() { + this->interrogation(IEC60870_GLOBAL_COMMON_ADDRESS, + CS101_COT_ACTIVATION, QOI_STATION, true); + this->clockSync(IEC60870_GLOBAL_COMMON_ADDRESS, true); + setState(OPEN); + }, + -1); + } + break; + case INIT_INTERROGATION: + if (auto c = getClient()) { + c->scheduleTask( + [this]() { + this->interrogation(IEC60870_GLOBAL_COMMON_ADDRESS, + CS101_COT_ACTIVATION, QOI_STATION, true); + setState(OPEN); + }, + -1); } + break; + case INIT_CLOCK_SYNC: + if (auto c = getClient()) { + c->scheduleTask( + [this]() { + this->clockSync(IEC60870_GLOBAL_COMMON_ADDRESS, true); + setState(OPEN); + }, + -1); + } + break; + default: + setState(OPEN); } } - return result; } -bool Connection::setOpen() { - DEBUG_PRINT(Debug::Connection, "set_open] " + getConnectionString()); +void Connection::setOpen() { // DO NOT LOCK connection_mutex: connect locks - ConnectionState const current = state.load(); + ConnectionState current = state.load(); - if (current == OPEN || current == OPEN_MUTED || - current == OPEN_AWAIT_CLOSED) { + if (OPEN == current || OPEN_MUTED == current) { // print DEBUG_PRINT(Debug::Connection, "set_open] Already opened to " + getConnectionString()); - return false; - } else { - // print - DEBUG_PRINT(Debug::Connection, - "set_open] Opening connection to " + getConnectionString()); + return; } - setState(OPEN_MUTED); + if (OPEN_AWAIT_CLOSED == current) { + if (auto c = getClient()) { + c->scheduleTask([this]() { this->disconnect(); }, -1); + } + return; + } + + if (INIT_MUTED != init) { + if (auto c = getClient()) { + c->scheduleTask([this]() { this->unmute(); }, -1); + } + } connectionCount++; - connectedAt_ms.store(GetTimestamp_ms()); + connectedAt.store(std::chrono::system_clock::now()); + setState(OPEN_MUTED); DEBUG_PRINT(Debug::Connection, - "set_open] Unmuting connection to " + getConnectionString()); - unmute(); - DEBUG_PRINT(Debug::Connection, "set_open] DONE " + getConnectionString()); - return true; + "set_open] Opened connection to " + getConnectionString()); } -bool Connection::setClosed() { +void Connection::setClosed() { // DO NOT LOCK connection_mutex: disconnect locks ConnectionState const current = state.load(); if (CLOSED == current) { - // print - DEBUG_PRINT(Debug::Connection, "set_closed] Already closed to " + - getConnectionString() + " | State " + - ConnectionState_toString(current)); - return false; - } else { // print DEBUG_PRINT(Debug::Connection, - "set_closed] Connection closed to " + getConnectionString()); + "set_closed] Already closed to " + getConnectionString()); + return; } if (CLOSED_AWAIT_OPEN != current && CLOSED_AWAIT_RECONNECT != current) { // set disconnected if connected previously - disconnectedAt_ms.store(GetTimestamp_ms()); + disconnectedAt.store(std::chrono::system_clock::now()); } // controlled close or connection lost? - setState((OPEN_AWAIT_CLOSED == current) ? CLOSED : CLOSED_AWAIT_RECONNECT); - return true; + if (OPEN_AWAIT_CLOSED == current) { + setState(CLOSED); + } else { + setState(CLOSED_AWAIT_RECONNECT); + if (auto c = getClient()) { + c->scheduleTask([this]() { this->connect(); }, 1000); + } + } + + DEBUG_PRINT(Debug::Connection, + "set_closed] Connection closed to " + getConnectionString()); } -bool Connection::prepareCommandSuccess( +void Connection::prepareCommandSuccess( const std::string &cmdId, CommandProcessState const process_state = COMMAND_AWAIT_CON) { std::lock_guard const map_lock( expectedResponseMap_mutex); - expectedResponseMap[cmdId] = process_state; - if (process_state > COMMAND_AWAIT_CON) { - if (sequenceId.empty()) { - sequenceId = cmdId; - } else { - return false; - } + auto const it = expectedResponseMap.find(cmdId); + if (it != expectedResponseMap.end()) { + throw std::runtime_error("[c104.Connection] command " + cmdId + + " already running!"); } - return true; + expectedResponseMap[cmdId] = process_state; } bool Connection::awaitCommandSuccess(const std::string &cmdId) { @@ -332,7 +378,8 @@ bool Connection::awaitCommandSuccess(const std::string &cmdId) { auto const it = expectedResponseMap.find(cmdId); if (it != expectedResponseMap.end()) { - auto const end = std::chrono::system_clock::now() + commandTimeout_ms * 1ms; + auto const end = std::chrono::steady_clock::now() + + std::chrono::milliseconds(commandTimeout_ms.load()); DEBUG_PRINT(Debug::Connection, "await_command_success] Await " + cmdId); @@ -371,13 +418,8 @@ bool Connection::awaitCommandSuccess(const std::string &cmdId) { } // print - DEBUG_PRINT(Debug::Connection, - "await_command_success] Stats " + cmdId + " | TOTAL " + - std::to_string( - std::chrono::duration_cast( - end - std::chrono::system_clock::now()) - .count()) + - u8" \xb5s"); + DEBUG_PRINT(Debug::Connection, "await_command_success] Stats " + cmdId + + " | TOTAL " + TICTOCNOW(end)); } map_lock.unlock(); @@ -393,31 +435,24 @@ void Connection::setCommandSuccess( ConnectionState const current = state.load(); bool found = false; - // continue init procedure event though the response might be negative - if (type == C_CS_NA_1 && current == OPEN_AWAIT_CLOCK_SYNC) { - setState(OPEN); - } - - if (type == C_IC_NA_1 && current == OPEN_AWAIT_INTERROGATION) { - if (init == INIT_INTERROGATION) { - setState(OPEN); - } else { - setState(OPEN_AWAIT_CLOCK_SYNC); - } - } - - std::string const cmdId = std::to_string(message->getCommonAddress()) + "-" + - TypeID_toString(type) + "-" + - std::to_string(message->getIOA()); - - // print - DEBUG_PRINT(Debug::Connection, "set_command_success] Result " + cmdId + ": " + - std::to_string(!message->isNegative())); + std::string cmdId = std::to_string(message->getCommonAddress()) + "-" + + TypeID_toString(type) + "-" + + std::to_string(message->getIOA()); + std::string const cmdIdAlt = std::to_string(IEC60870_GLOBAL_COMMON_ADDRESS) + + "-" + TypeID_toString(type) + "-" + + std::to_string(message->getIOA()); { std::lock_guard const map_lock( expectedResponseMap_mutex); auto it = expectedResponseMap.find(cmdId); + if (it == expectedResponseMap.end()) { + // try global common address + it = expectedResponseMap.find(cmdIdAlt); + if (it != expectedResponseMap.end()) { + cmdId = cmdIdAlt; + } + } if (it != expectedResponseMap.end()) { found = true; if (message->isNegative()) { @@ -444,22 +479,22 @@ void Connection::setCommandSuccess( break; case COMMAND_AWAIT_REQUEST: expectedResponseMap[cmdId] = - (message->getCauseOfTransmission() == CS101_COT_REQUEST) + (message->getCauseOfTransmission() == CS101_COT_ACTIVATION_CON || + message->getCauseOfTransmission() == CS101_COT_REQUEST) ? COMMAND_SUCCESS : COMMAND_FAILURE; break; default: expectedResponseMap[cmdId] = COMMAND_SUCCESS; } - // clear active sequence - if (expectedResponseMap[cmdId] == COMMAND_SUCCESS && - sequenceId == cmdId) { - sequenceId = ""; - } } } } + // print + DEBUG_PRINT(Debug::Connection, "set_command_success] Result " + cmdId + ": " + + std::to_string(!message->isNegative()) + + " | found: " + std::to_string(found)); // notify about update if (found) { response_wait.notify_all(); @@ -473,9 +508,6 @@ void Connection::cancelCommandSuccess(const std::string &cmdId) { if (it != expectedResponseMap.end()) { expectedResponseMap.erase(it); } - if (sequenceId == cmdId) { - sequenceId = ""; - } } bool Connection::hasStations() const { @@ -533,13 +565,22 @@ void Connection::setOnReceiveRawCallback(py::object &callable) { void Connection::onReceiveRaw(unsigned char *msg, unsigned char msgSize) { if (py_onReceiveRaw.is_set()) { - DEBUG_PRINT(Debug::Connection, "CALLBACK on_receive_raw"); - Module::ScopedGilAcquire const scoped("Connection.on_receive_raw"); - PyObject *pymemview = - PyMemoryView_FromMemory((char *)msg, msgSize, PyBUF_READ); - PyObject *pybytes = PyBytes_FromObject(pymemview); + if (auto c = getClient()) { + // create a copy + auto *cp = new char[msgSize]; + memcpy(cp, msg, msgSize); + + c->scheduleTask([this, cp, msgSize]() { + DEBUG_PRINT(Debug::Connection, "CALLBACK on_receive_raw"); + Module::ScopedGilAcquire const scoped("Connection.on_receive_raw"); + PyObject *pymemview = PyMemoryView_FromMemory(cp, msgSize, PyBUF_READ); + PyObject *pybytes = PyBytes_FromObject(pymemview); - py_onReceiveRaw.call(shared_from_this(), py::handle(pybytes)); + this->py_onReceiveRaw.call(shared_from_this(), py::handle(pybytes)); + + delete[] cp; + }); + } } } @@ -549,13 +590,22 @@ void Connection::setOnSendRawCallback(py::object &callable) { void Connection::onSendRaw(unsigned char *msg, unsigned char msgSize) { if (py_onSendRaw.is_set()) { - DEBUG_PRINT(Debug::Connection, "CALLBACK on_send_raw"); - Module::ScopedGilAcquire const scoped("Connection.on_send_raw"); - PyObject *pymemview = - PyMemoryView_FromMemory((char *)msg, msgSize, PyBUF_READ); - PyObject *pybytes = PyBytes_FromObject(pymemview); + if (auto c = getClient()) { + // create a copy + auto *cp = new char[msgSize]; + memcpy(cp, msg, msgSize); + + c->scheduleTask([this, cp, msgSize]() { + DEBUG_PRINT(Debug::Connection, "CALLBACK on_send_raw"); + Module::ScopedGilAcquire const scoped("Connection.on_send_raw"); + PyObject *pymemview = PyMemoryView_FromMemory(cp, msgSize, PyBUF_READ); + PyObject *pybytes = PyBytes_FromObject(pymemview); + + this->py_onSendRaw.call(shared_from_this(), py::handle(pybytes)); - py_onSendRaw.call(shared_from_this(), py::handle(pybytes)); + delete[] cp; + }); + } } } @@ -563,6 +613,22 @@ void Connection::setOnStateChangeCallback(py::object &callable) { py_onStateChange.reset(callable); } +std::optional +Connection::getConnectedAt() const { + if (isOpen()) { + return connectedAt.load(); + } + return std::nullopt; +} + +std::optional +Connection::getDisconnectedAt() const { + if (!isOpen()) { + return disconnectedAt.load(); + } + return std::nullopt; +} + bool Connection::interrogation(std::uint_fast16_t commonAddress, CS101_CauseOfTransmission cause, CS101_QualifierOfInterrogation qualifier, @@ -577,9 +643,8 @@ bool Connection::interrogation(std::uint_fast16_t commonAddress, std::to_string(qualifier)); std::string const cmdId = std::to_string(commonAddress) + "-C_IC_NA_1-0"; - if (wait_for_response && - !prepareCommandSuccess(cmdId, COMMAND_AWAIT_CON_TERM)) { - return false; + if (wait_for_response) { + prepareCommandSuccess(cmdId, COMMAND_AWAIT_CON_TERM); } std::unique_lock lock(connection_mutex); @@ -611,9 +676,8 @@ bool Connection::counterInterrogation(std::uint_fast16_t commonAddress, std::to_string(qualifier)); std::string const cmdId = std::to_string(commonAddress) + "-C_CI_NA_1-0"; - if (wait_for_response && - !prepareCommandSuccess(cmdId, COMMAND_AWAIT_CON_TERM)) { - return false; + if (wait_for_response) { + prepareCommandSuccess(cmdId, COMMAND_AWAIT_CON_TERM); } std::unique_lock lock(connection_mutex); @@ -639,12 +703,12 @@ bool Connection::clockSync(std::uint_fast16_t commonAddress, return false; std::string const cmdId = std::to_string(commonAddress) + "-C_CS_NA_1-0"; - if (wait_for_response && !prepareCommandSuccess(cmdId)) { - return false; + if (wait_for_response) { + prepareCommandSuccess(cmdId); } - sCP56Time2a time; - CP56Time2a_createFromMsTimestamp(&time, GetTimestamp_ms()); + sCP56Time2a time{}; + from_time_point(&time, std::chrono::system_clock::now()); std::unique_lock lock(connection_mutex); bool const result = @@ -669,13 +733,13 @@ bool Connection::test(std::uint_fast16_t commonAddress, bool with_time, return false; std::string const cmdId = std::to_string(commonAddress) + "-C_TS_TA_1-0"; - if (wait_for_response && !prepareCommandSuccess(cmdId)) { - return false; + if (wait_for_response) { + prepareCommandSuccess(cmdId); } if (with_time) { - sCP56Time2a time; - CP56Time2a_createFromMsTimestamp(&time, GetTimestamp_ms()); + sCP56Time2a time{}; + from_time_point(&time, std::chrono::system_clock::now()); std::unique_lock lock(connection_mutex); bool const result = CS104_Connection_sendTestCommandWithTimestamp( @@ -708,8 +772,7 @@ bool Connection::test(std::uint_fast16_t commonAddress, bool with_time, } bool Connection::transmit(std::shared_ptr point, - const CS101_CauseOfTransmission cause, - const CS101_QualifierOfCommand qualifier) { + const CS101_CauseOfTransmission cause) { auto type = point->getType(); // is a supported control command? @@ -720,7 +783,7 @@ bool Connection::transmit(std::shared_ptr point, bool selectAndExecute = point->getCommandMode() == SELECT_AND_EXECUTE_COMMAND; // send select command if (selectAndExecute) { - auto message = Message::PointCommand::create(point, true, qualifier); + auto message = Message::PointCommand::create(point, true); message->setCauseOfTransmission(cause); // Select success ? if (!command(std::move(message), true)) { @@ -729,7 +792,7 @@ bool Connection::transmit(std::shared_ptr point, } // send execute command - auto message = Message::PointCommand::create(point, false, qualifier); + auto message = Message::PointCommand::create(point, false); message->setCauseOfTransmission(cause); if (selectAndExecute) { // wait for ACT_TERM after ACT_CON @@ -749,8 +812,8 @@ bool Connection::command(std::shared_ptr message, std::string const cmdId = std::to_string(message->getCommonAddress()) + "-" + TypeID_toString(message->getType()) + "-" + std::to_string(message->getIOA()); - if (wait_for_response && !prepareCommandSuccess(cmdId, state)) { - return false; + if (wait_for_response) { + prepareCommandSuccess(cmdId, state); } std::unique_lock lock(connection_mutex); @@ -788,9 +851,8 @@ bool Connection::read(std::shared_ptr point, std::string const cmdId = std::to_string(ca) + "-C_RD_NA_1-" + std::to_string(ioa); - if (wait_for_response && - !prepareCommandSuccess(cmdId, COMMAND_AWAIT_REQUEST)) { - return false; + if (wait_for_response) { + prepareCommandSuccess(cmdId, COMMAND_AWAIT_REQUEST); } std::unique_lock lock(connection_mutex); @@ -816,7 +878,15 @@ void Connection::rawMessageHandler(void *parameter, uint_fast8_t *msg, begin = std::chrono::steady_clock::now(); } - auto instance = (static_cast(parameter)->shared_from_this()); + std::shared_ptr instance{}; + + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Connection, "Ignore raw message in shutdown"); + return; + } + if (sent) { instance->onSendRaw(msg, msgSize); } else { @@ -825,14 +895,9 @@ void Connection::rawMessageHandler(void *parameter, uint_fast8_t *msg, if (debug) { end = std::chrono::steady_clock::now(); - DEBUG_PRINT_CONDITION( - true, Debug::Connection, - "raw_message_handler] Stats | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Connection, + "raw_message_handler] Stats | TOTAL " + + TICTOC(begin, end)); } } @@ -847,17 +912,23 @@ void Connection::connectionHandler(void *parameter, CS104_Connection connection, begin = std::chrono::steady_clock::now(); } - auto instance = (static_cast(parameter)->shared_from_this()); + std::shared_ptr instance{}; - switch (event) { - case CS104_CONNECTION_FAILED: { - instance->setClosed(); - break; + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Connection, "Ignore connection event " + + ConnectionEvent_toString(event) + + " in shutdown"); + return; } + + switch (event) { case CS104_CONNECTION_OPENED: { instance->setOpen(); break; } + case CS104_CONNECTION_FAILED: case CS104_CONNECTION_CLOSED: { instance->setClosed(); break; @@ -874,15 +945,11 @@ void Connection::connectionHandler(void *parameter, CS104_Connection connection, if (debug) { end = std::chrono::steady_clock::now(); - DEBUG_PRINT_CONDITION( - true, Debug::Connection, - "connection_handler] Connection " + ConnectionEvent_toString(event) + - " to " + instance->getConnectionString() + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + DEBUG_PRINT_CONDITION(true, Debug::Connection, + "connection_handler] Connection " + + ConnectionEvent_toString(event) + " to " + + instance->getConnectionString() + " | TOTAL " + + TICTOC(begin, end)); } } @@ -896,9 +963,15 @@ bool Connection::asduHandler(void *parameter, int address, CS101_ASDU asdu) { begin = std::chrono::steady_clock::now(); } - auto instance = (static_cast(parameter)->shared_from_this()); - auto client = instance->getClient(); + std::shared_ptr instance{}; + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + DEBUG_PRINT(Debug::Connection, "asdu_handler] Connection removed"); + return false; + } + auto client = instance->getClient(); if (!client || !client->isRunning()) { DEBUG_PRINT_CONDITION(debug, Debug::Connection, "asdu_handler] Client stopped"); @@ -931,8 +1004,6 @@ bool Connection::asduHandler(void *parameter, int address, CS101_ASDU asdu) { } while (message->next()) { - // @todo add invalid - auto station = instance->getStation(message->getCommonAddress()); if (!station) { client->onNewStation(instance, message->getCommonAddress()); @@ -941,13 +1012,13 @@ bool Connection::asduHandler(void *parameter, int address, CS101_ASDU asdu) { if (station) { auto point = station->getPoint(message->getIOA()); if (!point) { + // accept point via callback? client->onNewPoint(station, message->getIOA(), message->getType()); point = station->getPoint(message->getIOA()); } if (point) { point->onReceive(message); } else { - // @todo add error callback? DEBUG_PRINT_CONDITION(debug, Debug::Connection, "asdu_handler] Message ignored: Unknown IOA"); } @@ -964,14 +1035,11 @@ bool Connection::asduHandler(void *parameter, int address, CS101_ASDU asdu) { true, Debug::Connection, "asdu_handler] " + std::string(TypeID_toString(message->getType())) + - " Report Stats | CA " + + " Report Stats" + " | CA " + std::to_string(message->getCommonAddress()) + " | IOA " + std::to_string(message->getIOA()) + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + TICTOC(begin, end)); } return true; } @@ -990,14 +1058,10 @@ bool Connection::asduHandler(void *parameter, int address, CS101_ASDU asdu) { true, Debug::Connection, "asdu_handler] " + std::string(TypeID_toString(message->getType())) + - " Response Stats | CA " + + " Response Stats" + " | CA " + std::to_string(message->getCommonAddress()) + " | IOA " + std::to_string(message->getIOA()) + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + TICTOC(begin, end)); } return true; } @@ -1008,14 +1072,10 @@ bool Connection::asduHandler(void *parameter, int address, CS101_ASDU asdu) { DEBUG_PRINT_CONDITION( true, Debug::Connection, "asduHandler] Unhandled " + - std::string(TypeID_toString(message->getType())) + - " Stats | CA " + std::to_string(message->getCommonAddress()) + + std::string(TypeID_toString(message->getType())) + " Stats" + + " | CA " + std::to_string(message->getCommonAddress()) + " | IOA " + std::to_string(message->getIOA()) + " | TOTAL " + - std::to_string( - std::chrono::duration_cast(end - - begin) - .count()) + - u8" \xb5s"); + TICTOC(begin, end)); } } catch (const std::exception &e) { diff --git a/src/remote/Connection.h b/src/remote/Connection.h index c0fd9cc..8ec091d 100644 --- a/src/remote/Connection.h +++ b/src/remote/Connection.h @@ -64,7 +64,7 @@ class Connection : public std::enable_shared_from_this { [[nodiscard]] static std::shared_ptr create( std::shared_ptr client, const std::string &ip, const uint_fast16_t port = IEC_60870_5_104_DEFAULT_PORT, - const uint_fast32_t command_timeout_ms = 4000, + const uint_fast16_t command_timeout_ms = 100, const ConnectionInit init = INIT_ALL, std::shared_ptr transport_security = nullptr, const uint_fast8_t originator_address = 0) { @@ -164,33 +164,26 @@ class Connection : public std::enable_shared_from_this { * @brief Setter for muted state * @param value value of new muted state (true = muted, false = unmuted) */ - bool setMuted(bool value); - - /** - * @brief Getter for internal connection object - * @return CS104_Connection refrence - */ - CS104_Connection getCS104(); + void setMuted(bool value); /** * @brief Setter for open state: Mark connection as open */ - bool setOpen(); + void setOpen(); /** * @brief Setter for open state: Mark connection as closed, start reconnect * state */ - bool setClosed(); + void setClosed(); /** * @brief add command id to awaiting command result map * @param cmdId unique command id * @param state command process state - * @returns if command preparation was successfully (no collision with active - * sequence) + * @throws std::runtime_error if cmdId already in use */ - bool prepareCommandSuccess(const std::string &cmdId, + void prepareCommandSuccess(const std::string &cmdId, CommandProcessState state); /** @@ -268,6 +261,11 @@ class Connection : public std::enable_shared_from_this { */ void setOnStateChangeCallback(py::object &callable); + std::optional getConnectedAt() const; + + std::optional + getDisconnectedAt() const; + /** * @brief send interrogation command * @param commonAddress @@ -320,15 +318,12 @@ class Connection : public std::enable_shared_from_this { * @brief transmit a command to a remote server * @param point control point * @param cause reason for transmission - * @param qualifier parameter for command duration * @returns if operation was successful * @throws std::invalid_argument if point type is not supported for this * operation */ - bool - transmit(std::shared_ptr point, - CS101_CauseOfTransmission cause, - CS101_QualifierOfCommand qualifier = CS101_QualifierOfCommand::NONE); + bool transmit(std::shared_ptr point, + CS101_CauseOfTransmission cause); /** * @brief add command id to awaiting command result map @@ -398,7 +393,7 @@ class Connection : public std::enable_shared_from_this { * @throws std::invalid_argument if ip or port invalid */ Connection(std::shared_ptr _client, const std::string &_ip, - uint_fast16_t _port, uint_fast32_t command_timeout_ms, + uint_fast16_t _port, uint_fast16_t command_timeout_ms, ConnectionInit init, std::shared_ptr transport_security, uint_fast8_t originator_address); @@ -411,7 +406,7 @@ class Connection : public std::enable_shared_from_this { "Connection::connection_mutex"}; /// @brief timeout in milliseconds before an inactive connection gets closed - std::uint_fast32_t commandTimeout_ms{1000}; + std::atomic_uint_fast16_t commandTimeout_ms{100}; /// @brief IP address of remote server std::string ip = ""; @@ -438,10 +433,10 @@ class Connection : public std::enable_shared_from_this { std::atomic state{CLOSED}; /// @brief timestamp of last successfully connection opening - std::atomic_uint_fast64_t connectedAt_ms{0}; + std::atomic connectedAt{}; /// @brief timestamp of last disconnect - std::atomic_uint_fast64_t disconnectedAt_ms{0}; + std::atomic disconnectedAt{}; /// @brief MUTEX Lock to wait for command response mutable Module::GilAwareMutex expectedResponseMap_mutex{ @@ -451,10 +446,6 @@ class Connection : public std::enable_shared_from_this { /// expectedResponseMap_mutex) std::map expectedResponseMap{}; - /// @brief currently active command sequence, if any (must be access with - /// expectedResponseMap_mutex) - std::string sequenceId{""}; - /// @brief Condition to wait for successfully command confirmation and success /// information or timeout std::condition_variable_any response_wait{}; @@ -488,6 +479,21 @@ class Connection : public std::enable_shared_from_this { * @return connection state enum */ void setState(ConnectionState connectionState); + +public: + std::string toString() const { + size_t len = 0; + { + std::scoped_lock const lock(stations_mutex); + len = stations.size(); + } + std::ostringstream oss; + oss << "<104.Connection ip=" << ip << ", port=" << std::to_string(port) + << ", state=" << ConnectionState_toString(state) + << ", #stations=" << std::to_string(len) << " at " << std::hex + << std::showbase << reinterpret_cast(this) << ">"; + return oss.str(); + }; }; /** diff --git a/src/remote/Helper.cpp b/src/remote/Helper.cpp index 0398e38..de89dbe 100644 --- a/src/remote/Helper.cpp +++ b/src/remote/Helper.cpp @@ -226,7 +226,7 @@ py::dict Remote::rawMessageDictionaryFormatter(uint_fast8_t *msg, if ((type >= C_SC_NA_1 && type <= C_SE_NC_1) || (type >= C_SC_TA_1 && type <= C_SE_TC_1)) { - d["select"] = (bool)(msg[IEC60870_OBJECT_OFFSET + 3] << 7); + d["select"] = (bool)(msg[IEC60870_OBJECT_OFFSET + 3] >> 7); } } diff --git a/src/remote/TransportSecurity.cpp b/src/remote/TransportSecurity.cpp index 2627629..d903da4 100644 --- a/src/remote/TransportSecurity.cpp +++ b/src/remote/TransportSecurity.cpp @@ -37,8 +37,17 @@ using namespace Remote; void TransportSecurity::eventHandler(void *parameter, TLSEventLevel eventLevel, int eventCode, const char *msg, TLSConnection con) { - std::shared_ptr instance = - static_cast(parameter)->shared_from_this(); + std::shared_ptr instance{}; + try { + instance = static_cast(parameter)->shared_from_this(); + } catch (const std::bad_weak_ptr &e) { + if (DEBUG_TEST(Debug::Server) || DEBUG_TEST(Debug::Client)) { + std::cout << "[c104.TransportSecurity] failed to handle event: instance " + "already removed" + << std::endl; + } + return; + } char peerAddrBuf[60]; char *peerAddr = nullptr; @@ -50,7 +59,7 @@ void TransportSecurity::eventHandler(void *parameter, TLSEventLevel eventLevel, } if (DEBUG_TEST(Debug::Server) || DEBUG_TEST(Debug::Client)) { - printf("TransportSecurity.event] %s (t: %i, c: %i, version: %s remote-ip: " + printf("[c104.TransportSecurity] %s (t: %i, c: %i, version: %s remote-ip: " "%s)\n", msg, eventLevel, eventCode, tlsVersion, peerAddr); } diff --git a/src/remote/TransportSecurity.h b/src/remote/TransportSecurity.h index 7ad207a..b372ab8 100644 --- a/src/remote/TransportSecurity.h +++ b/src/remote/TransportSecurity.h @@ -95,6 +95,14 @@ class TransportSecurity TransportSecurity(bool validate, bool only_known); TLSConfiguration config{nullptr}; + +public: + std::string toString() const { + std::ostringstream oss; + oss << "<104.TransportSecurity at " << std::hex << std::showbase + << reinterpret_cast(this) << ">"; + return oss.str(); + }; }; } // namespace Remote diff --git a/src/remote/message/IMessageInterface.h b/src/remote/message/IMessageInterface.h index 9bd3bc4..c2f567f 100644 --- a/src/remote/message/IMessageInterface.h +++ b/src/remote/message/IMessageInterface.h @@ -1,5 +1,5 @@ /** - * Copyright 2020-2023 Fraunhofer Institute for Applied Information Technology + * Copyright 2020-2024 Fraunhofer Institute for Applied Information Technology * FIT * * This file is part of iec104-python. @@ -100,9 +100,9 @@ class IMessageInterface { /** * @brief Get the value from an information object inside the remote message - * @return value as double + * @return value as Information object */ - virtual double getValue() const { return value.load(); } + virtual std::shared_ptr getInfo() const { return info; } /** * @brief test if message test flag is set @@ -122,37 +122,6 @@ class IMessageInterface { */ virtual bool isSequence() const { return sequence.load(); } - /** - * @brief Test if message has a connectionString of connection used for this - * message - * @return information if message has a connectionString - */ - virtual bool hasConnectionString() const { - std::lock_guard const lock(access_mutex); - - return !connectionString.empty(); - } - - /** - * @brief Getter for connectionString of connection used for this message - * @return ip:port connectionString - */ - virtual std::string getConnectionString() const { - std::lock_guard const lock(access_mutex); - - return connectionString; - } - - /** - * @brief Setter for connectionString of connection used for this message - * @param s ip:port connectionString - */ - virtual void setConnectionString(const std::string &s) { - std::lock_guard const lock(access_mutex); - - connectionString = s; - } - /** * @brief Getter for cause of transmission: why was this message transmitted * @return cause of transmission as enum @@ -161,12 +130,6 @@ class IMessageInterface { return causeOfTransmission.load(); } - /** - * @brief Getter for quality of message information - * @return quality as bitset object - */ - virtual Quality getQuality() const { return quality.load(); } - protected: IMessageInterface() = default; @@ -192,14 +155,8 @@ class IMessageInterface { std::atomic causeOfTransmission{ CS101_COT_UNKNOWN_COT}; - /// @brief IEC60870-5-104 describes the quality of the information - std::atomic quality{Quality::None}; - - /// @brief ip:port connectionString of the used connection - std::string connectionString{}; - - /// @brief value of the informationObject - std::atomic value{-1}; + /// @brief abstract representation of information + std::shared_ptr info{nullptr}; /// @brief state that defines if informationObject has a value std::atomic_bool test{false}; diff --git a/src/remote/message/IncomingMessage.cpp b/src/remote/message/IncomingMessage.cpp index d0d5617..4899a43 100644 --- a/src/remote/message/IncomingMessage.cpp +++ b/src/remote/message/IncomingMessage.cpp @@ -31,16 +31,22 @@ */ #include "IncomingMessage.h" + +#include "object/Information.h" #include "remote/Helper.h" +#include using namespace Remote::Message; IncomingMessage::IncomingMessage(CS101_ASDU packet, CS101_AppLayerParameters app_layer_parameters) - : IMessageInterface(), asdu(packet), parameters(app_layer_parameters), + : IMessageInterface(), asdu(nullptr), parameters(app_layer_parameters), position(0), positionReset(true), positionValid(false), numberOfObject(0) { if (packet) { + asdu = CS101_ASDU_clone(packet, nullptr); + } + if (asdu) { extractMetaData(); first(); } @@ -51,6 +57,9 @@ IncomingMessage::~IncomingMessage() { if (io) { InformationObject_destroy(io); } + if (asdu) { + CS101_ASDU_destroy(asdu); + } DEBUG_PRINT(Debug::Message, "Removed (incoming)"); } @@ -198,8 +207,6 @@ std::uint_fast8_t IncomingMessage::getNumberOfObject() const { return numberOfObject; } -std::uint_fast64_t IncomingMessage::getUpdatedAt() const { return updatedAt; } - void IncomingMessage::first() { { std::lock_guard const lock(position_mutex); @@ -210,7 +217,7 @@ void IncomingMessage::first() { } if (positionValid) - extractInformationObject(); + extractInformation(); } bool IncomingMessage::next() { @@ -228,12 +235,12 @@ bool IncomingMessage::next() { } if (positionValid) - extractInformationObject(); + extractInformation(); return positionValid; } -void IncomingMessage::extractInformationObject() { +void IncomingMessage::extractInformation() { std::lock_guard const lock(access_mutex); if (io) @@ -242,424 +249,391 @@ void IncomingMessage::extractInformationObject() { io = CS101_ASDU_getElement(asdu, position); informationObjectAddress = (io == nullptr) ? 0 : InformationObject_getObjectAddress(io); - value = 0; - quality = Quality::Invalid; - - if (positionValid && informationObjectAddress) { - bool transient = false; - // IEC 60870-5-104 standard uses only messages without timestamp or with - // CP56Time2a timestamp - CP16Time2a elapsed = nullptr; - CP56Time2a timestamp56 = nullptr; + info.reset(); + if ((io != nullptr) && positionValid) { switch (type) { - /** - * MONITORING - */ + /** + * MONITORING + */ - // s->c: bool Single Point case M_SP_NA_1: { - value = SinglePointInformation_getValue((SinglePointInformation)io); - quality = static_cast( - SinglePointInformation_getQuality((SinglePointInformation)io)); + info = std::make_shared( + SinglePointInformation_getValue((SinglePointInformation)io), + static_cast( + SinglePointInformation_getQuality((SinglePointInformation)io)), + std::nullopt, true); } break; - // s->c: bool Single Point + Extended Time case M_SP_TB_1: { - value = SinglePointInformation_getValue((SinglePointInformation)io); - quality = static_cast( - SinglePointInformation_getQuality((SinglePointInformation)io)); - timestamp56 = - SinglePointWithCP56Time2a_getTimestamp((SinglePointWithCP56Time2a)io); + info = std::make_shared( + SinglePointInformation_getValue((SinglePointInformation)io), + static_cast( + SinglePointInformation_getQuality((SinglePointInformation)io)), + to_time_point(SinglePointWithCP56Time2a_getTimestamp( + (SinglePointWithCP56Time2a)io)), + true); } break; - // s->c: enum Double Point [INTERMEDIATE|ON|OFF|INDETERMINATE] case M_DP_NA_1: { - DoublePointValue dpv1 = - DoublePointInformation_getValue((DoublePointInformation)io); - value = dpv1; - quality = static_cast( - DoublePointInformation_getQuality((DoublePointInformation)io)); + info = std::make_shared( + DoublePointInformation_getValue((DoublePointInformation)io), + static_cast( + DoublePointInformation_getQuality((DoublePointInformation)io)), + std::nullopt, true); } break; - // s->c: enum Double Point [INTERMEDIATE|ON|OFF|INDETERMINATE] + Extended - // Time case M_DP_TB_1: { - DoublePointValue dpv3 = - DoublePointInformation_getValue((DoublePointInformation)io); - value = dpv3; - quality = static_cast( - DoublePointInformation_getQuality((DoublePointInformation)io)); - timestamp56 = - DoublePointWithCP56Time2a_getTimestamp((DoublePointWithCP56Time2a)io); + info = std::make_shared( + DoublePointInformation_getValue((DoublePointInformation)io), + static_cast( + DoublePointInformation_getQuality((DoublePointInformation)io)), + to_time_point(DoublePointWithCP56Time2a_getTimestamp( + (DoublePointWithCP56Time2a)io)), + true); } break; - // s->c: int [-64,63] StepPosition (Trafo) case M_ST_NA_1: { - //@todo getObjectAddress ??? transient ??? - uint_fast16_t objAddr1 = - StepPositionInformation_getObjectAddress((StepPositionInformation)io); - value = StepPositionInformation_getValue((StepPositionInformation)io); - quality = static_cast( - StepPositionInformation_getQuality((StepPositionInformation)io)); - transient = - StepPositionInformation_isTransient((StepPositionInformation)io); + info = std::make_shared( + LimitedInt7( + StepPositionInformation_getValue((StepPositionInformation)io)), + StepPositionInformation_isTransient((StepPositionInformation)io), + static_cast( + StepPositionInformation_getQuality((StepPositionInformation)io)), + std::nullopt, true); } break; - // s->c: int [-64,63] StepPosition (Trafo) + Extended Time case M_ST_TB_1: { - //@todo getObjectAddress ??? transient ??? - uint_fast16_t objAddr3 = - StepPositionInformation_getObjectAddress((StepPositionInformation)io); - value = StepPositionInformation_getValue((StepPositionInformation)io); - quality = static_cast( - StepPositionInformation_getQuality((StepPositionInformation)io)); - transient = - StepPositionInformation_isTransient((StepPositionInformation)io); - timestamp56 = StepPositionWithCP56Time2a_getTimestamp( - (StepPositionWithCP56Time2a)io); - } break; - - // s->c: [0,2^32] BitString 32bits + info = std::make_shared( + LimitedInt7( + StepPositionInformation_getValue((StepPositionInformation)io)), + StepPositionInformation_isTransient((StepPositionInformation)io), + static_cast( + StepPositionInformation_getQuality((StepPositionInformation)io)), + to_time_point(StepPositionWithCP56Time2a_getTimestamp( + (StepPositionWithCP56Time2a)io)), + true); + } break; + case M_BO_NA_1: { - //@todo usage? conversion? - value = BitString32_getValue((BitString32)io); - quality = static_cast(BitString32_getQuality((BitString32)io)); + info = std::make_shared( + Byte32(BitString32_getValue((BitString32)io)), + static_cast(BitString32_getQuality((BitString32)io)), + std::nullopt, true); } break; - // s->c: [0,2^32] BitString 32bits + Extended Time case M_BO_TB_1: { - //@todo usage? conversion? - value = BitString32_getValue((BitString32)io); - quality = static_cast(BitString32_getQuality((BitString32)io)); - timestamp56 = - Bitstring32WithCP56Time2a_getTimestamp((Bitstring32WithCP56Time2a)io); + info = std::make_shared( + Byte32(BitString32_getValue((BitString32)io)), + static_cast(BitString32_getQuality((BitString32)io)), + to_time_point(Bitstring32WithCP56Time2a_getTimestamp( + (Bitstring32WithCP56Time2a)io)), + true); } break; - // s->c: float Measurement Value (NORMALIZED) case M_ME_NA_1: { - value = MeasuredValueNormalized_getValue((MeasuredValueNormalized)io); - quality = static_cast( - MeasuredValueNormalized_getQuality((MeasuredValueNormalized)io)); + info = std::make_shared( + NormalizedFloat( + MeasuredValueNormalized_getValue((MeasuredValueNormalized)io)), + static_cast( + MeasuredValueNormalized_getQuality((MeasuredValueNormalized)io)), + std::nullopt, true); } break; - // s->c: float Measurement Value (NORMALIZED) + Extended Time case M_ME_TD_1: { - value = MeasuredValueNormalized_getValue((MeasuredValueNormalized)io); - quality = static_cast( - MeasuredValueNormalized_getQuality((MeasuredValueNormalized)io)); - timestamp56 = MeasuredValueNormalizedWithCP56Time2a_getTimestamp( - (MeasuredValueNormalizedWithCP56Time2a)io); + info = std::make_shared( + NormalizedFloat( + MeasuredValueNormalized_getValue((MeasuredValueNormalized)io)), + static_cast( + MeasuredValueNormalized_getQuality((MeasuredValueNormalized)io)), + to_time_point(MeasuredValueNormalizedWithCP56Time2a_getTimestamp( + (MeasuredValueNormalizedWithCP56Time2a)io)), + true); } break; - // s->c: int Measurement Value (SCALED) case M_ME_NB_1: { - value = MeasuredValueScaled_getValue((MeasuredValueScaled)io); - quality = static_cast( - MeasuredValueScaled_getQuality((MeasuredValueScaled)io)); + info = std::make_shared( + LimitedInt16(MeasuredValueScaled_getValue((MeasuredValueScaled)io)), + static_cast( + MeasuredValueScaled_getQuality((MeasuredValueScaled)io)), + std::nullopt, true); } break; - // s->c: int Measurement Value (SCALED) + Extended Time case M_ME_TE_1: { - value = MeasuredValueScaled_getValue((MeasuredValueScaled)io); - quality = static_cast( - MeasuredValueScaled_getQuality((MeasuredValueScaled)io)); - timestamp56 = MeasuredValueScaledWithCP56Time2a_getTimestamp( - (MeasuredValueScaledWithCP56Time2a)io); + info = std::make_shared( + LimitedInt16(MeasuredValueScaled_getValue((MeasuredValueScaled)io)), + static_cast( + MeasuredValueScaled_getQuality((MeasuredValueScaled)io)), + to_time_point(MeasuredValueScaledWithCP56Time2a_getTimestamp( + (MeasuredValueScaledWithCP56Time2a)io)), + true); } break; - // s->c: float Measurement Value (SHORT) case M_ME_NC_1: { - //@todo not normalized or scaled ? - value = MeasuredValueShort_getValue((MeasuredValueShort)io); - quality = static_cast( - MeasuredValueShort_getQuality((MeasuredValueShort)io)); + info = std::make_shared( + MeasuredValueShort_getValue((MeasuredValueShort)io), + static_cast( + MeasuredValueShort_getQuality((MeasuredValueShort)io)), + std::nullopt, true); } break; - // s->c: float Measurement Value (SHORT) + Extended Time case M_ME_TF_1: { - value = MeasuredValueShort_getValue((MeasuredValueShort)io); - quality = static_cast( - MeasuredValueShort_getQuality((MeasuredValueShort)io)); - timestamp56 = MeasuredValueShortWithCP56Time2a_getTimestamp( - (MeasuredValueShortWithCP56Time2a)io); + info = std::make_shared( + MeasuredValueShort_getValue((MeasuredValueShort)io), + static_cast( + MeasuredValueShort_getQuality((MeasuredValueShort)io)), + to_time_point(MeasuredValueShortWithCP56Time2a_getTimestamp( + (MeasuredValueShortWithCP56Time2a)io)), + true); } break; - // s->c: Encoded Counter Value case M_IT_NA_1: { - //@todo usecase? value conversion? - //@todo what about quality ? BinaryCounterReading bcr1 = IntegratedTotals_getBCR((IntegratedTotals)io); - value = (double)(bcr1->encodedValue[0] + - ((uint_fast64_t)bcr1->encodedValue[1] << 8) + - ((uint_fast64_t)bcr1->encodedValue[2] << 16) + - ((uint_fast64_t)bcr1->encodedValue[3] << 24) + - ((uint_fast64_t)bcr1->encodedValue[4] << 32)); - quality.store(Quality::None); + info = std::make_shared( + BinaryCounterReading_getValue(bcr1), + LimitedUInt5(static_cast( + BinaryCounterReading_getSequenceNumber(bcr1))), + BinaryCounterQuality(bcr1->encodedValue[4] & 0b11100000), + std::nullopt, true); } break; - // s->c: Encoded Counter Value + Extended Timer case M_IT_TB_1: { - //@todo support BCR, usecase? value? - //@todo what about quality ? - BinaryCounterReading bcr3 = IntegratedTotals_getBCR((IntegratedTotals)io); - value = (double)(bcr3->encodedValue[0] + - ((uint_fast64_t)bcr3->encodedValue[1] << 8) + - ((uint_fast64_t)bcr3->encodedValue[2] << 16) + - ((uint_fast64_t)bcr3->encodedValue[3] << 24) + - ((uint_fast64_t)bcr3->encodedValue[4] << 32)); - timestamp56 = IntegratedTotalsWithCP56Time2a_getTimestamp( - (IntegratedTotalsWithCP56Time2a)io); - quality.store(Quality::None); - } break; - - // s->c: SingleEvent Protection Equipment + Extended Timer + BinaryCounterReading bcr1 = IntegratedTotals_getBCR((IntegratedTotals)io); + info = std::make_shared( + BinaryCounterReading_getValue(bcr1), + LimitedUInt5(static_cast( + BinaryCounterReading_getSequenceNumber(bcr1))), + BinaryCounterQuality(bcr1->encodedValue[4] & 0b11100000), + to_time_point(IntegratedTotalsWithCP56Time2a_getTimestamp( + (IntegratedTotalsWithCP56Time2a)io)), + true); + } break; + case M_EP_TD_1: { - //@todo usecase? handle event?? value? - //@todo what about quality ? - SingleEvent ev2 = EventOfProtectionEquipmentWithCP56Time2a_getEvent( - (EventOfProtectionEquipmentWithCP56Time2a)io); - elapsed = EventOfProtectionEquipmentWithCP56Time2a_getElapsedTime( - (EventOfProtectionEquipmentWithCP56Time2a)io); - timestamp56 = EventOfProtectionEquipmentWithCP56Time2a_getTimestamp( - (EventOfProtectionEquipmentWithCP56Time2a)io); - quality.store(Quality::None); - } break; - - // s->c: StartEvent Protection Equipment + Extended Timer + SingleEvent single_event = + EventOfProtectionEquipmentWithCP56Time2a_getEvent( + (EventOfProtectionEquipmentWithCP56Time2a)io); + info = std::make_shared( + static_cast(*single_event & 0b00000111), + LimitedUInt16(CP16Time2a_getEplapsedTimeInMs( + EventOfProtectionEquipmentWithCP56Time2a_getElapsedTime( + (EventOfProtectionEquipmentWithCP56Time2a)io))), + static_cast(*single_event & 0b11111000), + to_time_point(EventOfProtectionEquipmentWithCP56Time2a_getTimestamp( + (EventOfProtectionEquipmentWithCP56Time2a)io)), + true); + } break; + case M_EP_TE_1: { - //@todo usecase? handle event?? value? - StartEvent se2 = - PackedStartEventsOfProtectionEquipmentWithCP56Time2a_getEvent( - (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)io); - elapsed = - PackedStartEventsOfProtectionEquipmentWithCP56Time2a_getElapsedTime( - (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)io); - quality = static_cast( - PackedStartEventsOfProtectionEquipmentWithCP56Time2a_getQuality( - (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)io)); - timestamp56 = - PackedStartEventsOfProtectionEquipmentWithCP56Time2a_getTimestamp( - (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)io); - } break; - - // s->c: OuputCircuitInfo Protection Equipment + Extended Timer + info = std::make_shared( + StartEvents( + PackedStartEventsOfProtectionEquipmentWithCP56Time2a_getEvent( + (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)io) & + 0b00111111), + LimitedUInt16(CP16Time2a_getEplapsedTimeInMs( + PackedStartEventsOfProtectionEquipmentWithCP56Time2a_getElapsedTime( + (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)io))), + static_cast( + PackedStartEventsOfProtectionEquipmentWithCP56Time2a_getQuality( + (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)io)), + to_time_point( + PackedStartEventsOfProtectionEquipmentWithCP56Time2a_getTimestamp( + (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)io)), + true); + } break; + case M_EP_TF_1: { - //@todo usecase? handle event?? value? - OutputCircuitInfo oci2 = PackedOutputCircuitInfoWithCP56Time2a_getOCI( - (PackedOutputCircuitInfoWithCP56Time2a)io); - elapsed = PackedOutputCircuitInfoWithCP56Time2a_getOperatingTime( - (PackedOutputCircuitInfoWithCP56Time2a)io); - quality = + info = std::make_shared( + OutputCircuits(PackedOutputCircuitInfoWithCP56Time2a_getOCI( + (PackedOutputCircuitInfoWithCP56Time2a)io) & + 0b00001111), + LimitedUInt16(CP16Time2a_getEplapsedTimeInMs( + PackedOutputCircuitInfoWithCP56Time2a_getOperatingTime( + (PackedOutputCircuitInfoWithCP56Time2a)io))), static_cast(PackedOutputCircuitInfoWithCP56Time2a_getQuality( - (PackedOutputCircuitInfoWithCP56Time2a)io)); - timestamp56 = PackedOutputCircuitInfoWithCP56Time2a_getTimestamp( - (PackedOutputCircuitInfoWithCP56Time2a)io); + (PackedOutputCircuitInfoWithCP56Time2a)io)), + to_time_point(PackedOutputCircuitInfoWithCP56Time2a_getTimestamp( + (PackedOutputCircuitInfoWithCP56Time2a)io)), + true); } break; - // s->c: StatusAndStatusChangeDetection Single + Change Detection case M_PS_NA_1: { - //@todo usecase? decoded value? StatusAndStatusChangeDetection sscd = PackedSinglePointWithSCD_getSCD((PackedSinglePointWithSCD)io); - value = (double)(sscd->encodedValue[0] + - ((uint_fast64_t)sscd->encodedValue[1] << 8) + - ((uint_fast64_t)sscd->encodedValue[2] << 16) + - ((uint_fast64_t)sscd->encodedValue[3] << 24)); - quality = static_cast( - PackedSinglePointWithSCD_getQuality((PackedSinglePointWithSCD)io)); + info = std::make_shared( + FieldSet16(((uint16_t)sscd->encodedValue[0] << 0) + + ((uint16_t)sscd->encodedValue[1] << 8)), + FieldSet16(((uint16_t)sscd->encodedValue[2] << 0) + + ((uint16_t)sscd->encodedValue[3] << 8)), + static_cast(PackedSinglePointWithSCD_getQuality( + (PackedSinglePointWithSCD)io)), + std::nullopt, true); } break; - // s->c: float Measurement Value (NORMALIZED) - Quality case M_ME_ND_1: { - //@todo usecase? - //@todo what about quality ? - value = MeasuredValueNormalizedWithoutQuality_getValue( - (MeasuredValueNormalizedWithoutQuality)io); - quality.store(Quality::None); + info = std::make_shared( + NormalizedFloat(MeasuredValueNormalizedWithoutQuality_getValue( + (MeasuredValueNormalizedWithoutQuality)io)), + Quality::None, std::nullopt, true); } break; /** * CONTROL */ - // c->s: bool case C_SC_NA_1: { - selectFlag = SingleCommand_isSelect((SingleCommand)io); - qualifierOfCommand = SingleCommand_getQU((SingleCommand)io); - value = SingleCommand_getState((SingleCommand)io); - if (value == 0 || value == 1) { - quality.store(Quality::None); - } + info = std::make_shared( + SingleCommand_getState((SingleCommand)io), + SingleCommand_isSelect((SingleCommand)io), + static_cast( + SingleCommand_getQU((SingleCommand)io)), + std::nullopt, true); } break; - // c->s: bool + Extended Time case C_SC_TA_1: { - selectFlag = SingleCommand_isSelect((SingleCommand)io); - qualifierOfCommand = SingleCommand_getQU((SingleCommand)io); - value = SingleCommand_getState((SingleCommand)io); - timestamp56 = SingleCommandWithCP56Time2a_getTimestamp( - (SingleCommandWithCP56Time2a)io); - if (value == 0 || value == 1) { - quality.store(Quality::None); - } + info = std::make_shared( + SingleCommand_getState((SingleCommand)io), + SingleCommand_isSelect((SingleCommand)io), + static_cast( + SingleCommand_getQU((SingleCommand)io)), + to_time_point(SingleCommandWithCP56Time2a_getTimestamp( + (SingleCommandWithCP56Time2a)io)), + true); } break; - // c->s: int /enum?? case C_DC_NA_1: { - selectFlag = DoubleCommand_isSelect((DoubleCommand)io); - qualifierOfCommand = DoubleCommand_getQU((DoubleCommand)io); - value = DoubleCommand_getState((DoubleCommand)io); - if (value == 0 || value == 1 || value == 2 || value == 3) { - quality.store(Quality::None); - } + info = std::make_shared( + static_cast( + DoubleCommand_getState((DoubleCommand)io)), + DoubleCommand_isSelect((DoubleCommand)io), + static_cast( + DoubleCommand_getQU((DoubleCommand)io)), + std::nullopt, true); } break; - // c->s: int /enum? + Extended Time case C_DC_TA_1: { - selectFlag = - DoubleCommandWithCP56Time2a_isSelect((DoubleCommandWithCP56Time2a)io); - qualifierOfCommand = - DoubleCommandWithCP56Time2a_getQU((DoubleCommandWithCP56Time2a)io); - value = - DoubleCommandWithCP56Time2a_getState((DoubleCommandWithCP56Time2a)io); - timestamp56 = DoubleCommandWithCP56Time2a_getTimestamp( - (DoubleCommandWithCP56Time2a)io); - if (value == 0 || value == 1 || value == 2 || value == 3) { - quality.store(Quality::None); - } - } break; - - // c->s: Regulation Step Command + info = std::make_shared( + static_cast( + DoubleCommand_getState((DoubleCommand)io)), + DoubleCommand_isSelect((DoubleCommand)io), + static_cast( + DoubleCommand_getQU((DoubleCommand)io)), + to_time_point(DoubleCommandWithCP56Time2a_getTimestamp( + (DoubleCommandWithCP56Time2a)io)), + true); + } break; + case C_RC_NA_1: { - selectFlag = StepCommand_isSelect((StepCommand)io); - qualifierOfCommand = StepCommand_getQU((StepCommand)io); - StepCommandValue scv1 = StepCommand_getState((StepCommand)io); - value = scv1; - if (value == 1 || value == 2) { - quality.store(Quality::None); - } + info = std::make_shared( + static_cast(StepCommand_getState((StepCommand)io)), + StepCommand_isSelect((StepCommand)io), + static_cast( + StepCommand_getQU((StepCommand)io)), + std::nullopt, true); } break; - // c->s: Regulation Step Command + Extended Time case C_RC_TA_1: { - selectFlag = StepCommand_isSelect((StepCommand)io); - qualifierOfCommand = - StepCommandWithCP56Time2a_getQU((StepCommandWithCP56Time2a)io); - StepCommandValue scv2 = - StepCommandWithCP56Time2a_getState((StepCommandWithCP56Time2a)io); - value = scv2; - timestamp56 = - StepCommandWithCP56Time2a_getTimestamp((StepCommandWithCP56Time2a)io); - if (value == 1 || value == 2) { - quality.store(Quality::None); - } - } break; - - // c->s: Setpoint Command (NORMALIZED) + info = std::make_shared( + static_cast(StepCommand_getState((StepCommand)io)), + StepCommand_isSelect((StepCommand)io), + static_cast( + StepCommand_getQU((StepCommand)io)), + to_time_point(StepCommandWithCP56Time2a_getTimestamp( + (StepCommandWithCP56Time2a)io)), + true); + } break; + case C_SE_NA_1: { - //@todo what about quality / use ql for command ? - int ql1 = SetpointCommandNormalized_getQL((SetpointCommandNormalized)io); - selectFlag = - SetpointCommandNormalized_isSelect((SetpointCommandNormalized)io); - value = SetpointCommandNormalized_getValue((SetpointCommandNormalized)io); - if (value >= -1 && value <= 1) { - quality.store(Quality::None); - } + info = std::make_shared( + NormalizedFloat(SetpointCommandNormalized_getValue( + (SetpointCommandNormalized)io)), + SetpointCommandNormalized_isSelect((SetpointCommandNormalized)io), + LimitedUInt7(static_cast( + SetpointCommandNormalized_getQL((SetpointCommandNormalized)io))), + std::nullopt, true); } break; - // c->s: Setpoint Command (NORMALIZED) * Extended Time case C_SE_TA_1: { - //@todo what about quality / use ql for command ? - int ql2 = SetpointCommandNormalizedWithCP56Time2a_getQL( - (SetpointCommandNormalizedWithCP56Time2a)io); - selectFlag = SetpointCommandNormalizedWithCP56Time2a_isSelect( - (SetpointCommandNormalizedWithCP56Time2a)io); - value = SetpointCommandNormalizedWithCP56Time2a_getValue( - (SetpointCommandNormalizedWithCP56Time2a)io); - timestamp56 = SetpointCommandNormalizedWithCP56Time2a_getTimestamp( - (SetpointCommandNormalizedWithCP56Time2a)io); - if (value >= -1 && value <= 1) { - quality.store(Quality::None); - } - } break; - - // c->s: Setpoint Command (SCALED) + info = std::make_shared( + NormalizedFloat(SetpointCommandNormalized_getValue( + (SetpointCommandNormalized)io)), + SetpointCommandNormalized_isSelect((SetpointCommandNormalized)io), + LimitedUInt7(static_cast( + SetpointCommandNormalized_getQL((SetpointCommandNormalized)io))), + to_time_point(SetpointCommandNormalizedWithCP56Time2a_getTimestamp( + (SetpointCommandNormalizedWithCP56Time2a)io)), + true); + } break; + case C_SE_NB_1: { - //@todo what about quality / use ql for command ? - int ql3 = SetpointCommandScaled_getQL((SetpointCommandScaled)io); - selectFlag = SetpointCommandScaled_isSelect((SetpointCommandScaled)io); - value = SetpointCommandScaled_getValue((SetpointCommandScaled)io); - if (value >= -65536. && value <= 65535.) { - quality.store(Quality::None); - } + info = std::make_shared( + LimitedInt16( + SetpointCommandScaled_getValue((SetpointCommandScaled)io)), + SetpointCommandScaled_isSelect((SetpointCommandScaled)io), + LimitedUInt7(static_cast( + SetpointCommandScaled_getQL((SetpointCommandScaled)io))), + std::nullopt, true); } break; - // c->s: Setpoint Command (SCALED) + Extended Time case C_SE_TB_1: { - //@todo what about quality / use ql for command ? - int ql4 = SetpointCommandScaledWithCP56Time2a_getQL( - (SetpointCommandScaledWithCP56Time2a)io); - selectFlag = SetpointCommandScaledWithCP56Time2a_isSelect( - (SetpointCommandScaledWithCP56Time2a)io); - value = SetpointCommandScaledWithCP56Time2a_getValue( - (SetpointCommandScaledWithCP56Time2a)io); - timestamp56 = SetpointCommandScaledWithCP56Time2a_getTimestamp( - (SetpointCommandScaledWithCP56Time2a)io); - if (value >= -65536. && value <= 65535.) { - quality.store(Quality::None); - } - } break; - - // c->s: Setpoint Command (SHORT) + info = std::make_shared( + LimitedInt16( + SetpointCommandScaled_getValue((SetpointCommandScaled)io)), + SetpointCommandScaled_isSelect((SetpointCommandScaled)io), + LimitedUInt7(static_cast( + SetpointCommandScaled_getQL((SetpointCommandScaled)io))), + to_time_point(SetpointCommandScaledWithCP56Time2a_getTimestamp( + (SetpointCommandScaledWithCP56Time2a)io)), + true); + } break; + case C_SE_NC_1: { - //@todo what about quality / use ql for command ? - int ql5 = SetpointCommandShort_getQL((SetpointCommandShort)io); - selectFlag = SetpointCommandShort_isSelect((SetpointCommandShort)io); - value = SetpointCommandShort_getValue((SetpointCommandShort)io); - if (value >= -16777216. && value <= 16777215.) { - quality.store(Quality::None); - } + info = std::make_shared( + SetpointCommandShort_getValue((SetpointCommandShort)io), + SetpointCommandShort_isSelect((SetpointCommandShort)io), + LimitedUInt7(static_cast( + SetpointCommandShort_getQL((SetpointCommandShort)io))), + std::nullopt, true); } break; - // c->s: Setpoint Command (SHORT) + Extended Time case C_SE_TC_1: { - //@todo what about quality / use ql for command ? - int ql6 = SetpointCommandShortWithCP56Time2a_getQL( - (SetpointCommandShortWithCP56Time2a)io); - selectFlag = SetpointCommandShortWithCP56Time2a_isSelect( - (SetpointCommandShortWithCP56Time2a)io); - value = SetpointCommandShortWithCP56Time2a_getValue( - (SetpointCommandShortWithCP56Time2a)io); - timestamp56 = SetpointCommandShortWithCP56Time2a_getTimestamp( - (SetpointCommandShortWithCP56Time2a)io); - if (value >= -16777216. && value <= 16777215.) { - quality.store(Quality::None); - } - } break; - - // c->s: BitString 32bit Command - case C_BO_NA_1: { - //@todo what about quality / use ql for command ? - value = (double)Bitstring32Command_getValue((Bitstring32Command)io); - quality.store(Quality::None); + info = std::make_shared( + SetpointCommandShort_getValue((SetpointCommandShort)io), + SetpointCommandShort_isSelect((SetpointCommandShort)io), + LimitedUInt7(static_cast( + SetpointCommandShort_getQL((SetpointCommandShort)io))), + to_time_point(SetpointCommandShortWithCP56Time2a_getTimestamp( + (SetpointCommandShortWithCP56Time2a)io)), + true); } break; - // c->s: BitString 32bit Command + Extended Time - case C_BO_TA_1: { - //@todo what about quality / use ql for command ? - value = (double)Bitstring32CommandWithCP56Time2a_getValue( - (Bitstring32CommandWithCP56Time2a)io); - timestamp56 = Bitstring32CommandWithCP56Time2a_getTimestamp( - (Bitstring32CommandWithCP56Time2a)io); - quality.store(Quality::None); + case C_BO_NA_1: { + info = std::make_shared( + Byte32(Bitstring32Command_getValue((Bitstring32Command)io)), + std::nullopt, true); } break; - // c->s: READ Command - case C_RD_NA_1: { - // accept incoming message instances within read commands - quality.store(Quality::None); - } break; + case C_BO_TA_1: { + info = std::make_shared( + Byte32(Bitstring32Command_getValue((Bitstring32Command)io)), + to_time_point(Bitstring32CommandWithCP56Time2a_getTimestamp( + (Bitstring32CommandWithCP56Time2a)io)), + true); + } break; + case C_CS_NA_1: { + info = std::make_shared( + to_time_point(ClockSynchronizationCommand_getTime( + (ClockSynchronizationCommand)io)), + true); + } break; + + case C_IC_NA_1: + case C_CI_NA_1: + case C_RD_NA_1: + case C_TS_NA_1: + // allow get valid message to pass and extract informationObjectAddress + // even though no further info is extracted + break; /** * Unhandled message @@ -669,19 +643,6 @@ void IncomingMessage::extractInformationObject() { std::cerr << "[c104.IncomingMessage.extract] Unsupported type " << TypeID_toString(type) << std::endl; } - - if (timestamp56) { - updatedAt = CP56Time2a_toMsTimestamp(timestamp56); - } - - // detect NaN values - if (std::isnan(value.load())) { - // value.store(0); - // quality.store(Quality::Invalid); - DEBUG_PRINT(Debug::Point, "IncomingMessage.extract] detected NaN value " - "in incoming message at IOA " + - std::to_string(informationObjectAddress)); - } } } @@ -896,25 +857,20 @@ bool IncomingMessage::requireConfirmation() const { } bool IncomingMessage::isSelectCommand() const { - if ((type < C_SC_NA_1 && type > C_SE_NC_1) || - (type < C_SC_TA_1 && type > C_SE_TC_1)) { - DEBUG_PRINT(Debug::Message, - "IncomingMessage.isSelectCommand] point at IOA " + - std::to_string(informationObjectAddress) + " of TypeID " + - TypeID_toString(type) + " does not carry a SELECT flag"); - } - return selectFlag; + auto cmd = std::dynamic_pointer_cast(info); + return cmd && cmd->isSelectable() && cmd->isSelect(); } -CS101_QualifierOfCommand IncomingMessage::getCommandQualifier() const { - if ((type < C_SC_NA_1 || (type > C_RC_NA_1 && type < C_SC_TA_1) || - type > C_RC_TA_1)) { - - DEBUG_PRINT(Debug::Message, - "IncomingMessage.getCommandQualifier] point at IOA " + - std::to_string(informationObjectAddress) + " of TypeID " + - TypeID_toString(type) + - " does not carry a command QUALIFIER"); - } - return static_cast(qualifierOfCommand.load()); -} +std::string IncomingMessage::toString() const { + std::ostringstream oss; + oss << "(this) << ">"; + return oss.str(); +}; diff --git a/src/remote/message/IncomingMessage.h b/src/remote/message/IncomingMessage.h index c0453bb..4f508ea 100644 --- a/src/remote/message/IncomingMessage.h +++ b/src/remote/message/IncomingMessage.h @@ -101,8 +101,6 @@ class IncomingMessage : public IMessageInterface, */ bool next(); - std::uint_fast64_t getUpdatedAt() const; - /** * @brief test if cause of transmission is compatible with information type * @return if cause is valid @@ -117,15 +115,11 @@ class IncomingMessage : public IMessageInterface, bool requireConfirmation() const; /** - * @brief test if message is a command with select flag set + * @brief test if message is a command and requires a confirmation (ACK) */ bool isSelectCommand() const; - /** - * @brief get the command duration qualifier, only available for single, - * double or regulation step commands - */ - CS101_QualifierOfCommand getCommandQualifier() const; + std::string toString() const; private: /** @@ -140,9 +134,9 @@ class IncomingMessage : public IMessageInterface, CS101_AppLayerParameters app_layer_parameters); /// @brief IEC60870-5-104 asdu struct - CS101_ASDU asdu{nullptr}; + CS101_ASDU asdu; - CS101_AppLayerParameters parameters{nullptr}; + const CS101_AppLayerParameters parameters; ///< @brief MUTEX Lock to change extracted information object position mutable Module::GilAwareMutex position_mutex{ @@ -161,18 +155,6 @@ class IncomingMessage : public IMessageInterface, /// @brief number of available information objects inside this message std::atomic_uint_fast8_t numberOfObject{0}; - /// @brief timestamp in milliseconds since latest value update - std::atomic_uint_fast64_t updatedAt{0}; - - /// @brief state that describes if a command results in a SELECT or an EXECUTE - /// action - std::atomic_bool selectFlag{false}; - - /// @brief command duration parameter, only used for single, double and - /// regulation step command messages - std::atomic_uint_fast8_t qualifierOfCommand{ - IEC60870_QOC_NO_ADDITIONAL_DEFINITION}; - /** * @brief extract meta data from this message: commonAddress, * originatorAddress, message identifier and mode, ... @@ -184,7 +166,7 @@ class IncomingMessage : public IMessageInterface, /** * @brief extract values of an information object at the current position */ - void extractInformationObject(); + void extractInformation(); }; } // namespace Message } // namespace Remote diff --git a/src/remote/message/OutgoingMessage.cpp b/src/remote/message/OutgoingMessage.cpp index a4448be..32111b7 100644 --- a/src/remote/message/OutgoingMessage.cpp +++ b/src/remote/message/OutgoingMessage.cpp @@ -39,7 +39,8 @@ using namespace Remote::Message; -OutgoingMessage::OutgoingMessage(std::shared_ptr point) +OutgoingMessage::OutgoingMessage( + const std::shared_ptr &point) : IMessageInterface() { if (!point) throw std::invalid_argument("Cannot create OutgoingMessage without point"); @@ -47,8 +48,7 @@ OutgoingMessage::OutgoingMessage(std::shared_ptr point) io = nullptr; type = point->getType(); - quality = point->getQuality(); - value = point->getValue(); + info = point->getInfo(); causeOfTransmission = CS101_COT_UNKNOWN_COT; @@ -59,6 +59,9 @@ OutgoingMessage::OutgoingMessage(std::shared_ptr point) commonAddress = _station->getCommonAddress(); + // updated locally processed timestamp before transmission + point->setProcessedAt(std::chrono::system_clock::now()); + informationObjectAddress = point->getInformationObjectAddress(); DEBUG_PRINT(Debug::Message, "Created (outgoing)"); } diff --git a/src/remote/message/OutgoingMessage.h b/src/remote/message/OutgoingMessage.h index 5cdc3fa..ba5a5e6 100644 --- a/src/remote/message/OutgoingMessage.h +++ b/src/remote/message/OutgoingMessage.h @@ -71,7 +71,7 @@ class OutgoingMessage : public IMessageInterface, * @param point point that defines the receiver and related information of the * outgoing message */ - explicit OutgoingMessage(std::shared_ptr point); + explicit OutgoingMessage(const std::shared_ptr &point); }; } // namespace Message diff --git a/src/remote/message/PointCommand.cpp b/src/remote/message/PointCommand.cpp index f87a31c..82bf3ba 100644 --- a/src/remote/message/PointCommand.cpp +++ b/src/remote/message/PointCommand.cpp @@ -31,120 +31,127 @@ #include "PointCommand.h" #include "object/DataPoint.h" +#include "object/Information.h" using namespace Remote::Message; PointCommand::PointCommand(std::shared_ptr point, - const bool select, - const CS101_QualifierOfCommand qualifier) - : OutgoingMessage(point), updated_at(0), duration({0}), time({0}) { + const bool select) + : OutgoingMessage(point) { causeOfTransmission = CS101_COT_ACTIVATION; - updated_at = point->getUpdatedAt_ms(); - - CP56Time2a_createFromMsTimestamp(&time, updated_at); switch (type) { - // bool Single Point Command + case C_SC_NA_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)SingleCommand_create( - nullptr, informationObjectAddress, (bool)value.load(), select, - static_cast(qualifier)); + nullptr, informationObjectAddress, i->isOn(), select, + static_cast(i->getQualifier())); } break; - // bool Single Point Command + Extended Time case C_SC_TA_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)SingleCommandWithCP56Time2a_create( - nullptr, informationObjectAddress, (bool)value.load(), select, - static_cast(qualifier), &time); + nullptr, informationObjectAddress, i->isOn(), select, + static_cast(i->getQualifier()), &time); } break; - // enum Double Point Command [INVALID|OFF|ON|INVALID] case C_DC_NA_1: { - auto state = (DoublePointValue)value.load(); + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)DoubleCommand_create( - nullptr, informationObjectAddress, state, select, - static_cast(qualifier)); + nullptr, informationObjectAddress, i->getState(), select, + static_cast(i->getQualifier())); } break; - // enum Double Point Command [INVALID|OFF|ON|INVALID] + Extended Time case C_DC_TA_1: { - auto state = (DoublePointValue)value.load(); + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)DoubleCommandWithCP56Time2a_create( - nullptr, informationObjectAddress, state, select, - static_cast(qualifier), &time); + nullptr, informationObjectAddress, i->getState(), select, + static_cast(i->getQualifier()), &time); } break; - // int [INVALID,LOWER,HIGHER,INVALID] Regulating StepPosition Command - // (Trafo) case C_RC_NA_1: { - auto state = (StepCommandValue)value.load(); + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)StepCommand_create( - nullptr, informationObjectAddress, state, select, - static_cast(qualifier)); + nullptr, informationObjectAddress, i->getStep(), select, + static_cast(i->getQualifier())); } break; - // int [INVALID,LOWER,HIGHER,INVALID] Regulating StepPosition Command - // (Trafo) + Extended Time case C_RC_TA_1: { - auto state = (StepCommandValue)value.load(); + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)StepCommandWithCP56Time2a_create( - nullptr, informationObjectAddress, state, select, - static_cast(qualifier), &time); + nullptr, informationObjectAddress, i->getStep(), select, + static_cast(i->getQualifier()), &time); } break; - //[0,2^32] BitString Command 32bits case C_BO_NA_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)Bitstring32Command_create( - nullptr, informationObjectAddress, (uint32_t)value.load()); + nullptr, informationObjectAddress, i->getBlob().get()); } break; - //[0,2^32] BitString 32bits Command + Extended Time case C_BO_TA_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)Bitstring32CommandWithCP56Time2a_create( - nullptr, informationObjectAddress, (uint32_t)value.load(), &time); + nullptr, informationObjectAddress, i->getBlob().get(), &time); } break; - // float Setpoint Command (NORMALIZED) case C_SE_NA_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)SetpointCommandNormalized_create( - nullptr, informationObjectAddress, (float)value.load(), select, - static_cast(quality.load())); + nullptr, informationObjectAddress, i->getTarget().get(), select, + i->getQualifier().get()); } break; - // float Setpoint Command (NORMALIZED) + Extended Time case C_SE_TA_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)SetpointCommandNormalizedWithCP56Time2a_create( - nullptr, informationObjectAddress, (float)value.load(), select, - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->getTarget().get(), select, + i->getQualifier().get(), &time); } break; - // int Setpoint Command (SCALED) case C_SE_NB_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)SetpointCommandScaled_create( - nullptr, informationObjectAddress, (int)value.load(), select, - static_cast(quality.load())); + nullptr, informationObjectAddress, i->getTarget().get(), select, + i->getQualifier().get()); } break; - // int Setpoint Command (SCALED) + Extended Time - // Valid cause of transmission: 1,2,3,5,20-36 case C_SE_TB_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)SetpointCommandScaledWithCP56Time2a_create( - nullptr, informationObjectAddress, (int)value.load(), select, - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->getTarget().get(), select, + i->getQualifier().get(), &time); } break; // float Setpoint Command (SHORT) case C_SE_NC_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)SetpointCommandShort_create( - nullptr, informationObjectAddress, (float)value.load(), select, - static_cast(quality.load())); + nullptr, informationObjectAddress, i->getTarget(), select, + i->getQualifier().get()); } break; // float Setpoint Command (SHORT) + Extended Time case C_SE_TC_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)SetpointCommandShortWithCP56Time2a_create( - nullptr, informationObjectAddress, (float)value.load(), select, - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->getTarget(), select, + i->getQualifier().get(), &time); } break; default: diff --git a/src/remote/message/PointCommand.h b/src/remote/message/PointCommand.h index 59d8430..e62da2c 100644 --- a/src/remote/message/PointCommand.h +++ b/src/remote/message/PointCommand.h @@ -48,20 +48,17 @@ class PointCommand : public OutgoingMessage { /** * @brief Create a message for a certain DataPoint, type of message is * identified via DataPoint - * @param point point whom's value should be reported to remote client + * @param point point who's value should be reported to remote client * @param select flag for select and execute command mode (lock control * access) - * @param qualifier parameter for command duration * @throws std::invalid_argument if point reference, connection or server * reference is invalid */ [[nodiscard]] static std::shared_ptr - create(std::shared_ptr point, const bool select = false, - const CS101_QualifierOfCommand qualifier = - CS101_QualifierOfCommand::NONE) { + create(std::shared_ptr point, const bool select = false) { // Not using std::make_shared because the constructor is private. return std::shared_ptr( - new PointCommand(std::move(point), select, qualifier)); + new PointCommand(std::move(point), select)); } /** @@ -76,21 +73,10 @@ class PointCommand : public OutgoingMessage { * @param point point whom's value should be reported to remote client * @param select flag for select and execute command mode (lock control * access) - * @param qualifier parameter for command duration * @throws std::invalid_argument if point reference, connection or server * reference is invalid */ - explicit PointCommand(std::shared_ptr point, bool select, - CS101_QualifierOfCommand qualifier); - - /// @brief timestamp of measurement in milliseconds - uint_fast64_t updated_at; - - /// @brief duration of event if required - sCP16Time2a duration; - - /// @brief timestamp of measurement formatted as CP56Time2a - sCP56Time2a time; + PointCommand(std::shared_ptr point, bool select); }; } // namespace Message diff --git a/src/remote/message/PointMessage.cpp b/src/remote/message/PointMessage.cpp index fc3e552..fe9d28b 100644 --- a/src/remote/message/PointMessage.cpp +++ b/src/remote/message/PointMessage.cpp @@ -31,225 +31,242 @@ #include "PointMessage.h" #include "object/DataPoint.h" +#include "object/Information.h" using namespace Remote::Message; PointMessage::PointMessage(std::shared_ptr point) - : OutgoingMessage(point), updated_at(0), duration({0}), time({0}) { + : OutgoingMessage(point) { causeOfTransmission = CS101_COT_SPONTANEOUS; - updated_at = point->getUpdatedAt_ms(); - - CP56Time2a_createFromMsTimestamp(&time, updated_at); switch (type) { - // bool Single Point + // Valid cause of transmission: 2,3,5,11,12,20-36 case M_SP_NA_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)SinglePointInformation_create( - nullptr, informationObjectAddress, (bool)value.load(), - static_cast(quality.load())); + nullptr, informationObjectAddress, i->isOn(), + static_cast(std::get(i->getQuality()))); } break; - // bool Single Point + Extended Time // Valid cause of transmission: 2,3,5,11,12,20-36 case M_SP_TB_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)SinglePointWithCP56Time2a_create( - nullptr, informationObjectAddress, (bool)value.load(), - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->isOn(), + static_cast(std::get(i->getQuality())), &time); } break; - // enum Double Point [INTERMEDIATE|ON|OFF|INDETERMINATE] // Valid cause of transmission: 2,3,5,11,12,20-36 case M_DP_NA_1: { - auto state = (DoublePointValue)value.load(); + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)DoublePointInformation_create( - nullptr, informationObjectAddress, state, - static_cast(quality.load())); + nullptr, informationObjectAddress, i->getState(), + static_cast(std::get(i->getQuality()))); } break; - // enum Double Point [INTERMEDIATE|ON|OFF|INDETERMINATE] + Extended Time // Valid cause of transmission: 2,3,5,11,12,20-36 case M_DP_TB_1: { - auto state = (DoublePointValue)value.load(); + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)DoublePointWithCP56Time2a_create( - nullptr, informationObjectAddress, state, - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->getState(), + static_cast(std::get(i->getQuality())), &time); } break; - // int [-64,63] StepPosition (Trafo) // Valid cause of transmission: 2,3,5,11,12,20-36 case M_ST_NA_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)StepPositionInformation_create( - nullptr, informationObjectAddress, (int)value.load(), false, - static_cast(quality.load())); + nullptr, informationObjectAddress, i->getPosition().get(), + i->isTransient(), + static_cast(std::get(i->getQuality()))); } break; - // int [-64,63] StepPosition (Trafo) + Extended Time // Valid cause of transmission: 2,3,5,11,12,20-36 case M_ST_TB_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)StepPositionWithCP56Time2a_create( - nullptr, informationObjectAddress, (int)value.load(), false, - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->getPosition().get(), + i->isTransient(), + static_cast(std::get(i->getQuality())), &time); } break; - // uint32_t [0,2^32] BitString 32bits case M_BO_NA_1: { - // @todo what happens in case of bad quality ?? + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)BitString32_create( - nullptr, informationObjectAddress, (uint32_t)value.load()); + nullptr, informationObjectAddress, i->getBlob().get()); } break; - // uint32_t [0,2^32] BitString 32bits + Extended Time case M_BO_TB_1: { - // @todo what happens in case of bad quality ?? + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)Bitstring32WithCP56Time2a_create( - nullptr, informationObjectAddress, (uint32_t)value.load(), &time); + nullptr, informationObjectAddress, i->getBlob().get(), &time); } break; - // float Measurement Value (NORMALIZED) - // NORMALIZATION: from [-1.0f to +1.0f] encoded to int16 [-32.768‬ - // to 32.767] Valid cause of transmission: 1,2,3,5,20-36 + // Valid cause of transmission: 1,2,3,5,20-36 case M_ME_NA_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)MeasuredValueNormalized_create( - nullptr, informationObjectAddress, (float)value.load(), - static_cast(quality.load())); + nullptr, informationObjectAddress, i->getActual().get(), + static_cast(std::get(i->getQuality()))); } break; - // int16 Measurement Value (NORMALIZED) + Extended Time - // NORMALIZATION: from [-1.0f to +1.0f] encoded to [-32.768‬ to 32.767] // Valid cause of transmission: 1,2,3,5,20-36 case M_ME_TD_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)MeasuredValueNormalizedWithCP56Time2a_create( - nullptr, informationObjectAddress, (float)value.load(), - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->getActual().get(), + static_cast(std::get(i->getQuality())), &time); } break; - // uint16 Measurement Value (SCALED) - // SCALED: from [-65536 to +65535] encoded to [0 to 65535] via negative - // values + 65535 Valid cause of transmission: 1,2,3,5,20-36 + // Valid cause of transmission: 1,2,3,5,20-36 case M_ME_NB_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)MeasuredValueScaled_create( - nullptr, informationObjectAddress, (int)value.load(), - static_cast(quality.load())); + nullptr, informationObjectAddress, i->getActual().get(), + static_cast(std::get(i->getQuality()))); } break; - // uint16 Measurement Value (SCALED) + Extended Time - // SCALED: from [-65536 to +65535] encoded to [0 to 65535] via negative - // values + 65535 Valid cause of transmission: 1,2,3,5,20-36 + // Valid cause of transmission: 1,2,3,5,20-36 case M_ME_TE_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)MeasuredValueScaledWithCP56Time2a_create( - nullptr, informationObjectAddress, (int)value.load(), - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->getActual().get(), + static_cast(std::get(i->getQuality())), &time); } break; - // float (32bit-256) Measurement Value (SHORT) - // [-32.768‬ to 32.767] // Valid cause of transmission: 1,2,3,5,20-36 case M_ME_NC_1: { + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)MeasuredValueShort_create( - nullptr, informationObjectAddress, (float)value.load(), - static_cast(quality.load())); + nullptr, informationObjectAddress, i->getActual(), + static_cast(std::get(i->getQuality()))); } break; - // float (32bit-256) Measurement Value (SHORT) + Extended Time - // [-32.768‬ to 32.767] // Valid cause of transmission: 1,2,3,5,20-36 case M_ME_TF_1: { + auto i = std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); io = (InformationObject)MeasuredValueShortWithCP56Time2a_create( - nullptr, informationObjectAddress, (float)value.load(), - static_cast(quality.load()), &time); + nullptr, informationObjectAddress, i->getActual(), + static_cast(std::get(i->getQuality())), &time); } break; - // Encoded Counter Value case M_IT_NA_1: { - // @todo what happens in case of bad quality ?? - // @todo flags and sequence number usage - uint_fast8_t seqNumber = 0; - bool hasCarry = ::test(quality.load(), Quality::Overflow), - isAdjusted = false, isInvalid = is_any(quality.load()); - io = (InformationObject)BinaryCounterReading_create( - nullptr, (int)value.load(), seqNumber, hasCarry, isAdjusted, isInvalid); + auto i = std::dynamic_pointer_cast(info); + auto q = std::get(i->getQuality()); + BinaryCounterReading _value = BinaryCounterReading_create( + nullptr, i->getCounter(), i->getSequence().get(), + ::test(q, BinaryCounterQuality::Carry), + ::test(q, BinaryCounterQuality::Adjusted), + ::test(q, BinaryCounterQuality::Invalid)); + io = (InformationObject)IntegratedTotals_create( + nullptr, informationObjectAddress, _value); } break; - // Encoded Counter Value + Extended Timer case M_IT_TB_1: { - // @todo what happens in case of bad quality ?? - // @todo flags and sequence number usage - uint_fast8_t seqNumber = 0; - bool hasCarry = ::test(quality.load(), Quality::Overflow), - isAdjusted = false, isInvalid = is_any(quality.load()); + auto i = std::dynamic_pointer_cast(info); + auto q = std::get(i->getQuality()); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); BinaryCounterReading _value = BinaryCounterReading_create( - nullptr, (int)value.load(), seqNumber, hasCarry, isAdjusted, isInvalid); + nullptr, i->getCounter(), i->getSequence().get(), + ::test(q, BinaryCounterQuality::Carry), + ::test(q, BinaryCounterQuality::Adjusted), + ::test(q, BinaryCounterQuality::Invalid)); io = (InformationObject)IntegratedTotalsWithCP56Time2a_create( nullptr, informationObjectAddress, _value, &time); } break; - // SingleEvent Protection Equipment + Extended Timer - //@todo not yet supported - set event case M_EP_TD_1: { - throw std::invalid_argument("Event messages not supported!"); - uint_fast32_t elapsedTime_ms = 0; - CP16Time2a_setEplapsedTimeInMs(&duration, elapsedTime_ms); - tSingleEvent event = IEC60870_EVENTSTATE_ON; // untested... lifetime ?? + auto i = + std::dynamic_pointer_cast(info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); + sCP16Time2a elapsed{}; + CP16Time2a_setEplapsedTimeInMs(&elapsed, i->getElapsed_ms().get()); + tSingleEvent event = + ((static_cast(i->getState()) & 0b00000111) | + (static_cast(std::get(i->getQuality())) & + 0b11111000)); io = (InformationObject)EventOfProtectionEquipmentWithCP56Time2a_create( - nullptr, informationObjectAddress, &event, &duration, &time); + nullptr, informationObjectAddress, &event, &elapsed, &time); } break; - // StartEvent Protection Equipment + Extended Timer - //@todo not yet supported - set event case M_EP_TE_1: { - throw std::invalid_argument("Event messages not supported!"); - uint_fast32_t elapsedTime_ms = 0; - CP16Time2a_setEplapsedTimeInMs(&duration, elapsedTime_ms); + auto i = + std::dynamic_pointer_cast( + info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); + sCP16Time2a elapsed{}; + CP16Time2a_setEplapsedTimeInMs(&elapsed, i->getRelayDuration_ms().get()); io = (InformationObject) PackedStartEventsOfProtectionEquipmentWithCP56Time2a_create( - nullptr, informationObjectAddress, IEC60870_START_EVENT_GS, - static_cast(quality.load()), &duration, &time); + nullptr, informationObjectAddress, + static_cast(i->getEvents()), + static_cast(std::get(i->getQuality())), &elapsed, + &time); } break; - // OuputCircuitInfo Protection Equipment + Extended Timer - //@todo not yet supported - set output curcuit info case M_EP_TF_1: { - throw std::invalid_argument("Event messages not supported!"); - uint_fast32_t operatingTime_ms = 0; - CP16Time2a_setEplapsedTimeInMs(&duration, operatingTime_ms); + auto i = + std::dynamic_pointer_cast( + info); + sCP56Time2a time{}; + from_time_point(&time, i->getRecordedAt().value_or(i->getProcessedAt())); + sCP16Time2a elapsed{}; + CP16Time2a_setEplapsedTimeInMs(&elapsed, i->getRelayOperating_ms().get()); io = (InformationObject)PackedOutputCircuitInfoWithCP56Time2a_create( - nullptr, informationObjectAddress, IEC60870_OUTPUT_CI_GC, - static_cast(quality.load()), &duration, &time); + nullptr, informationObjectAddress, + static_cast(i->getCircuits()), + static_cast(std::get(i->getQuality())), &elapsed, + &time); } break; - // StatusAndStatusChangeDetection Single + Change Detection - //@todo not yet supported - set sscd info case M_PS_NA_1: { - throw std::invalid_argument("StatusAndStatusChangeDetection messages not " - "supported!"); - sStatusAndStatusChangeDetection sscd{0}; // untested... lifetime ?? - StatusAndStatusChangeDetection_setSTn(&sscd, 0); + auto i = std::dynamic_pointer_cast(info); + sStatusAndStatusChangeDetection sscd{}; + auto status = static_cast(i->getStatus()); + auto changed = static_cast(i->getChanged()); + sscd.encodedValue[0] = (status >> 0) & 0b11111111; + sscd.encodedValue[1] = (status >> 8) & 0b11111111; + sscd.encodedValue[2] = (changed >> 0) & 0b11111111; + sscd.encodedValue[3] = (changed >> 8) & 0b11111111; io = (InformationObject)PackedSinglePointWithSCD_create( nullptr, informationObjectAddress, &sscd, - static_cast(quality.load())); + static_cast(std::get(i->getQuality()))); } break; // float Measurement Value (NORMALIZED) - Quality case M_ME_ND_1: { - // @todo what happens in case of bad quality ?? + auto i = std::dynamic_pointer_cast(info); io = (InformationObject)MeasuredValueNormalizedWithoutQuality_create( - nullptr, informationObjectAddress, (float)value.load()); + nullptr, informationObjectAddress, i->getActual().get()); } break; // End of initialization case M_EI_NA_1: { + // todo remove?? throw std::invalid_argument("End of initialization is not a PointMessage!"); informationObjectAddress = 0; io = (InformationObject)EndOfInitialization_create( nullptr, IEC60870_COI_REMOTE_RESET); } break; - case S_IT_TC_1: { - // @todo add IT messages - throw std::invalid_argument("Integrated totals messages not supported!"); - } break; - case M_SP_TA_1: case M_DP_TA_1: case M_ST_TA_1: diff --git a/src/remote/message/PointMessage.h b/src/remote/message/PointMessage.h index b331ecb..a473b61 100644 --- a/src/remote/message/PointMessage.h +++ b/src/remote/message/PointMessage.h @@ -59,18 +59,9 @@ class PointMessage : public OutgoingMessage { /** * @brief Create a message for a certain DataPoint, type of message is * identified via DataPoint - * @param point point whom's value should be reported to remote client + * @param point point who's value should be reported to remote client */ explicit PointMessage(std::shared_ptr point); - - /// @brief timestamp of measurement in milliseconds - uint_fast64_t updated_at; - - /// @brief duration of event if required - sCP16Time2a duration; - - /// @brief timestamp of measurement formatted as CP56Time2a - sCP56Time2a time; }; } // namespace Message diff --git a/src/types.cpp b/src/types.cpp index a42e013..2c3566b 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -30,8 +30,6 @@ */ #include "types.h" -#include -#include std::atomic GLOBAL_DEBUG_MODE{Debug::None}; @@ -49,14 +47,47 @@ void disableDebug(Debug mode) { void printDebugMessage(const Debug mode, const std::string &message) { if (test(GLOBAL_DEBUG_MODE.load(), mode)) { - std::stringstream print_str{}; - print_str << "[c104." << Debug_toFlagString(mode) << "] " << message - << std::endl; - std::cout << print_str.str(); + std::ostringstream oss; + oss << "[c104." << Debug_toFlagString(mode) << "] " << message << std::endl; + std::cout << oss.str(); std::cout.flush(); } } +std::string bool_toString(const bool &val) { return val ? "True" : "False"; } + +std::string Byte32_toString(const Byte32 &byte) { + std::bitset<32> bits(byte.get()); + return "0b" + bits.to_string(); +} + +std::string +TimePoint_toString(const std::chrono::system_clock::time_point &time) { + using us_t = std::chrono::duration; + auto us = std::chrono::duration_cast(time.time_since_epoch() % + std::chrono::seconds(1)); + if (us.count() < 0) { + us += std::chrono::seconds(1); + } + + std::time_t tt = std::chrono::system_clock::to_time_t( + std::chrono::time_point_cast(time - + us)); + + std::tm local_tt = *std::localtime(&tt); + + std::ostringstream oss; + oss << std::put_time(&local_tt, "%Y-%m-%dT%H:%M:%S"); + oss << '.' << std::setw(3) << std::setfill('0') << us.count(); + oss << std::put_time(&local_tt, "%z"); + return oss.str(); +} + +std::string TimePoint_toString( + const std::optional &time) { + return time.has_value() ? TimePoint_toString(time.value()) : "None"; +} + void Assert_IPv4(const std::string &s) { if (s == "localhost" || s == "lo") return; @@ -74,4 +105,69 @@ void Assert_Port(const int_fast64_t port) { " is invalid!"); } -uint_fast64_t GetTimestamp_ms() { return Hal_getTimeInMs(); } +std::chrono::system_clock::time_point to_time_point(const CP56Time2a time) { + return std::chrono::system_clock::time_point( + std::chrono::milliseconds(CP56Time2a_toMsTimestamp(time))); +} + +void from_time_point(CP56Time2a time, + const std::chrono::system_clock::time_point time_point) { + auto millis = std::chrono::duration_cast( + time_point.time_since_epoch()) + .count(); + + CP56Time2a_createFromMsTimestamp(time, static_cast(millis)); +} + +struct InfoValueToStringVisitor { + std::string operator()(std::monostate value) const { return "N.A."; } + std::string operator()(bool value) const { return std::to_string(value); } + std::string operator()(DoublePointValue value) const { + return DoublePointValue_toString(value); + } + std::string operator()(const LimitedInt7 &obj) { + return std::to_string(obj.get()); + } + std::string operator()(StepCommandValue value) const { + return StepCommandValue_toString(value); + } + std::string operator()(const Byte32 &obj) { + return std::to_string(obj.get()); + } + std::string operator()(const NormalizedFloat &obj) { + return std::to_string(obj.get()); + } + std::string operator()(const LimitedInt16 &obj) { + return std::to_string(obj.get()); + } + std::string operator()(float value) const { return std::to_string(value); } + std::string operator()(int32_t value) const { return std::to_string(value); } + std::string operator()(EventState value) const { + return EventState_toString(value); + } + std::string operator()(const StartEvents &obj) { + return StartEvents_toString(obj); + } + std::string operator()(const OutputCircuits &obj) { + return OutputCircuits_toString(obj); + } + std::string operator()(const FieldSet16 &obj) { + return FieldSet16_toString(obj); + } +}; + +std::string InfoValue_toString(const InfoValue &value) { + return std::visit(InfoValueToStringVisitor(), value); +} + +struct QualityValueToStringVisitor { + std::string operator()(std::monostate value) const { return "N. A."; } + std::string operator()(const Quality &obj) { return Quality_toString(obj); } + std::string operator()(const BinaryCounterQuality &obj) { + return BinaryCounterQuality_toString(obj); + } +}; + +std::string InfoQuality_toString(const InfoQuality &value) { + return std::visit(QualityValueToStringVisitor(), value); +} diff --git a/src/types.h b/src/types.h index e595436..ad3d693 100644 --- a/src/types.h +++ b/src/types.h @@ -1,5 +1,5 @@ /** - * Copyright 2020-2023 Fraunhofer Institute for Applied Information Technology + * Copyright 2020-2024 Fraunhofer Institute for Applied Information Technology * FIT * * This file is part of iec104-python. @@ -34,6 +34,7 @@ #include #include +#include #include #include #include @@ -48,18 +49,32 @@ #include #include #include +#include #include #include +#include #include #include "enums.h" +#include "numbers.h" -#define DEBUG_PRINT_CONDITION(X, Y, Z) (X ? printDebugMessage(Y, Z) : (void)0) +#define DEBUG_PRINT_CONDITION(X, Y, Z) \ + ((X) ? printDebugMessage((Y), (Z)) : (void)0) #define DEBUG_PRINT(mode, Y) \ - (::test(GLOBAL_DEBUG_MODE.load(), mode) ? printDebugMessage(mode, Y) \ + (::test(GLOBAL_DEBUG_MODE.load(), mode) ? printDebugMessage(mode, (Y)) \ : (void)0) #define DEBUG_TEST(mode) ::test(GLOBAL_DEBUG_MODE.load(), mode) +#define MICRO_SEC_STR u8" \xc2\xb5s" +#define DIFF_MS(begin, end) \ + std::chrono::duration_cast((end) - (begin)).count() +#define TICTOC(begin, end) \ + (std::to_string(DIFF_MS(begin, end)) + \ + std::string(reinterpret_cast(MICRO_SEC_STR))) +#define TICTOCNOW(begin) TICTOC(begin, std::chrono::steady_clock::now()) +#define MAX_INFORMATION_OBJECT_ADDRESS 16777215 +#define UNDEFINED_INFORMATION_OBJECT_ADDRESS 16777216 + extern std::atomic GLOBAL_DEBUG_MODE; void setDebug(Debug mode); @@ -72,6 +87,16 @@ void disableDebug(Debug mode); void printDebugMessage(Debug mode, const std::string &message); +std::string bool_toString(const bool &val); + +std::string Byte32_toString(const Byte32 &byte); + +std::string +TimePoint_toString(const std::chrono::system_clock::time_point &time); + +std::string TimePoint_toString( + const std::optional &time); + /** * @brief Validate and convert an ip address from string to in_addr struct * @param s ipv4 address in string representation @@ -86,15 +111,33 @@ void Assert_IPv4(const std::string &s); */ void Assert_Port(int_fast64_t port); -/** - * @brief Get the time since epoch in milliseconds - * @return number of milliseconds since 2000-01-01 00:00:00 - */ -uint_fast64_t GetTimestamp_ms(); +std::chrono::system_clock::time_point to_time_point(const CP56Time2a time); +void from_time_point(CP56Time2a time, + const std::chrono::system_clock::time_point time_point); + +struct Task { + std::function function; + std::chrono::steady_clock::time_point schedule_time; + bool operator<(const Task &rhs) const { + return schedule_time > rhs.schedule_time; + } +}; +constexpr auto TASK_DELAY_THRESHOLD = std::chrono::milliseconds(100); + +typedef std::variant + InfoValue; +typedef std::variant InfoQuality; + +std::string InfoValue_toString(const InfoValue &value); +std::string InfoQuality_toString(const InfoQuality &value); // forward declaration to avoid .h loop inclusion namespace Object { class DataPoint; +class Information; class Station; } // namespace Object diff --git a/tests/client.py b/tests/client.py index 43f5311..abba59b 100644 --- a/tests/client.py +++ b/tests/client.py @@ -22,10 +22,10 @@ import functools import time -import sys +import datetime +from pathlib import Path import c104 -from pathlib import Path USE_TLS = True ROOT = Path(__file__).absolute().parent @@ -39,9 +39,9 @@ tlsconf.set_ca_certificate(cert=str(ROOT / "certs/ca.crt")) tlsconf.set_version(min=c104.TlsVersion.TLS_1_2, max=c104.TlsVersion.TLS_1_2) tlsconf.add_allowed_remote_certificate(cert=str(ROOT / "certs/server1.crt")) - my_client = c104.Client(tick_rate_ms=1000, command_timeout_ms=5000, transport_security=tlsconf) + my_client = c104.Client(tick_rate_ms=100, command_timeout_ms=100, transport_security=tlsconf) else: - my_client = c104.Client(tick_rate_ms=1000, command_timeout_ms=5000) + my_client = c104.Client(tick_rate_ms=100, command_timeout_ms=100) my_client.originator_address = 123 @@ -63,8 +63,8 @@ def cl_ct_on_state_change(connection: c104.Connection, state: c104.ConnectionSta # NEW DATA HANDLER ################################## -def cl_pt_on_receive_point(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - print("CL] {0} REPORT on IOA: {1} , new: {2}, timestamp: {3}, prev: {5}, cot: {6}, quality: {6}".format(point.type, point.io_address, point.value, point.updated_at_ms, previous_state, message.cot, point.quality)) +def cl_pt_on_receive_point(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + print("CL] {0} REPORT on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) # print("{0}".format(message.is_negative)) # print("-->| POINT: 0x{0} | EXPLAIN: {1}".format(message.raw.hex(), c104.explain_bytes(apdu=message.raw))) return c104.ResponseState.SUCCESS @@ -128,13 +128,13 @@ def cl_dump(): st = ct.stations[st_iter] st_pt_count = len(st.points) print(" |--+ STATION {0} has {1} points".format(st.common_address, st_pt_count)) - print(" | TYPE | IOA | VALUE | UPDATED AT | REPORTED AT | QUALITY ") - print(" |----------------|------------|----------------------|----------------------|----------------------|-------------------") + print(" | TYPE | IOA | VALUE | PROCESSED AT | RECORDED AT | QUALITY ") + print(" |----------------|---------|--------------------|----------------------------|----------------------------|-------------------") for pt_iter in range(st_pt_count): pt = st.points[pt_iter] - print(" | {0} | {1:10} | {2:20} | {3:20} | {4:20} | {5}".format(pt.type, pt.io_address, pt.value, pt.updated_at_ms, - pt.reported_at_ms, pt.quality)) - print(" |----------------|------------|----------------------|----------------------|----------------------|-------------------") + print(" | %s | %7s | %18s | %26s | %26s | %s" % (pt.type, pt.io_address, pt.value, pt.processed_at, + pt.recorded_at or 'N. A.', pt.quality)) + print(" |----------------|---------|--------------------|----------------------------|----------------------------|-------------------") ################################## @@ -144,7 +144,7 @@ def cl_dump(): input("Press Enter to start client...") my_client.start() -while not cl_connection_1.is_connected: +while not cl_connection_1.is_connected or cl_connection_1.is_muted: print("CL] Waiting for connection to {0}:{1}".format(cl_connection_1.ip, cl_connection_1.port)) time.sleep(3) @@ -188,16 +188,17 @@ def cl_dump(): if cl_single_command.transmit(cause=c104.Cot.ACTIVATION): print("CL] transmit: Single command OFF successful") else: - print("CL] transmit: Single command OFF failed") + print("CL] transmit: Single command OFF failed (not selected)") time.sleep(1) cl_single_command.command_mode = c104.CommandMode.SELECT_AND_EXECUTE print(cl_single_command.command_mode) -cl_single_command.value = False -if cl_single_command.transmit(cause=c104.Cot.ACTIVATION, qualifier=c104.Qoc.SHORT_PULSE): - print("CL] transmit: Single command OFF successful") +cl_single_command.info = c104.SingleCmd(on=False, qualifier=c104.Qoc.SHORT_PULSE) + +if cl_single_command.transmit(cause=c104.Cot.ACTIVATION): + print("CL] transmit: Single command OFF successful (selected)") else: print("CL] transmit: Single command OFF failed") @@ -209,15 +210,15 @@ def cl_dump(): cl_double_command = cl_station_2.add_point(io_address=22, type=c104.Type.C_DC_TA_1) -cl_double_command.set(value=c104.Double.ON, timestamp_ms=1711111111111) -if cl_double_command.transmit(cause=c104.Cot.ACTIVATION, qualifier=c104.Qoc.LONG_PULSE): +cl_double_command.info = c104.DoubleCmd(state=c104.Double.ON, qualifier=c104.Qoc.LONG_PULSE, recorded_at=datetime.datetime.fromtimestamp(1711111111.111)) +if cl_double_command.transmit(cause=c104.Cot.ACTIVATION): print("CL] transmit: Double command ON successful") else: print("CL] transmit: Double command ON failed") time.sleep(1) -cl_double_command.set(value=c104.Double.OFF, timestamp_ms=1711111111111) +cl_double_command.info = c104.DoubleCmd(state=c104.Double.OFF, qualifier=c104.Qoc.PERSISTENT, recorded_at=datetime.datetime.fromtimestamp(1711111111.111)) if cl_double_command.transmit(cause=c104.Cot.ACTIVATION): print("CL] transmit: Double command OFF successful") else: @@ -263,7 +264,8 @@ def cl_dump(): time.sleep(3) if cl_step_command: - if cl_step_command.transmit(cause=c104.Cot.ACTIVATION, qualifier=c104.Qoc.CONTINUOUS): + cl_step_command.info = c104.StepCmd(direction=c104.Step.HIGHER, qualifier=c104.Qoc.PERSISTENT) + if cl_step_command.transmit(cause=c104.Cot.ACTIVATION): print("CL] > transmit: Step command successful") else: print("CL] > transmit: Step command failed") diff --git a/tests/client_reconnect.py b/tests/client_reconnect.py new file mode 100644 index 0000000..d936d1d --- /dev/null +++ b/tests/client_reconnect.py @@ -0,0 +1,32 @@ +import c104 +import time + +IP_ADDRESS = "127.0.0.1" # Any invalid IP +STATION_COMMON_ADDRESS = 47 + + +def main(): + client = c104.Client(tick_rate_ms=100, command_timeout_ms=5000) + connection = client.add_connection( + ip=IP_ADDRESS, port=2404, init=c104.Init.INTERROGATION + ) + station = connection.add_station(common_address=STATION_COMMON_ADDRESS) + + try: + while True: + print("Loop Start") + client.start() + print("Loop Started") + time.sleep(0.2) + connection.disconnect() + print("Disconnected") + client.stop() # or client.reconnect_all() + print("Loop End") # This never occurs, because the script will stop at client.stop() + except KeyboardInterrupt: + print("quit client") + + +if __name__ == "__main__": + c104.set_debug_mode(mode=c104.Debug.Client|c104.Debug.Connection) + main() + time.sleep(1) diff --git a/tests/loop.py b/tests/loop.py new file mode 100644 index 0000000..0eda787 --- /dev/null +++ b/tests/loop.py @@ -0,0 +1,33 @@ +import c104 +import time + +IP_ADDRESS = "10.11.1.41" # Any invalid IP +STATION_COMMON_ADDRESS = 1 + + +def main(): + client = c104.Client(tick_rate_ms=1000, command_timeout_ms=1000) + connection = client.add_connection( + ip=IP_ADDRESS, port=2404, init=c104.Init.INTERROGATION + ) + station = connection.add_station(common_address=STATION_COMMON_ADDRESS) + + for i in range(30): + t = time.time() + print("Client start", i, "/", 30) + client.start() + client.stop() # or client.reconnect_all() + print("Client stopped after", round(time.time() - t, 3), "sec") + + server = c104.Server(ip="0.0.0.0", port=2404, tick_rate_ms=1000, max_connections=10) + + for i in range(30): + t = time.time() + print("Server start", i, "/", 30) + server.start() + server.stop() # or client.reconnect_all() + print("Server stopped after", round(time.time() - t, 3), "sec") + + +if __name__ == "__main__": + main() diff --git a/tests/server.py b/tests/server.py index 621e683..cbb65c3 100644 --- a/tests/server.py +++ b/tests/server.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ - Copyright 2020-2023 Fraunhofer Institute for Applied Information Technology FIT + Copyright 2020-2024 Fraunhofer Institute for Applied Information Technology FIT This file is part of iec104-python. iec104-python is free software: you can redistribute it and/or modify @@ -37,9 +37,9 @@ tlsconf.set_ca_certificate(cert=str(ROOT / "certs/ca.crt")) tlsconf.set_version(min=c104.TlsVersion.TLS_1_2, max=c104.TlsVersion.TLS_1_2) tlsconf.add_allowed_remote_certificate(cert=str(ROOT / "certs/client1.crt")) - my_server = c104.Server(ip="0.0.0.0", port=19998, tick_rate_ms=2000, max_connections=10, transport_security=tlsconf) + my_server = c104.Server(ip="0.0.0.0", port=19998, tick_rate_ms=100, select_timeout_ms=100, max_connections=10, transport_security=tlsconf) else: - my_server = c104.Server(ip="0.0.0.0", port=19998, tick_rate_ms=2000, max_connections=10) + my_server = c104.Server(ip="0.0.0.0", port=19998, tick_rate_ms=100, select_timeout_ms=100, max_connections=10) my_server.max_connections = 11 @@ -83,21 +83,18 @@ def sv_on_unexpected_message(server: c104.Server, message: c104.IncomingMessage, # MEASUREMENT POINT WITH COMMAND ################################## -def sv_pt_on_setpoint_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - print("SV] {0} SETPOINT COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality)) - - if point.quality.is_good(): - if point.related_io_address: - print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) - related_point = sv_station_2.get_point(point.related_io_address) - if related_point: - print("SV] -> RELATED POINT VALUE UPDATE") - related_point.value = point.value - else: - print("SV] -> RELATED POINT NOT FOUND!") - return c104.ResponseState.SUCCESS +def sv_pt_on_setpoint_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + print("SV] {0} SETPOINT COMMAND on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) - return c104.ResponseState.FAILURE + if point.related_io_address: + print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) + related_point = sv_station_2.get_point(point.related_io_address) + if related_point: + print("SV] -> RELATED POINT VALUE UPDATE") + related_point.value = point.value + else: + print("SV] -> RELATED POINT NOT FOUND!") + return c104.ResponseState.SUCCESS sv_measurement_point = sv_station_2.add_point(io_address=11, type=c104.Type.M_ME_TF_1, report_ms=1000) @@ -115,17 +112,14 @@ def sv_pt_on_setpoint_command(point: c104.Point, previous_state: dict, message: # SINGLE POINT WITH COMMAND ################################## -def sv_pt_on_single_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - print("SV] {0} SINGLE COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}, command_qualifier: {6}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality, message.command_qualifier)) - - if point.quality.is_good(): - if message.is_select_command: - print("SV] -> SELECTED BY: {}".format(point.selected_by)) - else: - print("SV] -> EXECUTED BY {}, NEW SELECTED BY={}".format(message.originator_address, point.selected_by)) - return c104.ResponseState.SUCCESS +def sv_pt_on_single_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + print("SV] {0} SINGLE COMMAND on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) - return c104.ResponseState.FAILURE + if message.is_select_command: + print("SV] -> SELECTED BY: {}".format(point.selected_by)) + else: + print("SV] -> EXECUTED BY {}, NEW SELECTED BY={}".format(message.originator_address, point.selected_by)) + return c104.ResponseState.SUCCESS sv_single_point = sv_station_2.add_point(io_address=15, type=c104.Type.M_SP_NA_1) @@ -141,21 +135,18 @@ def sv_pt_on_single_command(point: c104.Point, previous_state: dict, message: c1 # DOUBLE POINT WITH COMMAND ################################## -def sv_pt_on_double_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - print("SV] {0} DOUBLE COMMAND on IOA: {1}, new: {2}, timestamp: {3}, prev: {4}, cot: {5}, quality: {6}, command_qualifier: {7}".format(point.type, point.io_address, point.value, point.updated_at_ms, previous_state, message.cot, point.quality, message.command_qualifier)) - - if point.quality.is_good(): - if point.related_io_address: - print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) - related_point = sv_station_2.get_point(point.related_io_address) - if related_point: - print("SV] -> RELATED POINT VALUE UPDATE") - related_point.value = point.value - else: - print("SV] -> RELATED POINT NOT FOUND!") - return c104.ResponseState.SUCCESS +def sv_pt_on_double_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + print("SV] {0} DOUBLE COMMAND on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) - return c104.ResponseState.FAILURE + if point.related_io_address: + print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) + related_point = sv_station_2.get_point(point.related_io_address) + if related_point: + print("SV] -> RELATED POINT VALUE UPDATE") + related_point.value = point.value + else: + print("SV] -> RELATED POINT NOT FOUND!") + return c104.ResponseState.SUCCESS sv_double_point = sv_station_2.add_point(io_address=21, type=c104.Type.M_DP_TB_1) @@ -169,12 +160,12 @@ def sv_pt_on_double_command(point: c104.Point, previous_state: dict, message: c1 # STEP POINT WITH COMMAND ################################## -sv_global_step_point_value = 0 +sv_global_step_point_value = c104.Int7(0) -def sv_pt_on_step_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: +def sv_pt_on_step_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: global sv_global_step_point_value - print("SV] {0} STEP COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}, command_qualifier: {6}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality, message.command_qualifier)) + print("SV] {0} STEP COMMAND on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) if point.value == c104.Step.LOWER: sv_global_step_point_value -= 1 @@ -221,17 +212,17 @@ def __init__(self, x: str): self.x = x def pt_on_before_auto_transmit_measurement_point(self, point: c104.Point) -> None: - print(self.x, "--> {0} AUTO TRANSMIT on IOA: {1} updated at: {2}".format(point.type, point.io_address, point.updated_at_ms)) + print(self.x, "--> {0} AUTO TRANSMIT on IOA: {1} recorded at: {2}".format(point.type, point.io_address, point.recorded_at)) sv_pmct = ServerPointMethodCallbackTestClass("SV] CALLBACK METHOD") time.sleep(5) -sv_measurement_point.set(value=1234, timestamp_ms=1711111111111) +sv_measurement_point.info = c104.ShortInfo(actual=1234, recorded_at=datetime.datetime.fromtimestamp(1711111111.111)) sv_measurement_point.transmit(cause=c104.Cot.SPONTANEOUS) sv_measurement_point.on_before_auto_transmit(callable=sv_pmct.pt_on_before_auto_transmit_measurement_point) time.sleep(5) -sv_measurement_point.set(value=-1234.56, quality=c104.Quality.Invalid, timestamp_ms=1711111111111) +sv_measurement_point.info = c104.ShortInfo(actual=-1234.56, quality=c104.Quality.Invalid, recorded_at=datetime.datetime.fromtimestamp(1711111111.111)) sv_measurement_point.transmit(cause=c104.Cot.SPONTANEOUS) ################################## diff --git a/tests/server_passive.py b/tests/server_passive.py new file mode 100644 index 0000000..0a95776 --- /dev/null +++ b/tests/server_passive.py @@ -0,0 +1,38 @@ +import c104 +import time +import random + + +def on_before_auto_transmit_step(point: c104.Point) -> None: + print("SV] {0} PERIODIC TRANSMIT on IOA: {1}".format(point.type, point.io_address)) + point.value = c104.Int7(random.randint(-64,63)) # import random + +def main(): + # server and station preparation + server = c104.Server(ip="127.0.0.1", port=2404) + station = server.add_station(common_address=47) + + # monitoring point preparation + p1 = station.add_point(io_address=11, type=c104.Type.M_ME_NC_1, report_ms=500) + p2 = station.add_point(io_address=14, type=c104.Type.M_ST_TB_1, report_ms=500) + p2.on_before_auto_transmit(callable=on_before_auto_transmit_step) + + c1 = station.add_point(io_address=15, type=c104.Type.C_RC_TA_1) + + # start + server.start() + + print("Keep alive until CTRL+C") + + try: + while True: + time.sleep(1) + print("open %s active %s" % (server.open_connection_count, server.active_connection_count)) + except KeyboardInterrupt: + print("quit server") + + +if __name__ == "__main__": + c104.set_debug_mode(c104.Debug.Server|c104.Debug.Point|c104.Debug.Callback) + main() + time.sleep(1) diff --git a/tests/test.py b/tests/test.py index 90204b3..df83883 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ - Copyright 2020-2023 Fraunhofer Institute for Applied Information Technology FIT + Copyright 2020-2024 Fraunhofer Institute for Applied Information Technology FIT This file is part of iec104-python. iec104-python is free software: you can redistribute it and/or modify @@ -28,21 +28,21 @@ print("- RUN: TEST") print("-"*60) -c104.set_debug_mode(mode=c104.Debug()) +c104.set_debug_mode(c104.Debug.Server|c104.Debug.Client|c104.Debug.Connection|c104.Debug.Point|c104.Debug.Callback) print("DEBUG MODE: {0}".format(c104.get_debug_mode())) ################################## # CLIENT ################################## -my_client = c104.Client(tick_rate_ms=1000, command_timeout_ms=5000) +my_client = c104.Client(tick_rate_ms=100, command_timeout_ms=100) my_client.originator_address = 123 -cl_connection_1 = my_client.add_connection(ip="127.0.0.1", port=2404) +cl_connection_1 = my_client.add_connection(ip="127.0.0.1", port=2404, init=c104.Init.ALL) -def cl_pt_on_receive_point(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - print("CL] {0} REPORT on IOA: {1} , new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality)) +def cl_pt_on_receive_point(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + print("CL] {0} REPORT on IOA: {1} , message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) # print("{0}".format(message.is_negative)) # print("-->| POINT: 0x{0} | EXPLAIN: {1}".format(message.raw.hex(), c104.explain_bytes(apdu=message.raw))) return c104.ResponseState.SUCCESS @@ -93,7 +93,7 @@ def cl_ct_on_send_raw(connection: c104.Connection, data: bytes) -> None: def cl_dump(): - global my_client,cl_connection_1 + global my_client, cl_connection_1 if cl_connection_1.is_connected: print("") cl_ct_count = len(my_client.connections) @@ -106,20 +106,20 @@ def cl_dump(): st = ct.stations[st_iter] st_pt_count = len(st.points) print(" |--+ STATION {0} has {1} points".format(st.common_address, st_pt_count)) - print(" | TYPE | IOA | VALUE | UPDATED AT | REPORTED AT | QUALITY ") - print(" |----------------|------------|----------------------|----------------------|----------------------|-------------------") + print(" | TYPE | IOA | VALUE | PROCESSED AT | RECORDED AT | QUALITY ") + print(" |----------------|---------|-------------------|---------------|---------------|-------------------") for pt_iter in range(st_pt_count): pt = st.points[pt_iter] - print(" | {0} | {1:10} | {2:20} | {3:20} | {4:20} | {5}".format(pt.type, pt.io_address, pt.value, pt.updated_at_ms, - pt.reported_at_ms, pt.quality)) - print(" |----------------|------------|----------------------|----------------------|----------------------|-------------------") + print(" | {0} | {1:7} | {2:13} | {3:17} | {4:13} | {5}".format(pt.type, pt.io_address, str(pt.value), pt.recorded_at or 'N. A.', + pt.processed_at, pt.quality)) + print(" |----------------|---------|-------------------|---------------|---------------|-------------------") ################################## # SERVER ################################## -my_server = c104.Server(ip="0.0.0.0", port=2404, tick_rate_ms=2000, max_connections=10) +my_server = c104.Server(ip="0.0.0.0", port=2404, tick_rate_ms=100, max_connections=10) my_server.max_connections = 11 sv_station_2 = my_server.add_station(common_address=47) @@ -162,21 +162,18 @@ def sv_on_unexpected_message(server: c104.Server, message: c104.IncomingMessage, # SERVER: MEASUREMENT POINT WITH COMMAND ################################## -def sv_pt_on_setpoint_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - print("SV] {0} SETPOINT COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality)) - - if point.quality.is_good(): - if point.related_io_address: - print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) - related_point = sv_station_2.get_point(point.related_io_address) - if related_point: - print("SV] -> RELATED POINT VALUE UPDATE") - related_point.value = point.value - else: - print("SV] -> RELATED POINT NOT FOUND!") - return c104.ResponseState.SUCCESS - - return c104.ResponseState.FAILURE +def sv_pt_on_setpoint_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + print("SV] {0} SETPOINT COMMAND on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) + + if point.related_io_address: + print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) + related_point = sv_station_2.get_point(point.related_io_address) + if related_point: + print("SV] -> RELATED POINT VALUE UPDATE") + related_point.value = point.value + else: + print("SV] -> RELATED POINT NOT FOUND!") + return c104.ResponseState.SUCCESS # Nan in short measurement value @@ -184,7 +181,7 @@ def sv_pt_on_setpoint_command(point: c104.Point, previous_state: dict, message: sv_nan_point.value = float("NaN") sv_measurement_point = sv_station_2.add_point(io_address=11, type=c104.Type.M_ME_NC_1, report_ms=1000) -sv_measurement_point.value = 12.34 +sv_measurement_point.value = float(12.34) sv_measurement_setpoint = sv_station_2.add_point(io_address=12, type=c104.Type.C_SE_NC_1, report_ms=0, related_io_address=sv_measurement_point.io_address, related_io_autoreturn=True) sv_measurement_setpoint.on_receive(callable=sv_pt_on_setpoint_command) @@ -199,21 +196,18 @@ def sv_pt_on_setpoint_command(point: c104.Point, previous_state: dict, message: # SERVER: DOUBLE POINT WITH COMMAND ################################## -def sv_pt_on_double_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: - print("SV] {0} DOUBLE COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality)) - - if point.quality.is_good(): - if point.related_io_address: - print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) - related_point = sv_station_2.get_point(point.related_io_address) - if related_point: - print("SV] -> RELATED POINT VALUE UPDATE") - related_point.value = point.value - else: - print("SV] -> RELATED POINT NOT FOUND!") - return c104.ResponseState.SUCCESS - - return c104.ResponseState.FAILURE +def sv_pt_on_double_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: + print("SV] {0} DOUBLE COMMAND on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) + + if point.related_io_address: + print("SV] -> RELATED IO ADDRESS: {}".format(point.related_io_address)) + related_point = sv_station_2.get_point(point.related_io_address) + if related_point: + print("SV] -> RELATED POINT VALUE UPDATE") + related_point.value = point.value + else: + print("SV] -> RELATED POINT NOT FOUND!") + return c104.ResponseState.SUCCESS sv_double_point = sv_station_2.add_point(io_address=21, type=c104.Type.M_DP_TB_1) @@ -227,12 +221,12 @@ def sv_pt_on_double_command(point: c104.Point, previous_state: dict, message: c1 # SERVER: STEP POINT WITH COMMAND ################################## -sv_global_step_point_value = 0 +sv_global_step_point_value = c104.Int7(0) -def sv_pt_on_step_command(point: c104.Point, previous_state: dict, message: c104.IncomingMessage) -> c104.ResponseState: +def sv_pt_on_step_command(point: c104.Point, previous_info: c104.Information, message: c104.IncomingMessage) -> c104.ResponseState: global sv_global_step_point_value - print("SV] {0} STEP COMMAND on IOA: {1}, new: {2}, prev: {3}, cot: {4}, quality: {5}".format(point.type, point.io_address, point.value, previous_state, message.cot, point.quality)) + print("SV] {0} STEP COMMAND on IOA: {1}, message: {2}, previous: {3}, current: {4}".format(point.type, point.io_address, message, previous_info, point.info)) if point.value == c104.Step.LOWER: sv_global_step_point_value -= 1 @@ -276,7 +270,7 @@ def sv_pt_on_before_auto_transmit_step_point(point: c104.Point) -> None: my_client.start() my_server.start() -while not cl_connection_1.is_connected: +while not cl_connection_1.is_connected or cl_connection_1.is_muted: print("CL] Try to connect to {0}:{1}".format(cl_connection_1.ip, cl_connection_1.port)) cl_connection_1.connect() time.sleep(3) @@ -303,7 +297,7 @@ def pt_on_before_auto_transmit_measurement_point(self, point: c104.Point) -> Non time.sleep(3) print("-"*60) -sv_measurement_point.value = 1234 +sv_measurement_point.value = float(1234) sv_measurement_point.transmit(cause=c104.Cot.SPONTANEOUS) sv_measurement_point.on_before_auto_transmit(callable=pmct.pt_on_before_auto_transmit_measurement_point) @@ -312,7 +306,7 @@ def pt_on_before_auto_transmit_measurement_point(self, point: c104.Point) -> Non time.sleep(3) print("-"*60) -sv_measurement_point.set(value=-1234.56, quality=c104.Quality.Invalid, timestamp_ms=int(time.time() * 1000)) +sv_measurement_point.info = c104.ShortInfo(actual=-1234.56, quality=c104.Quality.Invalid, recorded_at=datetime.datetime.fromtimestamp(time.time())) sv_measurement_point.transmit(cause=c104.Cot.SPONTANEOUS) time.sleep(3) diff --git a/tests/test_object_datapoint.cpp b/tests/test_object_datapoint.cpp index ffa5944..c11f5a7 100644 --- a/tests/test_object_datapoint.cpp +++ b/tests/test_object_datapoint.cpp @@ -24,48 +24,52 @@ #include +#include "Server.h" #include "object/DataPoint.h" #include "object/Station.h" #include "remote/message/IncomingMessage.h" #include "types.h" TEST_CASE("Create point", "[object::point]") { - auto point = Object::DataPoint::create(11, IEC60870_5_TypeID::M_SP_NA_1, - nullptr, 0, 0, false); - REQUIRE(point->getStation().get() == nullptr); + auto server = Server::create(); + auto station = server->addStation(10); + auto point = station->addPoint(11, IEC60870_5_TypeID::M_SP_NA_1); + REQUIRE(point->getStation().get() == station.get()); REQUIRE(point->getInformationObjectAddress() == 11); REQUIRE(point->getRelatedInformationObjectAddress() == 0); REQUIRE(point->getRelatedInformationObjectAutoReturn() == false); REQUIRE(point->getType() == IEC60870_5_TypeID::M_SP_NA_1); REQUIRE(point->getReportInterval_ms() == 0); - REQUIRE(point->getQuality() == Quality::None); - REQUIRE(point->getValue() == 0); - REQUIRE(point->getValueAsInt32() == 0); - REQUIRE(point->getValueAsUInt32() == 0); - REQUIRE(point->getValueAsFloat() == 0); - REQUIRE(point->getUpdatedAt_ms() == 0); - REQUIRE(point->getReportedAt_ms() == 0); - REQUIRE(point->getReceivedAt_ms() == 0); - REQUIRE(point->getSentAt_ms() == 0); + REQUIRE(std::get(point->getInfo()->getQuality()) == Quality::None); + REQUIRE(std::get(point->getQuality()) == Quality::None); + REQUIRE(std::get(point->getInfo()->getValue()) == false); + REQUIRE(std::get(point->getValue()) == false); + REQUIRE(point->getProcessedAt() > + std::chrono::system_clock::time_point::min()); + REQUIRE(point->getRecordedAt().has_value() == false); } TEST_CASE("Set point value", "[object::point]") { - auto point = Object::DataPoint::create(11, IEC60870_5_TypeID::M_SP_NA_1, - nullptr, 0, 0, false); + auto server = Server::create(); + auto station = server->addStation(10); + auto point = station->addPoint(11, IEC60870_5_TypeID::M_ME_TE_1); - point->setValueEx(56.78, Quality::None, 1234567890); - REQUIRE(point->getValue() == 56.78); - REQUIRE(point->getValueAsInt32() == 56); - REQUIRE(point->getValueAsUInt32() == 56); - REQUIRE(point->getValueAsFloat() == (float)56.78); - REQUIRE(point->getUpdatedAt_ms() == 1234567890); - // SinglePoint value must be of [0, 1] - REQUIRE(point->getQuality() == Quality::Invalid); + point->setInfo( + Object::ScaledInfo::create(LimitedInt16(334), Quality::Invalid, + std::chrono::system_clock::time_point( + std::chrono::milliseconds(1234567890)))); + REQUIRE(std::get(point->getValue()).get() == + LimitedInt16(334).get()); + REQUIRE(point->getRecordedAt().value() == + std::chrono::system_clock::time_point( + std::chrono::milliseconds(1234567890))); + REQUIRE(std::get(point->getQuality()) == Quality::Invalid); } TEST_CASE("Set point value via message", "[object::point]") { - auto point = Object::DataPoint::create(11, IEC60870_5_TypeID::C_DC_TA_1, - nullptr, 0, 0, false); + auto server = Server::create(); + auto station = server->addStation(10); + auto point = station->addPoint(11, IEC60870_5_TypeID::C_DC_TA_1); sCS101_AppLayerParameters appLayerParameters{.sizeOfTypeId = 1, .sizeOfVSQ = 0, @@ -85,10 +89,12 @@ TEST_CASE("Set point value via message", "[object::point]") { Remote::Message::IncomingMessage::create(asdu, &appLayerParameters); point->onReceive(message); - REQUIRE(point->getValue() == 1); - REQUIRE(point->getUpdatedAt_ms() == 1680517666000); - // SinglePoint value must be of [0, 1] - REQUIRE(point->getQuality() == Quality::None); + REQUIRE(std::get(point->getValue()) == + IEC60870_DOUBLE_POINT_OFF); + REQUIRE(point->getRecordedAt() == + std::chrono::system_clock::time_point( + std::chrono::milliseconds(1680517666000))); + REQUIRE(std::get(point->getQuality()) == std::monostate{}); InformationObject_destroy(io); CS101_ASDU_destroy(asdu);