Cleanup stepper, use pure distutils, and bump to v0.3
[stepper.git] / stepper.py
1 # Python control of stepper motors.
2
3 # Copyright (C) 2008-2011  W. Trevor King
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 import logging as _logging
19 import logging.handlers as _logging_handlers
20 from time import sleep as _sleep
21
22
23 __version__ = '0.3'
24
25
26 LOG = _logging.getLogger('stepper')
27 "Stepper logger"
28
29 LOG.setLevel(_logging.WARN)
30 _formatter = _logging.Formatter(
31     '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
32
33 _stream_handler = _logging.StreamHandler()
34 _stream_handler.setLevel(_logging.DEBUG)
35 _stream_handler.setFormatter(_formatter)
36 LOG.addHandler(_stream_handler)
37
38
39 def _binary(i, width=4):
40     """Convert `i` to a binary string of width `width`.
41
42     >>> _binary(0)
43     '0000'
44     >>> _binary(1)
45     '0001'
46     """
47     str = bin(i)[2:]
48     return '0'*(width-len(str)) + str
49
50
51 class Stepper (object):
52     """Stepper motor control
53
54     inputs:
55
56     write      (fn(value)) write the 4-bit integer `value` to
57                appropriate digital output channels.
58     full_step  (boolean) select full or half stepping
59     logic      (boolean) select active high (True) or active low (False)
60     delay      (float) time delay between steps in seconds, in case the
61                 motor response is slower than the drigital output
62                 driver.
63     step_size  (float) approximate step size in meters
64
65
66     >>> from pycomedi.device import Device
67     >>> from pycomedi.channel import DigitalChannel
68     >>> from pycomedi.constant import SUBDEVICE_TYPE, IO_DIRECTION
69
70     >>> device = Device('/dev/comedi0')
71     >>> device.open()
72
73     >>> subdevice = device.find_subdevice_by_type(SUBDEVICE_TYPE.dio)
74     >>> channels = [subdevice.channel(i, factory=DigitalChannel)
75     ...             for i in (0, 1, 2, 3)]
76     >>> for chan in channels:
77     ...     chan.dio_config(IO_DIRECTION.output)
78
79     >>> def write(value):
80     ...     subdevice.dio_bitfield(bits=value, write_mask=2**4-1)
81
82     >>> s = Stepper(write=write)
83     >>> s.position
84     0
85     >>> s.single_step(1)
86     >>> s.position
87     2
88     >>> s.single_step(1)
89     >>> s.position
90     4
91     >>> s.single_step(1)
92     >>> s.position
93     6
94     >>> s.single_step(-1)
95     >>> s.position
96     4
97     >>> s.single_step(-1)
98     >>> s.position
99     2
100     >>> s.single_step(-1)
101     >>> s.position
102     0
103     >>> s.full_step = False
104     >>> s.single_step(-1)
105     >>> s.position
106     -1
107     >>> s.single_step(-1)
108     >>> s.position
109     -2
110     >>> s.single_step(-1)
111     >>> s.position
112     -3
113     >>> s.single_step(1)
114     >>> s.position
115     -2
116     >>> s.single_step(1)
117     >>> s.position
118     -1
119     >>> s.single_step(1)
120     >>> s.position
121     0
122     >>> s.step_to(1000)
123     >>> s.position
124     1000
125     >>> s.step_to(-1000)
126     >>> s.position
127     -1000
128     >>> s.step_relative(1000)
129     >>> s.position
130     0
131
132     >>> device.close()
133     """
134     def __init__(self, write, full_step=True, logic=True, delay=1e-5,
135                  step_size=170e-9):
136         self._write = write
137         self.full_step = full_step
138         self.logic = logic
139         self.delay = delay
140         self.step_size = step_size
141         self.port_values = [1,  # binary ---1  setup for logic == True
142                             5,  # binary -1-1
143                             4,  # binary -1--
144                             6,  # binary -11-
145                             2,  # binary --1-
146                             10, # binary 1-1-
147                             8,  # binary 1---
148                             9]  # binary 1--1
149         self._set_position(0)
150
151     def _get_output(self, position):
152         """Get the port value that places the stepper in `position`.
153
154         >>> s = Stepper(write=lambda value: value, logic=True)
155         >>> _binary(s._get_output(0))
156         '0001'
157         >>> _binary(s._get_output(1))
158         '0101'
159         >>> _binary(s._get_output(2))
160         '0100'
161         >>> _binary(s._get_output(-79))
162         '0101'
163         >>> _binary(s._get_output(81))
164         '0101'
165         >>> s.logic = False
166         >>> _binary(s._get_output(0))
167         '1110'
168         >>> _binary(s._get_output(1))
169         '1010'
170         >>> _binary(s._get_output(2))
171         '1011'
172         """
173         value = self.port_values[position % len(self.port_values)]
174         if not self.logic:
175             value = 2**4 - 1 - value
176         return value
177
178     def _set_position(self, position):
179         self.position = position  # current stepper index in half steps
180         output = self._get_output(position)
181         LOG.debug('set postition to %d (%s)' % (position, _binary(output)))
182         self._write(output)
183
184     def single_step(self, direction):
185         LOG.debug('single step')
186         if self.full_step and self.position % 2 == 1:
187             self.position -= 1  # round down to a full step
188         if direction > 0:
189             step = 1
190         elif direction < 0:
191             step = -1
192         else:
193             raise ValueError(direction)  # no step
194         if self.full_step:
195             step *= 2
196         self._set_position(self.position + step)
197         if self.delay > 0:
198             _sleep(self.delay)
199
200     def step_to(self, target_position):
201         if target_position != int(target_position):
202             raise ValueError(
203                 'target_position %s must be an int' % target_position)
204         if self.full_step and target_position % 2 == 1:
205             target_position -= 1  # round down to a full step
206         if target_position > self.position:
207             direction = 1
208         else:
209             direction = -1
210         while self.position != target_position:
211             LOG.debug('stepping %s -> %s (%s)' % (target_position, self.position, direction))
212             self.single_step(direction)
213
214     def step_relative(self, relative_target_position):
215         return self.step_to(self.position + relative_target_position)