From 50ce887aa9363f02d6f1f41f9927c63077b5df8a Mon Sep 17 00:00:00 2001 From: Avaneesh Tripathi Date: Tue, 3 Jan 2023 10:41:32 +0530 Subject: [PATCH 1/3] Skip scan delay in case of exception When adding large list of tags, few invalid tags cause exception and delays the loop. Hence in case of any exception no sleep is performed. tested in production --- pyctapi/connection.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyctapi/connection.py b/pyctapi/connection.py index 4c7f63f..26d266e 100755 --- a/pyctapi/connection.py +++ b/pyctapi/connection.py @@ -108,8 +108,8 @@ def _read_lists(self): # Reading entire tag list while self._ok_to_run: - - try: + skip_scan_delay = False + try: # Update internal tags lists self._update_tag_lists() @@ -122,6 +122,7 @@ def _read_lists(self): except CTAPITagDoesNotExist as e: print(self.host(), "Tag does not exist", e) + skip_scan_delay = True except CTAPIGeneralError as e: if e.error_code == 233: @@ -138,10 +139,11 @@ def _read_lists(self): print(self.host(), "error", pyctapi.CT_TO_WIN32_ERROR(e.error_code)) print(self.host(), "error", pyctapi.WIN32_TO_CT_ERROR(e.error_code)) break + skip_scan_delay = True + + if skip_scan_delay == False: + sleep(self._scan_rate) - - sleep(self._scan_rate) - if self._poll_lock != None and self.lock_status == True: self._poll_lock.release() self.lock_status = False From 7679ae1a10da7bd783dd099b9e4d154757cdbf08 Mon Sep 17 00:00:00 2001 From: Avaneesh Tripathi Date: Tue, 3 Jan 2023 10:56:28 +0530 Subject: [PATCH 2/3] Changed add_tag_to_list() in adapter.py to add extra polling parameters add_tag_to_list() function in `adapter.py` is changed to allow poll_period, deadband and raw_mode parameters to be passed to CTAPI. `pyctapi.py` and `connection.py` changed accordingly --- pyctapi/adapter.py | 4 ++-- pyctapi/connection.py | 13 +++++++------ pyctapi/pyctapi.py | 4 ++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pyctapi/adapter.py b/pyctapi/adapter.py index 388e1d1..01fae7f 100644 --- a/pyctapi/adapter.py +++ b/pyctapi/adapter.py @@ -108,8 +108,8 @@ def create_tag_list(self, list_name, mode=0): raise CTAPIGeneralError(self._ctapi.getErrorCode()) - def add_tag_to_list(self, list_name, tag_name): - tag_handle = self._ctapi.ctListAdd(self._tag_lists[list_name], tag_name) + def add_tag_to_list(self, list_name, tag_name, raw_mode=False, poll_period_ms=300,deadband_percent=-1.0): + tag_handle = self._ctapi.ctListAddEx(self._tag_lists[list_name], tag_name, raw_mode, poll_period_ms, deadband_percent) if tag_handle != None: self._tag_handles[tag_name] = tag_handle return tag_handle diff --git a/pyctapi/connection.py b/pyctapi/connection.py index 26d266e..403637a 100755 --- a/pyctapi/connection.py +++ b/pyctapi/connection.py @@ -62,8 +62,8 @@ def __init__(self, connection_params, dll_path, scan_rate=0.1, poll_lock=None): def add_list(self, list_name): self.tag_lists_changed.add(list_name) - def add_tag(self, list_name, tag_name): - self.tags_changed.add((list_name, tag_name,)) + def add_tag(self, list_name, tag_name, raw_mode=False, poll_period_ms=300,deadband_percent=-1.0): + self.tags_changed.add((list_name, tag_name,raw_mode, poll_period_ms,deadband_percent)) def subscribe(self, list_name, callback): self.subscribers.add((list_name, callback)) @@ -144,6 +144,7 @@ def _read_lists(self): if skip_scan_delay == False: sleep(self._scan_rate) + if self._poll_lock != None and self.lock_status == True: self._poll_lock.release() self.lock_status = False @@ -154,9 +155,9 @@ def _init_tag_lists(self): print(self.host(), "Created tag list %s" % list_name) self._ctapi.create_tag_list(list_name, pyctapi.CT_LIST_EVENT + pyctapi.CT_LIST_LIGHTWEIGHT_MODE) - for list_name, tag_name in self.tags: + for list_name, tag_name, raw_mode, poll_period_ms, deadband_percent in self.tags: #print(self.host(), "Created tag %s -> %s" % (list_name, tag_name)) - self._ctapi.add_tag_to_list(list_name, tag_name) + self._ctapi.add_tag_to_list(list_name, tag_name, raw_mode, poll_period_ms, deadband_percent) def _update_tag_lists(self): for list_name in self.tag_lists_changed - self.tag_lists: @@ -164,9 +165,9 @@ def _update_tag_lists(self): self._ctapi.create_tag_list(list_name, pyctapi.CT_LIST_EVENT + pyctapi.CT_LIST_LIGHTWEIGHT_MODE) self.tag_lists |= self.tag_lists_changed - for list_name, tag_name in self.tags_changed - self.tags: + for list_name, tag_name, raw_mode, poll_period_ms, deadband_percent in self.tags_changed - self.tags: #print(self.host(), "Added tag %s -> %s" % (list_name, tag_name)) - self._ctapi.add_tag_to_list(list_name, tag_name) + self._ctapi.add_tag_to_list(list_name, tag_name, raw_mode, poll_period_ms, deadband_percent) self.tags |= self.tags_changed def _increase_backoff_time(self): diff --git a/pyctapi/pyctapi.py b/pyctapi/pyctapi.py index 5a0264c..4790b99 100644 --- a/pyctapi/pyctapi.py +++ b/pyctapi/pyctapi.py @@ -119,6 +119,10 @@ def ctListFree(self, _list): def ctListAdd(self, _list, tag_name): return windll.CtApi.ctListAdd(_list, tag_name.encode("ascii")) + + def ctListAddEx(self, _list, tag_name, raw_mode=False, poll_period_ms=300,deadband_percent=-1.0 ): + db = ctypes.c_double(deadband_percent) + return windll.CtApi.ctListAddEx(_list, tag_name.encode("ascii"), raw_mode, poll_period_ms,db ) def ctListDelete(self, tag_handle): windll.CtApi.ctListDelete(tag_handle) From e47c1165de87d47db3ae8e7ffbad5678f0a46f23 Mon Sep 17 00:00:00 2001 From: Avaneesh Tripathi Date: Tue, 3 Jan 2023 11:31:09 +0530 Subject: [PATCH 3/3] Added CTAPI search functionality , using 'ctFindFirst()' family of functions Implemented search function in `adapter.py` which can be used to query citect for various functionality i.e. Trends, Alarms summary etc --- examples/test_search.py | 19 +++++++++++++ pyctapi/adapter.py | 42 +++++++++++++++++++++++++++++ pyctapi/pyctapi.py | 60 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 examples/test_search.py diff --git a/examples/test_search.py b/examples/test_search.py new file mode 100644 index 0000000..bcde012 --- /dev/null +++ b/examples/test_search.py @@ -0,0 +1,19 @@ +#! /usr/bin/env python + +import sys +sys.path.append("../") + +from pyctapi import adapter + +IP_ADDRESS = "127.0.0.1" +CITECT_USERNAME = "engineer" +CITECT_PASSWORD = "control" +SOME_CITECT_TAG_NAME = "KNODLRS_PM10_CALC_24H" +TIMESTAMP_UTC_IN_SECONDS = "1672725468" + +# Test search using TRNQUERY of CTAPI, fetch trend values +# trend tag read, Maximum 300 values can be queried at a time. See doc at https://johnwiltshire.com/citect-help/Content/trnQuery.html +with adapter.CTAPIAdapter(IP_ADDRESS, CITECT_USERNAME, CITECT_PASSWORD) as ct: + resultData = ct.search('TRNQUERY,{},0,1.00,3000,{},4194304,1,0,250'.format("TIMESTAMP_UTC_IN_SECONDS",SOME_CITECT_TAG_NAME)) + + diff --git a/pyctapi/adapter.py b/pyctapi/adapter.py index 01fae7f..1d62b66 100644 --- a/pyctapi/adapter.py +++ b/pyctapi/adapter.py @@ -7,6 +7,7 @@ # from ctypes import create_string_buffer +from ctypes.wintypes import HANDLE from pyctapi import pyctapi @@ -41,6 +42,8 @@ def __init__(self, citect_host, citect_username, citect_password, mode=pyctapi.C self._ctapi = pyctapi.CTAPIWrapper(dll_path) self._tag_lists = {} self._tag_handles = {} + self._search_handle = None + self._search_obj_handle = HANDLE() def __enter__(self): self.connect() @@ -159,3 +162,42 @@ def write_tag_list(self, tag_name, value): raise CTAPIGeneralError(self._ctapi.getErrorCode()) return status_code + #Attaching this callable to restype of DLL function + def _return_error_check(self, value): + if value : # Null or None has zeo boolean value + if value == 0: # CTAPI functions return '0' on error + raise CTAPIGeneralError(self._ctapi.getErrorCode()) + return value + raise CTAPIGeneralError(self._ctapi.getErrorCode())# CTAPI functions return NULL on error + + def search(self, search_str): + ''' Form search functionality as implemented by ctFindFirst family''' + try: + self._search_handle = self._return_error_check(self._ctapi.ctFindFirst(self._connection, search_str, self._search_obj_handle )) + # Get Metadata of result + value_buffer = create_string_buffer(b'0' * 50) + self._ctapi.ctGetProperty(self._search_obj_handle, 'object.fields.count',value_buffer) + fields_count = int(value_buffer.value) + fields=[] + for i in range(1,fields_count+1,1): + self._ctapi.ctGetProperty(self._search_obj_handle, 'object.fields({}).name'.format(i),value_buffer) + fields.append(value_buffer.value.decode("utf-8")) + + data=[] + while True: + data_item = dict() + for field in fields: + self._ctapi.ctGetProperty(self._search_obj_handle, field, value_buffer) + data_item[str(field)] = value_buffer.value.decode("utf-8") + data.append(data_item) + if self._ctapi.ctFindNext(self._search_handle, self._search_obj_handle) == 0: + break + return data + + except: + error = self._ctapi.getErrorCode() + print("Error in search {}".format(error)) + finally: + self._ctapi.ctFindClose(self._search_handle) + self._search_handle = None + diff --git a/pyctapi/pyctapi.py b/pyctapi/pyctapi.py index 4790b99..09f82c0 100644 --- a/pyctapi/pyctapi.py +++ b/pyctapi/pyctapi.py @@ -24,8 +24,10 @@ raise OSError from datetime import datetime -from ctypes import CDLL, windll, create_string_buffer, byref, sizeof, GetLastError +import ctypes +from ctypes import CDLL, windll, create_string_buffer, byref, sizeof, pointer, GetLastError from ast import literal_eval +import struct ERROR_USER_DEFINED_BASE = 0x10000000 @@ -78,6 +80,41 @@ def IsCitectError(dwStatus): return (ERROR_USER_DEFINED_BASE < dwStatus) PROPERTY_NAME_LEN = 256 +DBTYPE_EMPTY = 0 +DBTYPE_NULL = 1 +DBTYPE_I2 = 2 +DBTYPE_I4 = 3 +DBTYPE_R4 = 4 +DBTYPE_R8 = 5 +DBTYPE_CY = 6 +DBTYPE_DATE = 7 +DBTYPE_BSTR = 8 +DBTYPE_IDISPATCH = 9 +DBTYPE_ERROR = 10 +DBTYPE_BOOL = 11 +DBTYPE_VARIANT = 12 +DBTYPE_IUNKNOWN = 13 +DBTYPE_DECIMAL = 14 +DBTYPE_UI1 = 17 +DBTYPE_ARRAY = 0x2000 +DBTYPE_BYREF = 0x4000 +DBTYPE_I1 = 16 +DBTYPE_UI2 = 18 +DBTYPE_UI4 = 19 +DBTYPE_I8 = 20 +DBTYPE_UI8 = 21 +DBTYPE_GUID = 72 +DBTYPE_VECTOR = 0x1000 +DBTYPE_RESERVED = 0x8000 +DBTYPE_BYTES = 128 +DBTYPE_STR = 129 +DBTYPE_WSTR = 130 +DBTYPE_NUMERIC = 131 +DBTYPE_UDT = 132 +DBTYPE_DBDATE = 133 +DBTYPE_DBTIME = 134 +DBTYPE_DBTIMESTAMP = 135 + COMMON_WIN32_ERRORS = { "21" : "ERROR_INVALID_ACCESS", # Tag doesnt exist?? "111" : "ERROR_BUFFER_OVERFLOW", # Result buffer not big enough", @@ -135,10 +172,31 @@ def ctListWrite(self, tag_handle, value, overlapped=None): def ctListData(self, tag_handle, buff): return windll.CtApi.ctListData(tag_handle, byref(buff), sizeof(buff), 0) + + def ctListItem(self, tag_handle, buff, item_code=CT_LIST_VALUE): + return windll.CtApi.ctListItem(tag_handle, item_code, byref(buff), sizeof(buff), 0) def ctListEvent(self, connection, mode): return windll.CtApi.ctListEvent(connection, mode) + def ctFindFirst(self, connection, query, obj_handle): + return windll.CtApi.ctFindFirst(connection, str(query).encode("ascii"), None, pointer(obj_handle), 0 ) + + def ctFindNext(self, search_handle, obj_handle): + return windll.CtApi.ctFindNext(search_handle, pointer(obj_handle)) + + def ctFindScroll(self, search_handle, search_mode, search_offset, obj_handle): + return windll.CtApi.ctFindScroll(search_handle, search_mode, search_offset, pointer(obj_handle)) + + def ctFindNumRecords(self, search_handle): + return windll.CtApi.ctFindNumRecords(search_handle) + + def ctFindClose(self, search_handle): + return windll.CtApi.ctFindClose(search_handle) + + def ctGetProperty(self, obj_handle, prop_name, buff): + return windll.CtApi.ctGetProperty(obj_handle, str(prop_name).encode("ascii"), byref(buff), sizeof(buff), None, DBTYPE_STR) + def getErrorCode(self): return GetLastError()