Skip to content

Commit cf3decc

Browse files
committed
Added fallback to type detection to allow automatic ENUM usage + Added exception raising for unknown types
1 parent 904bc39 commit cf3decc

File tree

3 files changed

+90
-27
lines changed

3 files changed

+90
-27
lines changed

pyads/connection.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
_dict_slice_generator,
9393
bytes_from_dict,
9494
size_of_structure,
95+
ADSError,
9596
)
9697
from .symbol import AdsSymbol
9798
from .utils import decode_ads
@@ -174,16 +175,16 @@ def _query_plc_datatype_from_name(self, data_name: str,
174175
175176
If cache_symbol_info is True then the SymbolInfo will be cached and adsGetSymbolInfo
176177
will only used once.
177-
178178
"""
179+
info = None
179180
if cache_symbol_info:
180181
info = self._symbol_info_cache.get(data_name)
181-
if info is None:
182-
info = adsGetSymbolInfo(self._port, self._adr, data_name)
183-
self._symbol_info_cache[data_name] = info
184-
else:
182+
if info is None:
185183
info = adsGetSymbolInfo(self._port, self._adr, data_name)
186-
return AdsSymbol.get_type_from_str(info.symbol_type)
184+
if cache_symbol_info:
185+
self._symbol_info_cache[data_name] = info
186+
187+
return AdsSymbol.get_type_from_info(info)
187188

188189
def open(self) -> None:
189190
"""Connect to the TwinCAT message router."""
@@ -543,6 +544,10 @@ def read_by_name(
543544
plc_datatype = self._query_plc_datatype_from_name(data_name,
544545
cache_symbol_info)
545546

547+
if plc_datatype is None:
548+
# `adsSyncReadReqEx2()` will fail for a None type
549+
raise ADSError(None, f"Failed to detect datatype for `{data_name}`")
550+
546551
return adsSyncReadByNameEx(
547552
self._port,
548553
self._adr,
@@ -688,6 +693,10 @@ def write_by_name(
688693
plc_datatype = self._query_plc_datatype_from_name(data_name,
689694
cache_symbol_info)
690695

696+
if plc_datatype is None:
697+
# `adsSyncWriteReqEx()` does not support `None` for a type
698+
raise ADSError(None, f"Failed to detect datatype for `{data_name}`")
699+
691700
return adsSyncWriteByNameEx(
692701
self._port, self._adr, data_name, value, plc_datatype, handle=handle
693702
)

pyads/symbol.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
from typing import TYPE_CHECKING, Any, Optional, List, Tuple, Callable, Union, Type
1717

1818
from . import constants # To access all constants, use package notation
19-
from .constants import PLCDataType
19+
from .constants import PLCDataType, ads_type_to_ctype
2020
from .pyads_ex import adsGetSymbolInfo, ADSError
21-
from .structs import NotificationAttrib
21+
from .structs import NotificationAttrib, SAdsSymbolEntry
2222

2323
# ads.Connection relies on structs.AdsSymbol (but in type hints only), so use
2424
# this 'if' to only include it when type hinting (False during execution)
@@ -125,7 +125,7 @@ def __init__(
125125
self.name = name
126126
self.index_offset = index_offset
127127
self.index_group = index_group
128-
self.symbol_type = symbol_type
128+
self.symbol_type = None # Apply `symbol_type` later
129129
self.comment = comment
130130
self._value: Any = None
131131

@@ -137,15 +137,14 @@ def __init__(
137137
from .ads import size_of_structure
138138
self._structure_size = size_of_structure(self.structure_def * self.array_size)
139139

140+
self.plc_type: Optional[Type[PLCDataType]] = None
141+
140142
if missing_info:
141143
self._create_symbol_from_info() # Perform remote lookup
142144

143-
# Now `self.symbol_type` should have a value, find the actual PLCTYPE
144-
# from it.
145-
# This is relevant for both lookup and full user definition.
146-
147-
self.plc_type: Optional[Type[PLCDataType]] = None
148-
if self.symbol_type is not None:
145+
# Apply user-provided type (overriding auto detect if any):
146+
if symbol_type is not None:
147+
self.symbol_type = symbol_type
149148
if isinstance(self.symbol_type, str): # Perform lookup if string
150149
self.plc_type = AdsSymbol.get_type_from_str(self.symbol_type)
151150
else: # Otherwise `symbol_type` is probably a pyads.PLCTYPE_* constant
@@ -166,12 +165,8 @@ def _create_symbol_from_info(self) -> None:
166165
if info.comment:
167166
self.comment = info.comment
168167

169-
# info.dataType is an integer mapping to a type in
170-
# constants.ads_type_to_ctype.
171-
# However, this type ignores whether the variable is really an array!
172-
# So are not going to be using this and instead rely on the textual
173-
# type
174-
self.symbol_type = info.symbol_type # Save the type as string
168+
self.plc_type = self.get_type_from_info(info)
169+
self.symbol_type = info.symbol_type # Also save the type as string
175170

176171
def _check_for_open_connection(self) -> None:
177172
"""Assert the current object is ready to read from/write to.
@@ -195,6 +190,12 @@ def read(self) -> Any:
195190
structure_size=self._structure_size,
196191
array_size=self.array_size)
197192
else:
193+
if self.plc_type is None:
194+
raise ADSError(
195+
None,
196+
f"Cannot read data with unknown datatype for symbol "
197+
f"{self.name} ({self.symbol_type})"
198+
)
198199
self._value = self._plc.read(self.index_group, self.index_offset, self.plc_type)
199200

200201
return self._value
@@ -218,6 +219,12 @@ def write(self, new_value: Optional[Any] = None) -> None:
218219
self._plc.write_structure_by_name(self.name, new_value, self.structure_def,
219220
structure_size=self._structure_size, array_size=self.array_size)
220221
else:
222+
if self.plc_type is None:
223+
raise ADSError(
224+
None,
225+
f"Cannot write data with unknown datatype for symbol "
226+
f"{self.name} ({self.symbol_type})"
227+
)
221228
self._plc.write(self.index_group, self.index_offset, new_value, self.plc_type)
222229

223230
def __repr__(self) -> str:
@@ -286,6 +293,30 @@ def _value_callback(self, notification: Any, data_name: Any) -> None:
286293
)
287294
self._value = value
288295

296+
@classmethod
297+
def get_type_from_info(cls, info: SAdsSymbolEntry) -> Optional[Type[PLCDataType]]:
298+
"""Get PLCTYPE_* from symbol info struct
299+
300+
Also see :meth:`~get_type_from_str`.
301+
"""
302+
plc_type = cls.get_type_from_str(info.symbol_type)
303+
if plc_type is not None:
304+
return plc_type
305+
306+
# Failed to detect by name (e.g. type is enum)
307+
308+
# Use `ADST_*` integer to detect type instead
309+
plc_type = ads_type_to_ctype.get(info.dataType, None)
310+
if plc_type is not None:
311+
array_size = int(info.size / sizeof(plc_type))
312+
# However, `dataType` is always a scalar, even if the object is an array:
313+
if array_size > 1:
314+
plc_type = plc_type * array_size
315+
316+
return plc_type
317+
318+
return None
319+
289320
@staticmethod
290321
def get_type_from_str(type_str: str) -> Optional[Type[PLCDataType]]:
291322
"""Get PLCTYPE_* from PLC name string

tests/test_symbol.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import pyads
1717
from pyads.testserver import AdsTestServer, AdvancedHandler, PLCVariable
18-
from pyads import constants, AdsSymbol, bytes_from_dict
18+
from pyads import constants, AdsSymbol, ADSError, bytes_from_dict
1919

2020
from tests.test_connection_class import create_notification_struct
2121

@@ -143,8 +143,8 @@ def test_init_by_name_matrix_style(self):
143143
struct.pack("<21b", *range(21)),
144144
ads_type=constants.ADST_VOID,
145145
symbol_type="matrix_21_int8_T", # Simulink array looks like this
146-
index_group = 123,
147-
index_offset = 100,
146+
index_group=123,
147+
index_offset=100,
148148
)
149149
self.handler.add_variable(var)
150150
self.plc.open()
@@ -158,7 +158,7 @@ def test_init_by_name_matrix_style(self):
158158

159159
# Verify looked up info
160160
self.assertEqual(constants.PLCTYPE_ARR_SINT(21), symbol.plc_type)
161-
self.assertEqual(var.symbol_type, symbol.symbol_type)
161+
self.assertEqual("matrix_21_int8_T", symbol.symbol_type)
162162

163163
self.assertAdsRequestsCount(0) # No requests
164164

@@ -265,10 +265,10 @@ def test_init_invalid_type(self):
265265
# Create symbol while providing everything:
266266
symbol = AdsSymbol(self.plc, name=var.name)
267267
self.assertEqual(var.symbol_type, symbol.symbol_type)
268-
with self.assertRaises(TypeError) as cm:
268+
with self.assertRaises(ADSError) as cm:
269269
# Error is thrown inside pyads_ex
270270
symbol.read()
271-
self.assertIn("NoneType", str(cm.exception))
271+
self.assertIn("unknown datatype", str(cm.exception))
272272
self.assertAdsRequestsCount(1) # Only a WRITE followed by a READ
273273

274274
def test_read_write_errors(self):
@@ -370,6 +370,29 @@ def test_read_structure_array(self):
370370

371371
self.assertEqual(values, read_values)
372372

373+
def test_read_enum(self):
374+
"""Test reading from a symbol when it's an ENUM type"""
375+
self.handler.add_variable(
376+
PLCVariable("TestEnum", 7, constants.ADST_UINT16, symbol_type="E_MyEnum"))
377+
378+
with self.plc:
379+
symbol = self.plc.get_symbol("TestEnum")
380+
value = symbol.read()
381+
382+
self.assertEqual(7, value)
383+
384+
def test_read_enum_array(self):
385+
"""Test reading from a symbol when it's an array of an ENUM type"""
386+
value_bytes = struct.pack("<3h", 1, 2, 3)
387+
self.handler.add_variable(
388+
PLCVariable("TestEnumList", value_bytes, constants.ADST_UINT16, symbol_type="ARRAY [1..3] OF E_MyEnum"))
389+
390+
with self.plc:
391+
symbol = self.plc.get_symbol("TestEnumList")
392+
value = symbol.read()
393+
394+
self.assertEqual([1, 2, 3], value)
395+
373396
def test_write(self):
374397
"""Test symbol value writing"""
375398
with self.plc:

0 commit comments

Comments
 (0)