Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions examples/test_search.py
Original file line number Diff line number Diff line change
@@ -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))


46 changes: 44 additions & 2 deletions pyctapi/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#

from ctypes import create_string_buffer
from ctypes.wintypes import HANDLE

from pyctapi import pyctapi

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

23 changes: 13 additions & 10 deletions pyctapi/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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()

Expand All @@ -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:
Expand All @@ -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()
Expand All @@ -152,19 +155,19 @@ 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:
print(self.host(), "Added tag list %s" % list_name)
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):
Expand Down
64 changes: 63 additions & 1 deletion pyctapi/pyctapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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()