diff --git a/.idea/misc.xml b/.idea/misc.xml index ada67d1..b35f69e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,9 +1,6 @@ - diff --git a/.idea/pyLspTools.xml b/.idea/pyLspTools.xml new file mode 100644 index 0000000..400aa9d --- /dev/null +++ b/.idea/pyLspTools.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/src/revpimodio2/__about__.py b/src/revpimodio2/__about__.py index cc03ee0..01a540c 100644 --- a/src/revpimodio2/__about__.py +++ b/src/revpimodio2/__about__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Metadata of package.""" + __author__ = "Sven Sager " __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" -__version__ = "2.8.1" +__version__ = "2.9.0" diff --git a/src/revpimodio2/__init__.py b/src/revpimodio2/__init__.py index 656c4a4..ff6f8fe 100644 --- a/src/revpimodio2/__init__.py +++ b/src/revpimodio2/__init__.py @@ -12,6 +12,7 @@ With the defined names, the desired data is accessed directly. The user can register functions as events for all IOs. The module executes these when data changes. """ + __all__ = [ "IOEvent", "RevPiModIO", diff --git a/src/revpimodio2/_internal.py b/src/revpimodio2/_internal.py index afb2b82..d14ddc3 100644 --- a/src/revpimodio2/_internal.py +++ b/src/revpimodio2/_internal.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Internal functions and values for this package.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" diff --git a/src/revpimodio2/app.py b/src/revpimodio2/app.py index 446e9c7..b7234ed 100644 --- a/src/revpimodio2/app.py +++ b/src/revpimodio2/app.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Maps the App section from piCtory.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" diff --git a/src/revpimodio2/device.py b/src/revpimodio2/device.py index 7f24cbc..15d34e7 100644 --- a/src/revpimodio2/device.py +++ b/src/revpimodio2/device.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Module for managing devices.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -354,8 +355,10 @@ class Device(object): elif bool(dict_io[key][7]): # Use IOBase for bit values io_new = IOBase(self, dict_io[key], iotype, "little", False) - elif isinstance(self, DioModule) and dict_io[key][3] in self._lst_counter: - # Counter IO on a DI or DIO + elif (isinstance(self, DioModule) or isinstance(self, MioModule)) and dict_io[key][ + 3 + ] in self._lst_counter: + # Counter IO on a DI, DIO or MIO module io_new = IntIOCounter( self._lst_counter.index(dict_io[key][3]), self, @@ -2030,6 +2033,23 @@ class DioModule(Device): super().__init__(parentmodio, dict_device, simulator=simulator) +class MioModule(Device): + """Represents a MIO module.""" + + __slots__ = "_lst_counter" + + def __init__(self, parentmodio, dict_device, simulator=False): + """ + Extended device class to recognize IntIOCounter. + + :rev: :func:`Device.__init__()` + """ + # String list of byte addresses that are a counter. + self._lst_counter = list(map(str, range(9, 17, 2))) + + super().__init__(parentmodio, dict_device, simulator=simulator) + + class RoModule(Device): """Relais output (RO) module with""" diff --git a/src/revpimodio2/errors.py b/src/revpimodio2/errors.py index 121ab3e..da225a3 100644 --- a/src/revpimodio2/errors.py +++ b/src/revpimodio2/errors.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Error classes of RevPiModIO.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" diff --git a/src/revpimodio2/helper.py b/src/revpimodio2/helper.py index ba415a5..9f5a1e7 100644 --- a/src/revpimodio2/helper.py +++ b/src/revpimodio2/helper.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """RevPiModIO helper classes and tools.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" diff --git a/src/revpimodio2/io.py b/src/revpimodio2/io.py index 130e9f6..e153233 100644 --- a/src/revpimodio2/io.py +++ b/src/revpimodio2/io.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """RevPiModIO module for managing IOs.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -1130,8 +1131,12 @@ class IntIOCounter(IntIO): # Load base class super().__init__(parentdevice, valuelist, iotype, byteorder, signed) - def reset(self) -> None: - """Resets the counter of the input.""" + def reset(self) -> bool: + """ + Resets the counter of the input. + + :return: True if reset was successful, False otherwise + """ if self._parentdevice._modio._monitoring: raise RuntimeError("can not reset counter, while system is in monitoring mode") if self._parentdevice._modio._simulator: @@ -1145,6 +1150,7 @@ class IntIOCounter(IntIO): ioctl(self._parentdevice._modio._myfh, 19220, self.__ioctl_arg) except Exception as e: self._parentdevice._modio._gotioerror("iorst", e) + return False elif hasattr(self._parentdevice._modio._myfh, "ioctl"): # IOCTL over network @@ -1153,6 +1159,7 @@ class IntIOCounter(IntIO): self._parentdevice._modio._myfh.ioctl(19220, self.__ioctl_arg) except Exception as e: self._parentdevice._modio._gotioerror("net_iorst", e) + return False else: # Simulate IOCTL in file @@ -1161,6 +1168,9 @@ class IntIOCounter(IntIO): self._parentdevice._modio._simulate_ioctl(19220, self.__ioctl_arg) except Exception as e: self._parentdevice._modio._gotioerror("file_iorst", e) + return False + + return True class IntIOReplaceable(IntIO): @@ -1275,7 +1285,8 @@ class RelaisOutput(IOBase): This function is only available locally on a Revolution Pi. This function cannot be used via RevPiNetIO. - :return: Integer of switching cycles as single value or tuple of all + :return: Integer of switching cycles as single value or tuple of all. The + value -1 indicates an error. """ # Using ioctl request K+29 = 19229 if self._parentdevice._modio._run_on_pi: @@ -1288,10 +1299,14 @@ class RelaisOutput(IOBase): self.__ioctl_arg, ) except Exception as e: - # If not implemented, we return the max value and set an error - ioctl_return_value = b"\xff" * struct.calcsize(self.__ioctl_arg_format) self._parentdevice._modio._gotioerror("rocounter", e) + # To report an error, return -1 values + if self._bitaddress == -1: + return -1, -1, -1, -1 + else: + return -1 + elif hasattr(self._parentdevice._modio._myfh, "ioctl"): # IOCTL over network """ diff --git a/src/revpimodio2/modio.py b/src/revpimodio2/modio.py index ff7fa47..0683759 100644 --- a/src/revpimodio2/modio.py +++ b/src/revpimodio2/modio.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """RevPiModIO main class for piControl0 access.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -403,6 +404,9 @@ class RevPiModIO(object): elif pt == ProductType.RO: # RO dev_new = devicemodule.RoModule(self, device, simulator=self._simulator) + elif pt == ProductType.MIO: + # MIO + dev_new = devicemodule.MioModule(self, device, simulator=self._simulator) else: # All other IO devices dev_new = devicemodule.Device(self, device, simulator=self._simulator) @@ -1530,7 +1534,15 @@ class RevPiModIODriver(RevPiModIOSelected): ) -def run_plc(func, cycletime=50, replace_io_file=None, debug=True, procimg=None, configrsc=None): +def run_plc( + func, + cycletime=50, + replace_io_file=None, + debug=True, + procimg=None, + configrsc=None, + shared_procimg=False, +): """ Run Revoluton Pi as real plc with cycle loop and exclusive IO access. @@ -1557,6 +1569,7 @@ def run_plc(func, cycletime=50, replace_io_file=None, debug=True, procimg=None, debug=debug, procimg=procimg, configrsc=configrsc, + shared_procimg=shared_procimg, ) rpi.handlesignalend() return rpi.cycleloop(func, cycletime) diff --git a/src/revpimodio2/netio.py b/src/revpimodio2/netio.py index 0d56db9..422d8c5 100644 --- a/src/revpimodio2/netio.py +++ b/src/revpimodio2/netio.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """RevPiModIO main class for network access.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" diff --git a/src/revpimodio2/pictory.py b/src/revpimodio2/pictory.py index b8d1839..e746753 100644 --- a/src/revpimodio2/pictory.py +++ b/src/revpimodio2/pictory.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Pictory aliases for IO values.""" + __author__ = "Théo Rozier" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" diff --git a/src/revpimodio2/summary.py b/src/revpimodio2/summary.py index 51e34da..ad8c1dd 100644 --- a/src/revpimodio2/summary.py +++ b/src/revpimodio2/summary.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Maps the Summary section from piCtory.""" + __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" diff --git a/tests/io_tests/config_ro_multi.rsc b/tests/io_tests/config_ro_multi.rsc new file mode 100644 index 0000000..d58b8bc --- /dev/null +++ b/tests/io_tests/config_ro_multi.rsc @@ -0,0 +1,309 @@ +{ + "App": { + "name": "PiCtory", + "version": "2.17.3", + "saveTS": "20260901080641", + "language": "en", + "layout": { + "north": { + "size": 70, + "initClosed": false, + "initHidden": false + }, + "south": { + "size": 353, + "initClosed": false, + "initHidden": false, + "children": { + "layout1": { + "east": { + "size": 500, + "initClosed": false, + "initHidden": false + } + } + } + }, + "east": { + "size": 70, + "initClosed": true, + "initHidden": false, + "children": {} + }, + "west": { + "size": 340, + "initClosed": false, + "initHidden": false, + "children": { + "layout1": {} + } + } + } + }, + "Summary": { + "inpTotal": 7, + "outTotal": 8 + }, + "Devices": [ + { + "GUID": "fa3b69a8-77f1-7f15-d9f0-339646253ace", + "id": "device_RevPiConnect5_20240315_1_0_001", + "type": "BASE", + "productType": "138", + "position": "0", + "name": "RevPi Connect 5", + "bmk": "RevPi Connect 5", + "inpVariant": 0, + "outVariant": 0, + "comment": "This is a RevPi Connect 5 Device", + "offset": 0, + "inp": { + "0": [ + "RevPiStatus", + "0", + "8", + "0", + true, + "0000", + "", + "" + ], + "1": [ + "RevPiIOCycle", + "0", + "8", + "1", + true, + "0001", + "", + "" + ], + "2": [ + "RS485ErrorCnt", + "0", + "16", + "2", + false, + "0002", + "", + "" + ], + "3": [ + "Core_Temperature", + "0", + "8", + "4", + false, + "0003", + "", + "" + ], + "4": [ + "Core_Frequency", + "0", + "8", + "5", + false, + "0004", + "", + "" + ] + }, + "out": { + "0": [ + "RevPiReservedByte", + "0", + "8", + "6", + false, + "0005", + "", + "" + ], + "1": [ + "RS485ErrorLimit1", + "10", + "16", + "7", + false, + "0006", + "", + "" + ], + "2": [ + "RS485ErrorLimit2", + "1000", + "16", + "9", + false, + "0007", + "", + "" + ], + "3": [ + "RevPiLED", + "0", + "16", + "11", + true, + "0008", + "", + "" + ] + }, + "mem": {}, + "extend": {} + }, + { + "GUID": "53c792e0-ec3d-8047-bb0c-1afa6772873f", + "id": "device_RevPiRO_20231018_1_0_001", + "type": "LEFT_RIGHT", + "productType": "137", + "position": "32", + "name": "RevPi RO", + "bmk": "RevPi RO", + "inpVariant": 0, + "outVariant": 0, + "comment": "", + "offset": 13, + "inp": { + "0": [ + "Status", + "0", + "8", + "0", + false, + "0000", + "", + "" + ] + }, + "out": { + "0": [ + "RelayOutput_1", + "0", + "1", + "1", + true, + "0001", + "", + "0" + ], + "1": [ + "RelayOutput_2", + "0", + "1", + "1", + true, + "0002", + "", + "1" + ], + "2": [ + "RelayOutput_3", + "0", + "1", + "1", + true, + "0003", + "", + "2" + ], + "3": [ + "RelayOutput_4", + "0", + "1", + "1", + true, + "0004", + "", + "3" + ], + "4": [ + "RelayOutputPadding_5", + "0", + "1", + "1", + true, + "0005", + "", + "4" + ], + "5": [ + "RelayOutputPadding_6", + "0", + "1", + "1", + true, + "0006", + "", + "5" + ], + "6": [ + "RelayOutputPadding_7", + "0", + "1", + "1", + true, + "0007", + "", + "6" + ], + "7": [ + "RelayOutputPadding_8", + "0", + "1", + "1", + true, + "0008", + "", + "7" + ] + }, + "mem": { + "0": [ + "RelayCycleWarningThreshold_1", + "0", + "32", + "2", + false, + "0009", + "", + "" + ], + "1": [ + "RelayCycleWarningThreshold_2", + "0", + "32", + "6", + false, + "0010", + "", + "" + ], + "2": [ + "RelayCycleWarningThreshold_3", + "0", + "32", + "10", + false, + "0011", + "", + "" + ], + "3": [ + "RelayCycleWarningThreshold_4", + "0", + "32", + "14", + false, + "0012", + "", + "" + ] + }, + "extend": {} + } + ], + "Connections": [] +} diff --git a/tests/io_tests/config_ro_single.rsc b/tests/io_tests/config_ro_single.rsc new file mode 100644 index 0000000..a899aca --- /dev/null +++ b/tests/io_tests/config_ro_single.rsc @@ -0,0 +1,239 @@ +{ + "App": { + "name": "PiCtory", + "version": "2.17.3", + "saveTS": "20260901080758", + "language": "en", + "layout": { + "north": { + "size": 70, + "initClosed": false, + "initHidden": false + }, + "south": { + "size": 353, + "initClosed": false, + "initHidden": false, + "children": { + "layout1": { + "east": { + "size": 500, + "initClosed": false, + "initHidden": false + } + } + } + }, + "east": { + "size": 70, + "initClosed": true, + "initHidden": false, + "children": {} + }, + "west": { + "size": 340, + "initClosed": false, + "initHidden": false, + "children": { + "layout1": {} + } + } + } + }, + "Summary": { + "inpTotal": 7, + "outTotal": 8 + }, + "Devices": [ + { + "GUID": "fa3b69a8-77f1-7f15-d9f0-339646253ace", + "id": "device_RevPiConnect5_20240315_1_0_001", + "type": "BASE", + "productType": "138", + "position": "0", + "name": "RevPi Connect 5", + "bmk": "RevPi Connect 5", + "inpVariant": 0, + "outVariant": 0, + "comment": "This is a RevPi Connect 5 Device", + "offset": 0, + "inp": { + "0": [ + "RevPiStatus", + "0", + "8", + "0", + true, + "0000", + "", + "" + ], + "1": [ + "RevPiIOCycle", + "0", + "8", + "1", + true, + "0001", + "", + "" + ], + "2": [ + "RS485ErrorCnt", + "0", + "16", + "2", + false, + "0002", + "", + "" + ], + "3": [ + "Core_Temperature", + "0", + "8", + "4", + false, + "0003", + "", + "" + ], + "4": [ + "Core_Frequency", + "0", + "8", + "5", + false, + "0004", + "", + "" + ] + }, + "out": { + "0": [ + "RevPiReservedByte", + "0", + "8", + "6", + false, + "0005", + "", + "" + ], + "1": [ + "RS485ErrorLimit1", + "10", + "16", + "7", + false, + "0006", + "", + "" + ], + "2": [ + "RS485ErrorLimit2", + "1000", + "16", + "9", + false, + "0007", + "", + "" + ], + "3": [ + "RevPiLED", + "0", + "16", + "11", + true, + "0008", + "", + "" + ] + }, + "mem": {}, + "extend": {} + }, + { + "GUID": "53c792e0-ec3d-8047-bb0c-1afa6772873f", + "id": "device_RevPiRO_20231018_1_0_001", + "type": "LEFT_RIGHT", + "productType": "137", + "position": "32", + "name": "RevPi RO", + "bmk": "RevPi RO", + "inpVariant": 0, + "outVariant": 1, + "comment": "", + "offset": 13, + "inp": { + "0": [ + "Status_i02", + "0", + "8", + "0", + false, + "0000", + "", + "" + ] + }, + "out": { + "0": [ + "RelayOutputs", + "0", + "8", + "1", + true, + "0001", + "", + "" + ] + }, + "mem": { + "0": [ + "RelayCycleWarningThreshold_1_i02", + "0", + "32", + "2", + false, + "0002", + "", + "" + ], + "1": [ + "RelayCycleWarningThreshold_2_i02", + "0", + "32", + "6", + false, + "0003", + "", + "" + ], + "2": [ + "RelayCycleWarningThreshold_3_i02", + "0", + "32", + "10", + false, + "0004", + "", + "" + ], + "3": [ + "RelayCycleWarningThreshold_4_i02", + "0", + "32", + "14", + false, + "0005", + "", + "" + ] + }, + "extend": {} + } + ], + "Connections": [] +} diff --git a/tests/io_tests/test_ios.py b/tests/io_tests/test_ios.py index c8488b1..edd36fb 100644 --- a/tests/io_tests/test_ios.py +++ b/tests/io_tests/test_ios.py @@ -105,3 +105,30 @@ class TestIos(TestRevPiModIO): self.assertEqual(rpi.device[65]._ba_devdata[32:38], b"\x20\x00\x00\x00\x00\x00") rpi.io.OutBit_48.value = True self.assertEqual(rpi.device[65]._ba_devdata[32:38], b"\x20\x00\x00\x00\x00\x80") + + def test_get_switching_cycles(self): + rpi = self.modio(configrsc="config_ro_multi.rsc") + self.assertEqual(rpi.io.RelayOutput_1.get_switching_cycles(), 0) + self.assertEqual(rpi.io.RelayOutput_2.get_switching_cycles(), 0) + self.assertEqual(rpi.io.RelayOutput_3.get_switching_cycles(), 0) + self.assertEqual(rpi.io.RelayOutput_4.get_switching_cycles(), 0) + + # This will use ioctl calls + rpi._run_on_pi = True + + with self.assertWarnsRegex(RuntimeWarning, r"'rocounter' and count \d"): + self.assertEqual(rpi.io.RelayOutput_1.get_switching_cycles(), -1) + self.assertEqual(rpi.io.RelayOutput_2.get_switching_cycles(), -1) + self.assertEqual(rpi.io.RelayOutput_3.get_switching_cycles(), -1) + self.assertEqual(rpi.io.RelayOutput_4.get_switching_cycles(), -1) + self.assertEqual(rpi.ioerrors, 4) + + rpi = self.modio(configrsc="config_ro_single.rsc") + self.assertEqual(rpi.io.RelayOutputs.get_switching_cycles(), (0, 0, 0, 0)) + + # This will use ioctl calls + rpi._run_on_pi = True + + with self.assertWarnsRegex(RuntimeWarning, r"'rocounter' and count \d"): + self.assertEqual(rpi.io.RelayOutputs.get_switching_cycles(), (-1, -1, -1, -1)) + self.assertEqual(rpi.ioerrors, 1)