Skip to content

Commit

Permalink
Release 2.0.0 (#26)
Browse files Browse the repository at this point in the history
* ✨ alpha 2.0

* ✨ add custom timer callback

* 🐛 fix read handler, add changelog draft, minor improvements

* 🎨 make init more robust, improve performance, add selection_timeout parameter to server, remove deprecation

* 🐛 fix test scripts

* ⚰️ cleanup interfaces

* ⚰️ improve datetime compatibility, fix test

* ⚰️ improve test

* 🐛 fix read command and incoming message order

* 🐛 fix IC, CS command success

* 🎨 use chrono for time information, update pybind11 to v2.13.4

* 🏷️ undo utc_clock changes and return to system_clock for pybind11 compatibility

* 🐛 fix tls, fix tests, add connected_at and disconnected_at to Connection

* 🐛 fix clock sync, improve to string conversion, fix tests

* 🐛 fix report interval setter on point construction, fix clock in tests

* ⬇️ return to c++ standard 17 for manylinux compatibility

* ⬆️ update dependencies lib60870 to latest, pybind11 to v2.13.5, catch to v3.7.0

* 🐛 fix manual reconnect deadlock, improve debug output, add tests

* 🎨 improve tests

* 🐛 fix select detection in explain_bytes_dict

* ⚡ allow different parallel commands

* update version info

* 🐛 fix fieldset16 causing to string crash, fix pybind using correct python version

* 📝 update documentation and changelog, update docs builder dependencies

* 🎨 c104.Byte32 can construct from bytes and converted to bytes, c104 numbers can be converted to int and float, fix __repr__ toString behavior

* 🐛 fix message __repr__ toString

* 🎨 remove python 3.6 from build script; add build script for aarch64, armv7l; fix cmake version for arm

* 📝 improve number and information documentation

* 🐛 fix numbers, fix tests, add lib60870 patch, improve docs

* 🚑 add lib60870 patch

* 🐛 fix build scripts

* 📝 improve type hints in documentation, improve source distribution
  • Loading branch information
m-unkel authored Sep 3, 2024
1 parent 6f4dd58 commit aee6a7a
Show file tree
Hide file tree
Showing 145 changed files with 7,958 additions and 2,759 deletions.
140 changes: 139 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
161 changes: 109 additions & 52 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
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()

# ##############################################################################
# pybind11 and Python3
# ##############################################################################

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()

# ##############################################################################
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
4 changes: 3 additions & 1 deletion Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit aee6a7a

Please sign in to comment.