From 70ab6433bf1a5cc4006f270135d07515108e0afc Mon Sep 17 00:00:00 2001 From: Steve Holland Date: Tue, 9 Jul 2024 09:28:21 -0500 Subject: [PATCH 1/6] ACR9000 motion controller now working --- .../modules/motion_control/acr9000.py | 97 +++++++++++++++---- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/dataguzzler_python/modules/motion_control/acr9000.py b/dataguzzler_python/modules/motion_control/acr9000.py index d1f85be..2f8f309 100644 --- a/dataguzzler_python/modules/motion_control/acr9000.py +++ b/dataguzzler_python/modules/motion_control/acr9000.py @@ -4,6 +4,7 @@ import re import threading import random +import atexit import numpy as np import pint from dataguzzler_python import dgpy @@ -265,7 +266,7 @@ class axis(object): # factor relative to deg. (float) configured=None # boolean, set once axis is fully configured #enabled=None # boolean, is drive turned on - targetpos=None # target position (float) + _targetpos=None # target position (float) parent=None # acr9000 object def __init__(self,**kwargs): @@ -398,12 +399,12 @@ def rel(self, value): raise ValueError("Axis is not enabled") actpos=self.parent._GetPReg(actualpos[self.axis_num])/self.ppu - self.targetpos=raw_value + actpos + self._targetpos=raw_value + actpos self.parent._control_socket.write(f"PROG{self.proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') - self.parent._control_socket.write(f"{self.axis_name:s}{self.targetpos:.10g}\r".encode("utf-8")) + self.parent._control_socket.write(f"{self.axis_name:s}{self._targetpos:.10g}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') pass finally: @@ -457,12 +458,12 @@ def pos(self, value): raise ValueError("Axis is not enabled") #actpos=self.parent._GetPReg(actualpos[self.axis_num])/self.ppu - self.targetpos=raw_value + self._targetpos=raw_value self.parent._control_socket.write(f"PROG{self.proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') - self.parent._control_socket.write(f"{self.axis_name:s}{self.targetpos:.10g}\r".encode("utf-8")) + self.parent._control_socket.write(f"{self.axis_name:s}{self._targetpos:.10g}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') pass finally: @@ -531,6 +532,7 @@ class acr9000(metaclass=Module): _wait_status=None # either "Cancelled" (between WaitCancel() and # WaitRestart()) # or "Waiting" (BASIC wait program running on ACR) + _wait_exit=None # set to True to trigger the wait thread to exit all=None # axis_group object representing all axes def __init__(self,module_name,pyserial_url,**axis_units): @@ -540,15 +542,33 @@ def __init__(self,module_name,pyserial_url,**axis_units): self._spareprog=15 self.axisdict=collections.OrderedDict() self._wait_status="Cancelled" + self._wait_exit=False #_configure_socket(comm1) - _set_timeout(self._control_socket,50) #Set timeout to 50ms + _set_timeout(self._control_socket,500) #Set timeout to 500ms + self._control_socket.write(b"SYS\r") # Change to system mode gotbuf="Startup" + total_gotbuf=b"" while len(gotbuf) > 0: gotbuf=self._control_socket.read_until(expected=b">") + total_gotbuf += gotbuf + pass + + if b"SYS" not in total_gotbuf: + # Not responding... send escape key + self._control_socket.write(b"\x1b\r") # Change to system mode + self._control_socket.write(b"SYS\r") # Change to system mode + gotbuf="Startup" + total_gotbuf=b"" + while len(gotbuf) > 0: + gotbuf=self._control_socket.read_until(expected=b">") + total_gotbuf += gotbuf + pass pass _set_timeout(self._control_socket,STARTUPTIMEOUT) self._control_socket.write(b"SYS\r") # Change to system mode response=self._control_socket.read_until(expected=b'>') + #import pdb + #pdb.set_trace() assert(response.endswith(b"SYS>")) self._control_socket.write(b"HALT ALL\r") # stop all axes @@ -698,9 +718,21 @@ def __init__(self,module_name,pyserial_url,**axis_units): self._waiter_thread.start() self._waiter_ack_cond.wait() pass + #atexit.register(self.atexit) # Register an atexit function so that we can cleanly trigger our subthread to end. Otherwise we might well crash on exit. self._restart_wait() pass + #def atexit(self): + # #print("acr9000: Performing atexit()") + # with self._waiter_cond: + # self._waiter_exit = True; + # self._waiter_cond.notify() + # pass + # + # self._waiter_thread.join() + # + # pass + def waitall(self): waitlist = list(self._wait_dict.keys()) self._wait(waitlist) @@ -770,7 +802,7 @@ def _waiter_thread_code(self): while True: with self._waiter_cond: - if self._wait_status=='Cancelled': + if self._wait_status=='Cancelled' and not self._wait_exit: self._waiter_ack_cond.notify() self._waiter_cond.wait() pass @@ -778,15 +810,27 @@ def _waiter_thread_code(self): pass self._waiter_ack_cond.notify() wait_status=self._wait_status + + if self._wait_exit: # not actually used + if self._wait_status == 'Waiting': + self._wait_status = 'Cancelled' + #Press the escape key + self._control_socket.write(b'\x1b') + self._control_socket.read_until(expected=b'>') + self._control_socket.write(b'HALT\r') + self._control_socket.read_until(expected=b'>') #Wait for prompt + pass + return # waiter thread exit pass while wait_status=='Waiting': #response=self._read() response=self._control_socket.read_until(expected=b'>') if response is not None: efpos=response.find(b'EXITFLAG') - if efpos >= 0: + if efpos >= 0 and efpos < len(response): efmatch=re.match(rb'EXITFLAG=(\d+)',response[efpos:]) assert(efmatch is not None) + efpos += len(efmatch.group(0)) linenum=int(efmatch.group(1)) with self._waiter_cond: if linenum in self._wait_dict: @@ -796,12 +840,14 @@ def _waiter_thread_code(self): pass pass - continue #Bypass check of wait status until we have something that is not an EXITFLAG. - pass - else: - #A different string: must be a prompt - #OK to check wait status, as we must have pressed escape + pass + #else: + # #A different string: must be a prompt + # #OK to check wait status, as we must have pressed escape + # pass + if efpos >= 0: + continue #Bypass check of wait status until we have something that is not an EXITFLAG. pass with self._waiter_cond: wait_status=self._wait_status @@ -828,8 +874,8 @@ def _wait(self,axislist): pass assert(linenum not in self._wait_dict) - self._wait_cond = threading.Condition(lock=self._waiter_cond) - self._wait_dict[linenum] = self._wait_cond + _wait_cond = threading.Condition(lock=self._waiter_cond) + self._wait_dict[linenum] = _wait_cond pass self._control_socket.write(f'PROG{self._spareprog:d}\r'.encode("utf-8")) @@ -843,11 +889,24 @@ def _wait(self,axislist): finally: self._restart_wait() pass - with self._wait_cond: - self._wait_cond.wait() - pass + with dgpy.UnprotectedContext: + with _wait_cond: + _wait_cond.wait() + pass + pass + # Need to erase the line of code (wait thread removed us from wait dict) + self._abort_wait() + try: + self._control_socket.write(f"PROG{self._spareprog:d}\r".encode("utf-8")) + self._control_socket.read_until(expected=b'>') #"P00>" + self._control_socket.write(f"{linenum:d}\r".encode("utf-8")) # just the line number alone deletes the line + self._control_socket.read_until(expected=b'>') #"P00>" + pass + finally: + self._restart_wait() + pass pass - + def _restart_wait(self): assert(self._wait_status == 'Cancelled') # go to our spare program level From f55268e65c779f021f1469ab950a29960f69ed40 Mon Sep 17 00:00:00 2001 From: Steve Holland Date: Wed, 10 Jul 2024 12:16:49 -0500 Subject: [PATCH 2/6] acr9000 fix multiaxis bugs --- .../modules/motion_control/acr9000.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/dataguzzler_python/modules/motion_control/acr9000.py b/dataguzzler_python/modules/motion_control/acr9000.py index 2f8f309..4742199 100644 --- a/dataguzzler_python/modules/motion_control/acr9000.py +++ b/dataguzzler_python/modules/motion_control/acr9000.py @@ -186,7 +186,10 @@ def rel(self, value): for axis_num in range(len(self.axis_names)): axis_name = self.axis_names[axis_num] axis = getattr(self.parent, axis_name) - axis.rel=value[axis_num] + desired_rel = value[axis_num] + if desired_rel is not None and not np.isnan(desired_rel): + axis.rel=desired_rel + pass pass pass @@ -222,19 +225,23 @@ def pos(self, value): for axis_num in range(len(self.axis_names)): axis_name = self.axis_names[axis_num] axis = getattr(self.parent, axis_name) - axis.pos = value[axis_num] + desired_pos = value[axis_num] + if desired_pos is not None and not np.isnan(desired_pos): + + axis.pos = desired_pos + pass pass pass @property def enabled(self): - pos = np.zeros(len(self.axis_names), dtype=bool) + enabled = np.zeros(len(self.axis_names), dtype=bool) for axis_num in range(len(self.axis_names)): axis_name = self.axis_names[axis_num] axis = getattr(self.parent, axis_name) - pos[axis_num] = axis.pos + enabled[axis_num] = axis.enabled pass - return pos + return enabled @enabled.setter def enabled(self, value): From 46a8decc770940cec34219538b93bd3b625fe511 Mon Sep 17 00:00:00 2001 From: Steve Holland Date: Wed, 10 Jul 2024 14:49:50 -0500 Subject: [PATCH 3/6] Refactor motion controller abstraction into a common location. Also create an axis abstraction and pull that out as well. Same with axis groups. --- README.md | 3 +- .../modules/motion_control/acr9000.py | 385 +++++++----------- dataguzzler_python/motion_controller.py | 335 +++++++++++++++ demos/acr9000.dgp | 2 +- 4 files changed, 474 insertions(+), 251 deletions(-) create mode 100644 dataguzzler_python/motion_controller.py diff --git a/README.md b/README.md index 48c3b73..87fe4a6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ For a quickstart guide see: doc/source/quickstart.rst Basic requirements are Python v3.8 or above with the following packages: numpy, setuptools, wheel -Basic installation is "pip install --no-deps --no-build-isolation ." +Basic installation is (possibly as root or Administrator): + pip install --no-deps --no-build-isolation . More detailed documentation is also available in doc/source/ diff --git a/dataguzzler_python/modules/motion_control/acr9000.py b/dataguzzler_python/modules/motion_control/acr9000.py index 4742199..fb36ca0 100644 --- a/dataguzzler_python/modules/motion_control/acr9000.py +++ b/dataguzzler_python/modules/motion_control/acr9000.py @@ -1,7 +1,7 @@ import sys import collections import numbers -import re +import re import threading import random import atexit @@ -9,6 +9,7 @@ import pint from dataguzzler_python import dgpy from dataguzzler_python.dgpy import Module,InitCompatibleThread +from dataguzzler_python.motion_controller import MotionControllerBase,AxisBase, SimpleAxisGroup STARTUPTIMEOUT=600 # ms NORMALTIMEOUT=None # disable timeout @@ -135,144 +136,20 @@ def _set_timeout(socket,timeout_ms_or_None): pass pass -class axis_group: - parent=None # motion controller object - axis_names=None # List of axis names - matching_units=None # True if all axes use exactly the same units - - def __init__(self, parent, axis_names): - self.parent = parent - self.axis_names = axis_names - - self.matching_units = True - axis0_quantity = getattr(self.parent, axis_names[0]).unit_quantity - for axis_num in range(1, len(self.axis_names)): - axis_name = axis_names[axis_num] - if getattr(self.parent, axis_name).unit_quantity != axis0_quantity: - self.matching_units = False - pass - pass - pass - - def zero(self): - for axis_name in self.axis_names: - axis = getattr(self.parent, axis_name) - axis.zero() - pass - pass - - def wait(self): - self.parent._wait(self.axis_names) - pass - - @property - def moving(self): - moving = np.zeros(len(self.axis_names), dtype=bool) - for axis_num in range(len(self.axis_names)): - axis_name = self.axis_names[axis_num] - axis = getattr(self.parent, axis_name) - moving[axis_num] = axis.moving - pass - return moving - - @property - def rel(self): - return None - - @rel.setter - def rel(self, value): - if len(value) != len(self.axis_names): - raise ValueError("Incorrect number of axis offsets given") - for axis_num in range(len(self.axis_names)): - axis_name = self.axis_names[axis_num] - axis = getattr(self.parent, axis_name) - desired_rel = value[axis_num] - if desired_rel is not None and not np.isnan(desired_rel): - axis.rel=desired_rel - pass - pass - pass - - def cancel(self): - for axis_name in self.axis_names: - axis = getattr(self.parent, axis_name) - axis.cancel() - pass - pass - - @property - def pos(self): - ur = pint.get_application_registry() - if self.matching_units: - # Create a single numpy quantity with the correct units - pos = ur.Quantity(np.zeros(len(self.axis_names), dtype='d'), getattr(self.parent, self.axis_names[0]).unit_quantity) - pass - else: - # Create a numpy array of objects which are scalar quantities - pos = np.zeros(len(self.axis_names), dtype=object) - pass - for axis_num in range(len(self.axis_names)): - axis_name = self.axis_names[axis_num] - axis = getattr(self.parent, axis_name) - pos[axis_num] = axis.pos - pass - return pos - - @pos.setter - def pos(self, value): - if len(value) != len(self.axis_names): - raise ValueError("Incorrect number of axis offsets given") - for axis_num in range(len(self.axis_names)): - axis_name = self.axis_names[axis_num] - axis = getattr(self.parent, axis_name) - desired_pos = value[axis_num] - if desired_pos is not None and not np.isnan(desired_pos): - - axis.pos = desired_pos - pass - pass - pass - - @property - def enabled(self): - enabled = np.zeros(len(self.axis_names), dtype=bool) - for axis_num in range(len(self.axis_names)): - axis_name = self.axis_names[axis_num] - axis = getattr(self.parent, axis_name) - enabled[axis_num] = axis.enabled - pass - return enabled - @enabled.setter - def enabled(self, value): - if isinstance(value, bool): - for axis_name in self.axis_names: - axis = getattr(self.parent, axis_name) - axis.enabled = value - pass - return - if len(value) != len(self.axis_names): - raise ValueError("Incorrect number of axis offsets given") - for axis_num in range(len(self.axis_names)): - axis_name = self.axis_names[axis_num] - axis = getattr(self.parent, axis_name) - axis.enabled = value[axis_num] - pass - pass - pass -class axis(object): - axis_name=None - proglevel=None # acr9000 program level assigned to this axis (integer) - axis_num=None # axis0, axis1, ... (integer) - ppu=None # pulses per unit (float) - unit_name=None - unit_quantity=None # pint quantity corresponding to axis units - unit_factor=None # unit factor. Sign is a flag for linear vs. rotational. +class ACR9000Axis(AxisBase): + axis_name=None # Name of this axis within its controller as a python string + _proglevel=None # acr9000 program level assigned to this axis (integer) + _axis_num=None # axis0, axis1, ... (integer) + _ppu=None # pulses per unit (float) + unit_name=None # String representing axis default units + unit_quantity=None # pint quantity corresponding to axis default units + _unit_factor=None # unit factor. Sign is a flag for linear vs. rotational. # if positive, factor relative to mm. If negative, # factor relative to deg. (float) - configured=None # boolean, set once axis is fully configured - #enabled=None # boolean, is drive turned on + + #enabled=None # boolean, is drive turned on (now a property) _targetpos=None # target position (float) parent=None # acr9000 object @@ -285,9 +162,49 @@ def __init__(self,**kwargs): raise ValueError(f"unknown attribute {arg:s}") pass pass + + # .wait() method implemented by AxisBase base class + #def wait(self): + # """Wait for this axis to stop moving.""" + # self.parent.wait([self.axis_name]) + # pass + #.waitrel property implemented by AxisBase base class + #@property + #def waitrel(self): + # """On read, returns None; on assignment, initiates a move + # relative to the current position and waits for the move to + # complete. Position can be a number, which is assumed to be in + # the axis default units, or a Pint quantity.""" + # return None + + #@waitrel.setter + #def waitrel(self, value): + # # Value may be given as just a number, in which case + # # default units are assumed, or as a pint quantity. + # self.rel = value + # self.wait() + # pass + + #.waitpos property implemented by AxisBase base class + #@property + #def waitpos(self): + # """On read, returns the current axis position; on assignment + # initiates the move to the specified position and waits for the + # move to complete. Position can be a number, which is assumed + # to be in the axis default units, or a Pint quantity.""" + # return self.pos + + #@waitpos.setter + #def waitpos(self, value): + # # Value may be given as just a number, in which case + # # default units are assumed, or as a pint quantity. + # self.pos = value + # self.wait() + # pass + @staticmethod - def units_to_factor(units): + def _units_to_factor(units): ur = pint.get_application_registry() quant = ur.Quantity(1.0,units) if quant.is_compatible_with(ur.millimeter): @@ -304,7 +221,7 @@ def units_to_factor(units): def _enabled(self): assert(self.parent._wait_status == 'Cancelled') # Must be called between _abort_wait and _restart_wait - self.parent._control_socket.write(f"PROG{self.proglevel:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"PROG{self._proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected =b'>') self.parent._control_socket.write(f"DRIVE {self.axis_name:s}\r".encode("utf-8")) drive_status_line=self.parent._control_socket.read_until(expected =b'>') @@ -322,7 +239,7 @@ def _enabled(self): if enabled: # Double-check that the kill-all-motion-request (KAMR) bit is not asserted - self.parent._control_socket.write(f"?bit{KAMRbit[self.axis_num]:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"?bit{KAMRbit[self._axis_num]:d}\r".encode("utf-8")) KAMR_line=self.parent._control_socket.read_until(expected=b'>') KAMR_match=re.match(rb"""\s*[?]bit\d+\s+(\d+)\s""", KAMR_line) bit_status=int(KAMR_match.group(1)) @@ -333,25 +250,26 @@ def _enabled(self): return enabled def zero(self): + """This method zeros the axis, defining the current position to be 0.0""" self.parent._abort_wait() try: - self.parent._control_socket.write(f"PROG{self.proglevel:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"PROG{self._proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') # issue REN command to cancel any preexisting position command self.parent._control_socket.write(f"REN {self.axis_name:s}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') # set the target equal to the actual position - self.parent._control_socket.write(f"P{targetpos[self.axis_num]:d}=P{trajectorypos[self.axis_num]:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"P{targetpos[self._axis_num]:d}=P{trajectorypos[self._axis_num]:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') # reset the encoder to define the current position as '0' self.parent._control_socket.write(f"RES {self.axis_name:s}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') # set the target equal to the actual position - self.parent._control_socket.write(f"P{targetpos[self.axis_num]:d}=P{trajectorypos[self.axis_num]:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"P{targetpos[self._axis_num]:d}=P{trajectorypos[self._axis_num]:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') - if abs(self.parent._GetPReg(actualpos[self.axis_num])) <= 5.0: + if abs(self.parent._GetPReg(actualpos[self._axis_num])) <= 5.0: # allow up to +- 5 encoder pulses of error return 0.0 @@ -363,17 +281,15 @@ def zero(self): pass pass - def wait(self): - self.parent._wait([self.axis_name]) - pass @property def moving(self): + """Returns True if the axis is moving or False if it is stopped""" self.parent._abort_wait() try: - trajpos=self.parent._GetPReg(trajectorypos[self.axis_num]) - targpos=self.parent._GetPReg(targetpos[self.axis_num]) + trajpos=self.parent._GetPReg(trajectorypos[self._axis_num]) + targpos=self.parent._GetPReg(targetpos[self._axis_num]) return trajpos!=targpos finally: self.parent._restart_wait() @@ -382,6 +298,8 @@ def moving(self): @property def rel(self): + """On read, returns None; on assignment, initiates + a move relative to the current position.""" return None @rel.setter @@ -405,10 +323,10 @@ def rel(self, value): if not self._enabled(): raise ValueError("Axis is not enabled") - actpos=self.parent._GetPReg(actualpos[self.axis_num])/self.ppu + actpos=self.parent._GetPReg(actualpos[self._axis_num])/self._ppu self._targetpos=raw_value + actpos - self.parent._control_socket.write(f"PROG{self.proglevel:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"PROG{self._proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') self.parent._control_socket.write(f"{self.axis_name:s}{self._targetpos:.10g}\r".encode("utf-8")) @@ -420,12 +338,13 @@ def rel(self, value): pass def cancel(self): + """Cancel any move in progress on this axis""" self.parent._abort_wait() try: - self.parent._control_socket.write(f"HALT PROG{self.proglevel:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"HALT PROG{self._proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') - self.parent._control_socket.write(f"P{targetpos[self.axis_num]:d}=P{trajectorypos[self.axis_num]:d}\r".encode("utf-8")) # set the target equal to the actual position so that we record the axis as not moving. + self.parent._control_socket.write(f"P{targetpos[self._axis_num]:d}=P{trajectorypos[self._axis_num]:d}\r".encode("utf-8")) # set the target equal to the actual position so that we record the axis as not moving. self.parent._control_socket.read_until(expected=b'>') pass finally: @@ -435,9 +354,12 @@ def cancel(self): @property def pos(self): + """On read, returns the current axis position; + on assignment initiates the move to the specified + position""" self.parent._abort_wait() try: - return (self.parent._GetPReg(actualpos[self.axis_num])/self.ppu)*self.unit_quantity + return (self.parent._GetPReg(actualpos[self._axis_num])/self._ppu)*self.unit_quantity finally: self.parent._restart_wait() pass @@ -464,10 +386,10 @@ def pos(self, value): if not self._enabled(): raise ValueError("Axis is not enabled") - #actpos=self.parent._GetPReg(actualpos[self.axis_num])/self.ppu + #actpos=self.parent._GetPReg(actualpos[self._axis_num])/self._ppu self._targetpos=raw_value - self.parent._control_socket.write(f"PROG{self.proglevel:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"PROG{self._proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') self.parent._control_socket.write(f"{self.axis_name:s}{self._targetpos:.10g}\r".encode("utf-8")) @@ -480,6 +402,9 @@ def pos(self, value): @property def enabled(self): + """On read, returns True if the current axis is enabled, False + otherwise. On assignment, attempts to turn the axis on or off + according to the truth value provided (True or False).""" self.parent._abort_wait() try: return self._enabled() @@ -495,16 +420,16 @@ def enabled(self, value): try: if enabled: # issue ctrl-y to clear all kill-all-motion-request (KAMR) flags - self.parent._control_socket.write(f"PROG{self.proglevel:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"PROG{self._proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') pass - self.parent._control_socket.write(f"PROG{self.proglevel:d}\r".encode("utf-8")) + self.parent._control_socket.write(f"PROG{self._proglevel:d}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') if enabled: # issue REN command to cancel any preexisting position self.parent._control_socket.write(f"REN {self.axis_name:s}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') - self.parent._control_socket.write(f"P{targetpos[self.axis_num]:d}=P{trajectorypos[self.axis_num]:d}\r".encode("utf-8")) # set the target equal to the actual position + self.parent._control_socket.write(f"P{targetpos[self._axis_num]:d}=P{trajectorypos[self._axis_num]:d}\r".encode("utf-8")) # set the target equal to the actual position self.parent._control_socket.read_until(expected=b'>') self.parent._control_socket.write(f"DRIVE ON {self.axis_name:s}\r".encode("utf-8")) self.parent._control_socket.read_until(expected=b'>') @@ -521,16 +446,10 @@ def enabled(self, value): pass pass -class acr9000(metaclass=Module): +class ACR9000(MotionControllerBase,metaclass=Module): _control_socket=None _spareprog=None # program level not used by any axis - axisdict=None -# reader_thread=None # dedicated thread needed for reading because there is no interruptible or asynchronous read functionality consistently available. - # read_lock=None # lock for read_complete_cond and read_request_cond - # read_request_cond=None # condition variable for notifying read request - # read_complete_cond=None # condition variable for notifying read complete - # read_request=None # list provided by requester. Filled out with success flag and result string. Locked by read_lock - + axis=None # Ordered dictionary of axis objects _waiter_cond=None # condition variable used to signal waiter thread and lock for wait_dict and wait_status _waiter_ack_cond = None # condition variable used by waiter thread to acknowledge. Uses same lock as waiter_cond @@ -540,14 +459,14 @@ class acr9000(metaclass=Module): # WaitRestart()) # or "Waiting" (BASIC wait program running on ACR) _wait_exit=None # set to True to trigger the wait thread to exit - all=None # axis_group object representing all axes + all=None # SimpleAxisGroup object representing all axes def __init__(self,module_name,pyserial_url,**axis_units): ur = pint.get_application_registry() self._control_socket=dgpy.include(dgpy,'serial_device.dpi',port_name=pyserial_url,baudrate=38400,xonxoff=True) self._spareprog=15 - self.axisdict=collections.OrderedDict() + self.axis=collections.OrderedDict() self._wait_status="Cancelled" self._wait_exit=False #_configure_socket(comm1) @@ -601,7 +520,7 @@ def __init__(self,module_name,pyserial_url,**axis_units): axis_name = attach_match.group(2).decode("utf-8") if axis_num < MAXAXES and len(axis_name) > 0: #Got valid attach line - unit_factor = axis.units_to_factor(axis_units[axis_name]) + unit_factor = Axis._units_to_factor(axis_units[axis_name]) #Extract the PPU self._control_socket.write(f'AXIS{axis_num:d} PPU\r'.encode("utf-8")) ppu_response=self._control_socket.read_until(expected=b'>') @@ -609,19 +528,18 @@ def __init__(self,module_name,pyserial_url,**axis_units): if ppu_match is None: raise ValueError(f'Bad PPU line for axis {axis_name:s}') ppu = float(ppu_match.group(1)) - Axis = axis(axis_name=axis_name, - proglevel=proglevel, - axis_num=axis_num, - ppu=ppu, + axis = Axis(axis_name=axis_name, + _proglevel=proglevel, + _axis_num=axis_num, + _ppu=ppu, unit_name=axis_units[axis_name], unit_quantity=ur.Quantity(axis_units[axis_name]), - unit_factor=unit_factor, - configured=True, + _unit_factor=unit_factor, parent=self) - self.axisdict[axis_name]=Axis + self.axis[axis_name]=axis if hasattr(self,axis_name): raise ValueError(f"{module_name:s} axis {axis_name:s} shadows a method or attribute") - setattr(self,axis_name,Axis) + setattr(self,axis_name,axis) pass pass pass @@ -634,7 +552,7 @@ def __init__(self,module_name,pyserial_url,**axis_units): pass # Create an 'all' object that refers to all axes - self.all = axis_group(self, list(self.axisdict.keys())) + self.all = self.create_group(list(self.axis.keys())) #Use spareprog to store our monitoring program self._control_socket.write(f'PROG{self._spareprog:d}\r'.encode("utf-8")) @@ -685,19 +603,19 @@ def __init__(self,module_name,pyserial_url,**axis_units): self._control_socket.read_until(expected=b'>') #"P00>" #Turn off all axes - for axis_name in self.axisdict: - Axis=self.axisdict[axis_name] - self._control_socket.write(f'PROG{Axis.proglevel:d}\r'.encode("utf-8")) + for axis_name in self.axis: + axis=self.axis[axis_name] + self._control_socket.write(f'PROG{axis._proglevel:d}\r'.encode("utf-8")) self._control_socket.read_until(expected=b'>') #"P00>" # issue REN command to cancel any preexisting position - self._control_socket.write(f'REN {Axis.axis_name:s}\r'.encode("utf-8")) + self._control_socket.write(f'REN {axis.axis_name:s}\r'.encode("utf-8")) self._control_socket.read_until(expected=b'>') #"P00>" - self._control_socket.write(f'P{targetpos[Axis.axis_num]:d}=P{trajectorypos[Axis.axis_num]:d}\r'.encode("utf-8")) # set the target equal to the actual position + self._control_socket.write(f'P{targetpos[axis._axis_num]:d}=P{trajectorypos[axis._axis_num]:d}\r'.encode("utf-8")) # set the target equal to the actual position self._control_socket.read_until(expected=b'>') #"P00>" - self._control_socket.write(f'DRIVE OFF {Axis.axis_name:s}\r'.encode("utf-8")) + self._control_socket.write(f'DRIVE OFF {axis.axis_name:s}\r'.encode("utf-8")) self._control_socket.read_until(expected=b'>') #"P00>" # enable multiple-move buffering on this program level @@ -729,6 +647,36 @@ def __init__(self,module_name,pyserial_url,**axis_units): self._restart_wait() pass + # .axes, .axis_unit_names, and .axis_unit_quantities + # properties are defined and implemented in the + # MotionControllerBase base class + #@property + #def axes(self): + # """Returns a list or array of axis names""" + # return list(self.axis.keys()) + + #@property + #def axis_unit_names(self): + # """Returns a list or array of axis unit names""" + # return [self.axis[axis_name.unit_name] for axis_name in self.axis ] + + #@property + #def axis_unit_quantities(self): + # """Returns a list or array of axis units (pint quantity)""" + # return [self.axis[axis_name.unit_quantity] for axis_name in self.axis ] + + # .create_group() method is defined and implemented in the + # MotionControllerBase base class + #def create_group(self,axis_name_list): + # """Create and return an axis group (instance or subclass of + # SimpleAxisGroup) based on the given list of axis names""" + # # Override this method if you have a custom group class + # # for your motion controller. + # return SimpleAxisGroup(self,axis_name_list) + + + + ## NOTE: atexit did not turn out to be neccesary #def atexit(self): # #print("acr9000: Performing atexit()") # with self._waiter_cond: @@ -740,70 +688,8 @@ def __init__(self,module_name,pyserial_url,**axis_units): # # pass - def waitall(self): - waitlist = list(self._wait_dict.keys()) - self._wait(waitlist) - pass - - r"""def _read(self): - assert(self.read_request is None) # module context locking should prevent multiple simultaneous reads - our_request=[] - with self.read_request_cond: - self.read_request=our_request - self.read_request_cond.notify() - pass - CompatibleContext=CreateCompatibleContext(self) - with CompatibleContext: # release our context lock - - with self.read_complete_cond: - self.read_complete_cond.wait() - self.read_request=None - pass - pass - (success,response)=our_request - if not success: - return None - return response - - def _abort_read(self): - assert(self.read_request is not None) - with self.read_complete_cond: - self.read_request.append(False) - self.read_request.append(None) - self.read_complete_cond.notify() - pass - pass - """ - r""" - def _reader_thread_code(self): - InitCompatibleThread(self) - rdstring=None - while True: - with self.read_request_cond: - if self.read_request is None: - self.read_request_cond.wait() - pass - read_request=self.read_request - pass - - if read_request is not None: - if rdstring is None: - rdbytes=self._control_socket.read() - rdstring=rdbytes.decode('utf-8') - pass - pass - with self.read_complete_cond: - if self.read_request is not None: - self.read_request.append(True) - self.read_request.append(rdstring) - rdstring=None - self.read_complete_cond.notify() - pass - pass - pass - pass -""" + def _waiter_thread_code(self): InitCompatibleThread(self,'_waiter_thread') @@ -864,8 +750,9 @@ def _waiter_thread_code(self): pass pass - # !!!*** Waiting prior to motion finishing does not seem to work (waits forever) - def _wait(self,axislist): + + def wait(self,axis_name_list): + """Waits for each axis named in the given list to stop moving""" self._abort_wait() try: with self._waiter_cond: @@ -887,7 +774,7 @@ def _wait(self,axislist): self._control_socket.write(f'PROG{self._spareprog:d}\r'.encode("utf-8")) self._control_socket.read_until(expected=b'>') #"P00>" - condition = "AND ".join([f"(P{trajectorypos[self.axisdict[axis_name].axis_num]:d}=P{targetpos[self.axisdict[axis_name].axis_num]:d}) " for axis_name in axislist]) + condition = "AND ".join([f"(P{trajectorypos[self.axis[axis_name]._axis_num]:d}=P{targetpos[self.axis[axis_name]._axis_num]:d}) " for axis_name in axis_name_list]) condition_line = f"{linenum:d} IF ( {condition:s}) THEN EXITFLAG={linenum:d}:GOTO 1000 \r" self._control_socket.write(condition_line.encode("utf-8")) self._control_socket.read_until(expected=b'>') #"P00>" diff --git a/dataguzzler_python/motion_controller.py b/dataguzzler_python/motion_controller.py new file mode 100644 index 0000000..479666a --- /dev/null +++ b/dataguzzler_python/motion_controller.py @@ -0,0 +1,335 @@ +class AxisBase: + """Base class for a motion controller axis providing attributes + and properties and methods you should override""" + axis_name = None # Name of this axis within its controller as a python string + unit_name=None # String representing axis default units + unit_quantity=None # Pint quantity corresponding to axis default units + parent = None # Motion controller that contains this axis + + + def wait(self): + """Wait for this axis to stop moving.""" + self.parent.wait([self.axis_name]) + pass + + @property + def waitrel(self): + """On read, returns None; on assignment, initiates a move + relative to the current position and waits for the move to + complete. Position can be a number, which is assumed to be in + the axis default units, or a Pint quantity.""" + return None + + @waitrel.setter + def waitrel(self, value): + # Value may be given as just a number, in which case + # default units are assumed, or as a pint quantity. + self.rel = value + self.wait() + pass + + @property + def waitpos(self): + """On read, returns the current axis position; on assignment + initiates the move to the specified position and waits for the + move to complete. Position can be a number, which is assumed + to be in the axis default units, or a Pint quantity.""" + return self.pos + + @waitpos.setter + def waitpos(self, value): + # Value may be given as just a number, in which case + # default units are assumed, or as a pint quantity. + self.pos = value + self.wait() + pass + + def zero(self): + """This method zeros the axis, defining the current position to be 0.0""" + raise NotImplementedError + + def moving(self): + """Returns True if the axis is moving or False if it is stopped""" + raise NotImplementedError + + @property + def rel(self): + """On read, returns None; on assignment, initiates a move + relative to the current position. Position can be a number, + which is assumed to be in the axis default units, or a Pint + quantity.""" + return None + + @rel.setter + def rel(self, value): + # Must be implemented by subclass + # Value may be given as just a number, in which case + # default units are assumed, or as a pint quantity. + raise NotImplementedError + + def cancel(self): + """Cancel any move in progress on this axis""" + raise NotImplementedError + + @property + def pos(self): + """On read, returns the current axis position; + on assignment initiates the move to the specified + position. Position can be a number, which is assumed + to be in the axis default units, or a Pint quantity.""" + raise NotImplementedError + + @pos.setter + def pos(self, value): + # Must be implemented by subclass + # Value may be given as just a number, in which case + # default units are assumed, or as a pint quantity. + raise NotImplementedError + + @property + def enabled(self): + """On read, returns True if the current axis is enabled, False + otherwise. On assignment, attempts to turn the axis on or off + according to the truth value provided (True or False).""" + raise NotImplementedError + + @enabled.setter + def enabled(self, value): + raise NotImplementedError + + pass + +class SimpleAxisGroup: + """Simple implementation of a group of axes that behaves similarly + to a single axis. Intended so that it can be subclassed when + special functionality is needed.""" + + axis_names=None # List of axis names + parent=None # Motion controller object + unit_names = None # List or array of names of axis default units + unit_quantities = None # List of axis default unit quantities + matching_units=None # True if all axes use exactly the same units + + def __init__(self, parent, axis_names): + self.parent = parent + self.axis_names = axis_names + self.unit_names = [] + self.unit_quantities = [] + + self.matching_units = True + axis0_quantity = self.parent.axis[axis_names[0]].unit_quantity + for axis_num in range(1, len(self.axis_names)): + axis_name = axis_names[axis_num] + if self.parent.axis[axis_name].unit_quantity != axis0_quantity: + self.matching_units = False + pass + pass + pass + + def wait(self): + """Wait for all axes in the group to stop moving""" + self.parent.wait(self.axis_names) + pass + + @property + def waitrel(self): + """On read, returns None; on assignment, initiates a move + relative to the current position and waits for the move to + complete. Position can be an iterable of numbers, which are + assumed to be in the axis default units, or Pint quantities. + + """ + return None + + @waitrel.setter + def waitrel(self, value): + # Value may be given as just numbers, in which case + # default units are assumed, or as pint quantities. + self.rel = value + self.wait() + pass + + @property + def waitpos(self): + """On read, returns the current axis positions; on assignment + initiates the move to the specified position and waits for the + move to complete. Position can be an iterable of numbers, + which are assumed to be in the axis default units, or Pint + quantities. + + """ + return self.pos + + @waitpos.setter + def waitpos(self, value): + # Value may be given as just numbers, in which case + # default units are assumed, or as pint quantities. + self.pos = value + self.wait() + pass + + def zero(self): + """This method zeros all the axes, defining the current + position to be 0.0""" + for axis_name in self.axis_names: + axis = self.parent.axis[axis_name] + axis.zero() + pass + pass + + + @property + def moving(self): + """Returns an array of Trues for axes that are moving and + Falses for axes that are stopped""" + moving = np.zeros(len(self.axis_names), dtype=bool) + for axis_num in range(len(self.axis_names)): + axis_name = self.axis_names[axis_num] + axis = self.parent.axis[axis_name] + moving[axis_num] = axis.moving + pass + return moving + + @property + def rel(self): + """On read, returns None; on assignment, initiates a move + relative to the current position. Position can be an iterable + of numbers, which are assumed to be in the axis default units, + or Pint quantities. + + """ + return None + + @rel.setter + def rel(self, value): + if len(value) != len(self.axis_names): + raise ValueError("Incorrect number of axis offsets given") + for axis_num in range(len(self.axis_names)): + axis_name = self.axis_names[axis_num] + axis = self.parent.axis[axis_name] + desired_rel = value[axis_num] + if desired_rel is not None and not np.isnan(desired_rel): + axis.rel=desired_rel + pass + pass + pass + + def cancel(self): + """Cancel any move in progress on all axes in this group""" + for axis_name in self.axis_names: + axis = self.parent.axis[axis_name] + axis.cancel() + pass + pass + + @property + def pos(self): + """On read, returns the current axis positions; on assignment + initiates the move to the specified position. Position can be + an iterable of numbers, which are assumed to be in the axis + default units, or Pint quantities. + + """ + ur = pint.get_application_registry() + if self.matching_units: + # Create a single numpy quantity with the correct units + pos = ur.Quantity(np.zeros(len(self.axis_names), dtype='d'), self.parent[self.axis_names[0]].unit_quantity) + pass + else: + # Create a numpy array of objects which are scalar quantities + pos = np.zeros(len(self.axis_names), dtype=object) + pass + for axis_num in range(len(self.axis_names)): + axis_name = self.axis_names[axis_num] + axis = self.parent.axis[axis_name] + pos[axis_num] = axis.pos + pass + return pos + + @pos.setter + def pos(self, value): + if len(value) != len(self.axis_names): + raise ValueError("Incorrect number of axis offsets given") + for axis_num in range(len(self.axis_names)): + axis_name = self.axis_names[axis_num] + axis = self.parent.axis[axis_name] + desired_pos = value[axis_num] + if desired_pos is not None and not np.isnan(desired_pos): + + axis.pos = desired_pos + pass + pass + pass + + @property + def enabled(self): + """On read, returns a boolean array that is true for axes that + are enabled, and False otherwise. On assignment, attempts to + turn the given axes on or off according to the truth value + provided (True or False). + + """ + enabled = np.zeros(len(self.axis_names), dtype=bool) + for axis_num in range(len(self.axis_names)): + axis_name = self.axis_names[axis_num] + axis = self.parent.axis[axis_name] + enabled[axis_num] = axis.enabled + pass + return enabled + + @enabled.setter + def enabled(self, value): + if isinstance(value, bool): + for axis_name in self.axis_names: + axis = self.parent.axis[axis_name] + axis.enabled = value + pass + return + if len(value) != len(self.axis_names): + raise ValueError("Incorrect number of axis offsets given") + for axis_num in range(len(self.axis_names)): + axis_name = self.axis_names[axis_num] + axis = self.parent.axis[axis_name] + axis.enabled = value[axis_num] + pass + pass + pass + +class MotionControllerBase: + """Base class for motion controllers providing attributes and + properties and methods you should override. Don't forget to + also use the dgpy.Module metaclass""" + + axis=None # Ordered dictionary of axis objects + all=None # axis_group object representing all axes + + # In addition each defined axis should be referenced + # by an attribute of the same name + + @property + def axes(self): + """Returns a list or array of axis names""" + return list(self.axis.keys()) + + @property + def axis_unit_names(self): + """Returns a list or array of axis unit names""" + return [self.axis[axis_name.unit_name] for axis_name in self.axis ] + + @property + def axis_unit_quantities(self): + """Returns a list or array of axis units (pint quantity)""" + return [self.axis[axis_name.unit_quantity] for axis_name in self.axis ] + + def create_group(self,axis_name_list): + """Create and return an axis group (instance or subclass of + SimpleAxisGroup) based on the given list of axis names""" + # Override this method if you have a custom group class + # for your motion controller. + return SimpleAxisGroup(self,axis_name_list) + + def wait(self,axis_name_list): + """Waits for each axis named in the given list to stop moving""" + raise NotImplementedError + + pass diff --git a/demos/acr9000.dgp b/demos/acr9000.dgp index 020db6a..e9ee6c8 100644 --- a/demos/acr9000.dgp +++ b/demos/acr9000.dgp @@ -5,4 +5,4 @@ include(dgpy,"dgpy_startup.dpi") # If you get a NameError here, be sure you are include(dgpy, "pint.dpi") from dataguzzler_python.modules.motion_control import acr9000 -mot = acr9000.acr9000("mot","socket://acr9000:5002?logging=debug",X="mm",Y="mm",Z="mm",RY="degrees") +mot = acr9000.ACR9000("mot","socket://acr9000:5002",X="mm",Y="mm",Z="mm",RY="degrees") From 6cbadf00e028cbb213aa05a915a74d6de3b2a25b Mon Sep 17 00:00:00 2001 From: Steve Holland Date: Thu, 11 Jul 2024 11:20:10 -0500 Subject: [PATCH 4/6] Fix refactored motion controller and ACR9000. --- dataguzzler_python/modules/motion_control/acr9000.py | 4 ++-- dataguzzler_python/motion_controller.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dataguzzler_python/modules/motion_control/acr9000.py b/dataguzzler_python/modules/motion_control/acr9000.py index fb36ca0..bf50c07 100644 --- a/dataguzzler_python/modules/motion_control/acr9000.py +++ b/dataguzzler_python/modules/motion_control/acr9000.py @@ -520,7 +520,7 @@ def __init__(self,module_name,pyserial_url,**axis_units): axis_name = attach_match.group(2).decode("utf-8") if axis_num < MAXAXES and len(axis_name) > 0: #Got valid attach line - unit_factor = Axis._units_to_factor(axis_units[axis_name]) + unit_factor = ACR9000Axis._units_to_factor(axis_units[axis_name]) #Extract the PPU self._control_socket.write(f'AXIS{axis_num:d} PPU\r'.encode("utf-8")) ppu_response=self._control_socket.read_until(expected=b'>') @@ -528,7 +528,7 @@ def __init__(self,module_name,pyserial_url,**axis_units): if ppu_match is None: raise ValueError(f'Bad PPU line for axis {axis_name:s}') ppu = float(ppu_match.group(1)) - axis = Axis(axis_name=axis_name, + axis = ACR9000Axis(axis_name=axis_name, _proglevel=proglevel, _axis_num=axis_num, _ppu=ppu, diff --git a/dataguzzler_python/motion_controller.py b/dataguzzler_python/motion_controller.py index 479666a..f8fbd6b 100644 --- a/dataguzzler_python/motion_controller.py +++ b/dataguzzler_python/motion_controller.py @@ -1,3 +1,6 @@ +import numpy as np +import pint + class AxisBase: """Base class for a motion controller axis providing attributes and properties and methods you should override""" From bc8baafee9a7308b819340e6d3ffb9fdd9011081 Mon Sep 17 00:00:00 2001 From: Steve Holland Date: Thu, 11 Jul 2024 11:20:41 -0500 Subject: [PATCH 5/6] Add explicit [project] to pyproject.toml --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3e5acf5..de4d308 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,11 @@ +[project] +name = 'dataguzzler-python' +dynamic = ['version'] +requires-python = ">= 3.8" + +[project.scripts] +dataguzzler-python = "dataguzzler_python.bin.dataguzzler_python:main" + [build-system] requires = ["setuptools","numpy"] build-backend = "setuptools.build_meta:__legacy__" From 8eb0a802880701e777ba7d47718416364bbfd555 Mon Sep 17 00:00:00 2001 From: Steve Holland Date: Thu, 11 Jul 2024 11:39:16 -0500 Subject: [PATCH 6/6] Add more details in pyproject.toml --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index de4d308..85cc315 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,12 @@ [project] name = 'dataguzzler-python' -dynamic = ['version'] +dynamic = ['version','authors','description','urls'] requires-python = ">= 3.8" +readme = "README.md" +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: BSD License" + ] [project.scripts] dataguzzler-python = "dataguzzler_python.bin.dataguzzler_python:main"