APIs, concepts, guides, and more
_helpers.py
1""" Helper functions for RapidCode Python samples.
2"""
3
4import struct
5import sys
6
7from _imports import RapidCode, RAPIDCODE_DIR, constants, platform
8
9
10# ---------------------------------------------------------------------------
11# stdin_is_interactive / key_pressed(): non-blocking "has the user pressed a key?"
12# for sample loops.
13#
14# The platform decision is made ONCE at import time by binding key_pressed to the
15# right private implementation, so the per-call cost is a single function call and
16# the platform-specific imports (msvcrt / select) only happen where they exist.
17# When stdin is not an interactive console (CI runs the samples with stdin
18# redirected), stdin_is_interactive is False and key_pressed is bound to a stub that
19# always returns False, so a sample polling it can never block or throw in an
20# automated run. Samples should also stop on their own when not interactive.
21# ---------------------------------------------------------------------------
22
24 """True only for a real console: isatty() is also True for the NUL device on Windows."""
25 import ctypes
26 import msvcrt
27 mode = ctypes.c_uint()
28 handle = msvcrt.get_osfhandle(sys.stdin.fileno())
29 return bool(ctypes.windll.kernel32.GetConsoleMode(handle, ctypes.byref(mode)))
30
31
33 """stdin is not a terminal (automated test run): never reports a key press."""
34 return False
35
36
38 """Windows console: msvcrt.kbhit() reports a pending key press without blocking."""
39 return _msvcrt.kbhit()
40
41
42def _key_pressed_posix() -> bool:
43 """Linux/macOS terminal: select() with a zero timeout polls stdin without blocking.
44 Note the terminal is line-buffered by default, so this reports the key after Enter."""
45 readable, _, _ = _select.select([sys.stdin], [], [], 0)
46 return bool(readable)
47
48
49try:
50 stdin_is_interactive = sys.stdin.isatty() and (platform.system() != "Windows" or _stdin_is_console_windows())
51except (AttributeError, OSError, ValueError): # stdin closed or replaced
52 stdin_is_interactive = False
53
54if not stdin_is_interactive:
55 key_pressed = _key_pressed_no_terminal
56elif platform.system() == "Windows":
57 import msvcrt as _msvcrt
58 key_pressed = _key_pressed_windows
59else:
60 import select as _select
61 key_pressed = _key_pressed_posix
62
63
64# EtherCAT AL Status register (0x0130): bits 0-3 are the state, bit 4 is the error flag.
65AL_STATUS_STATES = {
66 0x01: "INIT",
67 0x02: "PREOP",
68 0x03: "BOOT",
69 0x04: "SAFEOP",
70 0x08: "OP",
71}
72AL_STATUS_ERROR_SUFFIX = "+ERR"
73
74
75def decode_al_status(al_status: int) -> str:
76 """Decode one node's AL Status register to its state name, e.g. OP or SAFEOP+ERR."""
77 state_name = AL_STATUS_STATES.get(al_status & 0x0F, "?")
78 if al_status & 0x10:
79 state_name += AL_STATUS_ERROR_SUFFIX
80 return state_name
81
82
83def decode_al_status_ored(al_status: int) -> str:
84 """Decode the network-wide AL Status (NetworkStatus.AlStatus), the OR of every node's
85 register, to the states present, e.g. OP or SAFEOP|OP+ERR.
86
87 Only the single-bit states are unambiguous when ORed: BOOT (3) is INIT (1) | PREOP (2),
88 so a network showing INIT|PREOP may hold a node in BOOT."""
89 states_present = [name for bit, name in ((0x01, "INIT"), (0x02, "PREOP"), (0x04, "SAFEOP"), (0x08, "OP"))
90 if al_status & bit]
91 state_name = "|".join(states_present) if states_present else "NONE"
92 if al_status & 0x10:
93 state_name += AL_STATUS_ERROR_SUFFIX
94 return state_name
95
96
97def get_enum_name(prefix: str, value: int) -> str:
98 """Get enum name using reflection on SWIG-generated constants.
99
100 Args:
101 prefix: The enum prefix (e.g., "RSINetworkState_RSINetworkState")
102 value: The enum value to look up
103
104 Returns:
105 The enum name with prefix stripped, or "UNKNOWN(value)" if not found
106
107 Example:
108 get_enum_name("RSINetworkState_RSINetworkState", controller.NetworkStateGet())
109 # Returns "OPERATIONAL" for RSINetworkState_RSINetworkStateOPERATIONAL
110 """
111 for name in dir(RapidCode):
112 if name.startswith(prefix):
113 if getattr(RapidCode, name) == value:
114 return name[len(prefix):]
115 return f"UNKNOWN({value})"
116
117
118def get_creation_parameters():
119 # create a motion controller and return it.
120 # If any errors are found, raise an exception with the error log as the message.
121 creation_params: RapidCode.CreationParameters = RapidCode.CreationParameters()
122 creation_params.RmpPath = RAPIDCODE_DIR
123 creation_params.NicPrimary = constants.RMP_NIC_PRIMARY
124
125 if platform.system() == "Windows":
126 creation_params.NodeName = constants.RMP_NODE_NAME
127 elif platform.system() == "Linux":
128 creation_params.CpuAffinity = constants.RMP_CPU_AFFINITY
129 else:
130 raise Exception("Unsupported platform")
131
132 return creation_params
133
134def check_errors(rsi_object):
135 # check for errors in the given rsi_object and print any errors that are found.
136 # If the error log contains any errors (not just warnings), raises an exception with the error log as the message.
137 # returns a tuple containing a boolean indicating whether the error log contained any errors and the error log string.
138 error_string_builder = ""
139 i = rsi_object.ErrorLogCountGet()
140 while rsi_object.ErrorLogCountGet() > 0:
141 error:RapidCode.RsiError = rsi_object.ErrorLogGet()
142 error_type = "WARNING" if error.isWarning else "ERROR"
143 error_string_builder += f"{error_type}: {error.text}\n"
144 if len(error_string_builder) > 0:
145 print(error_string_builder)
146 if "ERROR" in error_string_builder:
147 raise Exception(error_string_builder)
148 return "ERROR" in error_string_builder, error_string_builder
149
150def start_the_network(controller):
151 # attempts to start the network using the given MotionController object.
152 # If the network fails to start, it reads and prints any log messages that may be helpful
153 # in determining the cause of the problem, and then raises an RsiError exception.
154 if controller.NetworkStateGet() != RapidCode.RSINetworkState_RSINetworkStateOPERATIONAL: # Check if network is started already.
155 print("Starting Network..")
156 controller.NetworkStart() # If not. Initialize The Network. (This can also be done from RapidSetup Tool)
157
158 if controller.NetworkStateGet() != RapidCode.RSINetworkState_RSINetworkStateOPERATIONAL: # Check if network is started again.
159 start_error = controller.LastNetworkStartErrorGet()
160 print(
161 "Network start error: "
162 f"{enum_to_name(start_error, 'RSINetworkStartError')} ({start_error})"
163 )
164
165 messages_to_read = controller.NetworkLogMessageCountGet() # Some kind of error starting the network, read the network log messages
166
167 for i in range(messages_to_read):
168 print(controller.NetworkLogMessageGet(i)) # Print all the messages to help figure out the problem
169 print("Expected OPERATIONAL state but the network did not get there.")
170 # raise Exception(Expected OPERATIONAL state but the network did not get there.) # Uncomment if you want your application to exit when the network isn't operational. (Comment when using phantom axis)
171 else: # Else, of network is operational.
172 print("Network Started")
173
174def abort_motion_object(motion_object):
175 # Aborts motion on the given motion object (Axis or MultiAxis), waits for motion to complete,
176 # clears faults, and verifies the object enters IDLE state.
177 # If the object fails to enter IDLE state, raises an exception with the error source.
178 motion_object.EStopAbort()
179 motion_object.MotionDoneWait()
180 motion_object.ClearFaults()
181
182 # check for idle state
183 verify_idle_state(motion_object)
184
185def verify_idle_state(motion_object):
186 # Verifies that the given motion object (Axis or MultiAxis) is in IDLE state.
187 # If not, raises an exception with the error source.
188 if motion_object.StateGet() != RapidCode.RSIState_RSIStateIDLE:
189 source = motion_object.SourceGet() # get state source enum
190 error_msg = f"Axis or MultiAxis {motion_object.NumberGet()} is expected to be in IDLE state, but is in state {enum_to_name(motion_object.StateGet(), 'RSIState')}. " \
191 f"\nError Source: {motion_object.SourceNameGet(source)}"
192 raise Exception(error_msg)
193
194def enum_to_name(value, prefix):
195 """Reverse lookup: int value -> enum name"""
196 for name in dir(RapidCode):
197 if name.startswith(prefix + "_" + prefix) and getattr(RapidCode, name) == value:
198 return name.split(prefix + "_" + prefix)[1]
199 return str(value)
200
201# RSIDataType name -> FirmwareValue attribute holding a value of that type. Every RSIDataType
202# member is covered (masks are read as their unsigned width; SHORT/USHORT are the deprecated
203# spellings of INT16/UINT16).
204_FIRMWARE_VALUE_ATTRIBUTES = {
205 "BOOL": "Bool",
206 "INT8": "Int8", "UINT8": "UInt8",
207 "INT16": "Int16", "UINT16": "UInt16", "SHORT": "Int16", "USHORT": "UInt16",
208 "INT32": "Int32", "UINT32": "UInt32", "MASK32": "UInt32",
209 "INT64": "Int64", "UINT64": "UInt64", "MASK64": "UInt64",
210 "FLOAT": "Float", "DOUBLE": "Double",
211}
212
213
214def firmware_value_attribute(data_type) -> str:
215 """Name of the FirmwareValue attribute that holds a value of the given RSIDataType.
216 Look it up once per address (it reflects over the RapidCode module), then read many
217 values with getattr(firmware_value, attribute)."""
218 data_type_name = enum_to_name(data_type, "RSIDataType")
219 try:
220 return _FIRMWARE_VALUE_ATTRIBUTES[data_type_name]
221 except KeyError:
222 raise ValueError(f"RSIDataType {data_type_name} cannot be read from a FirmwareValue") from None
223
224
225def recorder_value_reader(controller, data_type):
226 """The cheapest way to read one recorded value of the given RSIDataType from Python.
227
228 Returns a callable reader(recorder_number, record_index, data_index) for use after
229 RecorderRecordDataRetrieveBulk. Each RecorderRecordData*Get call crosses the SWIG
230 boundary once, and that crossing dominates the cost of draining a Recorder from Python:
231 RecorderRecordDataValueGet / RecorderRecordDataDoubleGet return a plain number (about
232 0.65 us per value), while RecorderRecordDataFirmwareValueGet returns a FirmwareValue
233 object that then needs an attribute read (about 1.3 us). A large Recorder set (many nodes,
234 every sample) only keeps up with the plain getters.
235
236 The Recorder copies 8 bytes from every address. Types up to 32 bits use ValueGet (the
237 low 32 bits) and are masked to their width and, if signed, sign-extended, so the bytes
238 next to a narrow value do not leak in; FLOAT reinterprets those 32 bits. DOUBLE uses
239 DoubleGet. 64-bit integer types use DoubleGet and reinterpret the 8 bytes, which is exact
240 unless the value's top 12 bits are all set (a NaN bit pattern): counters never get there.
241 """
242 data_type_name = enum_to_name(data_type, "RSIDataType")
243 value_get = controller.RecorderRecordDataValueGet
244 double_get = controller.RecorderRecordDataDoubleGet
245 if data_type_name == "INT32":
246 return value_get
247 if data_type_name in ("UINT32", "MASK32"):
248 return lambda recorder, record, index: value_get(recorder, record, index) & 0xFFFFFFFF
249 if data_type_name == "BOOL":
250 return lambda recorder, record, index: bool(value_get(recorder, record, index) & 0xFF)
251 unsigned_widths = {"UINT8": 8, "UINT16": 16, "USHORT": 16}
252 if data_type_name in unsigned_widths:
253 mask = (1 << unsigned_widths[data_type_name]) - 1
254 return lambda recorder, record, index: value_get(recorder, record, index) & mask
255 signed_widths = {"INT8": 8, "INT16": 16, "SHORT": 16}
256 if data_type_name in signed_widths:
257 mask = (1 << signed_widths[data_type_name]) - 1
258 sign_bit = 1 << (signed_widths[data_type_name] - 1)
259 return lambda recorder, record, index: ((value_get(recorder, record, index) & mask) ^ sign_bit) - sign_bit
260 if data_type_name == "FLOAT":
261 return lambda recorder, record, index: struct.unpack("<f", struct.pack("<I", value_get(recorder, record, index) & 0xFFFFFFFF))[0]
262 if data_type_name == "DOUBLE":
263 return double_get
264 if data_type_name in ("UINT64", "MASK64"):
265 return lambda recorder, record, index: struct.unpack("<Q", struct.pack("<d", double_get(recorder, record, index)))[0]
266 if data_type_name == "INT64":
267 return lambda recorder, record, index: struct.unpack("<q", struct.pack("<d", double_get(recorder, record, index)))[0]
268 raise ValueError(f"RSIDataType {data_type_name} cannot be read from a Recorder record")
269
270
271def firmware_value_get(firmware_value, data_type):
272 """Read a FirmwareValue (a 64-bit union) as the Python value of the given RSIDataType.
273
274 Pair it with the AddressDataTypeGet family (Axis, MultiAxis, MotionController,
275 RapidCodeNetworkNode) so recorded or interrupt user data is interpreted with the
276 type the firmware actually stores at that address, e.g.:
277
278 data_type = node.AddressDataTypeGet(RapidCode.RSINetworkNodeAddressType_RSINetworkNodeAddressTypeAL_STATUS)
279 value = helpers.firmware_value_get(controller.RecorderRecordDataFirmwareValueGet(0), data_type)
280 """
281 return getattr(firmware_value, firmware_value_attribute(data_type))
bool _stdin_is_console_windows()
Definition _helpers.py:23
str firmware_value_attribute(data_type)
Definition _helpers.py:214
enum_to_name(value, prefix)
Definition _helpers.py:194
recorder_value_reader(controller, data_type)
Definition _helpers.py:225
bool _key_pressed_no_terminal()
Definition _helpers.py:32
bool _key_pressed_windows()
Definition _helpers.py:37
firmware_value_get(firmware_value, data_type)
Definition _helpers.py:271
str get_enum_name(str prefix, int value)
Definition _helpers.py:97
bool _key_pressed_posix()
Definition _helpers.py:42
str decode_al_status_ored(int al_status)
Definition _helpers.py:83
str decode_al_status(int al_status)
Definition _helpers.py:75