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 388e1d1..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() @@ -108,8 +111,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 @@ -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/connection.py b/pyctapi/connection.py index 4c7f63f..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)) @@ -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,9 +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() @@ -152,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: @@ -162,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..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", @@ -119,6 +156,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) @@ -131,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()