3 # Copyright (C) 2008-2011 W. Trevor King <wking@drexel.edu>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with this program. If not, see
17 # <http://www.gnu.org/licenses/>.
19 # Version 0.2, W. Trevor King, Feb. 20th, 2008
20 # (added OptionParser code and stripchart debug parameters)
21 # Version 0.3, W. Trevor King, Mar. 25th, 2010
22 # (added multiple channels option)
23 # Version 0.4, W. Trevor King, Nov. 12th, 2011
24 # Transition from optparse -> argparse and update to pycomedi 0.3.
26 """Simple analog channel polling script.
28 Spends most of its time sleeping, waking up every `dt` seconds to
29 record the current voltage.
32 import logging as _logging
33 import os.path as _os_path
38 import numpy as _numpy
39 from matplotlib import pyplot as _pyplot
40 except (ImportError, RuntimeError), _matplotlib_import_error:
43 import pycomedi.device as _pycomedi_device
44 import pycomedi.subdevice as _pycomedi_subdevice
45 import pycomedi.channel as _pycomedi_channel
46 import pycomedi.constant as _pycomedi_constant
48 from pypid.backend.melcor import MelcorBackend as _TemperatureBackend
49 except ImportError, pypid_import_error:
50 _TemperatureBackend = None
56 class Monitor (object):
57 """Take measurements on a single channel every dt seconds.
59 Save '<time>\t<bit_val>\t<physical_val>\n' records.
62 def __init__(self, channels=[0], temperature=False,
63 ambient_temperature=False, dt=4, comedi_device='/dev/comedi0',
64 data_stream=None, logger=None, plotter=None):
65 self.channel_indexes = channels
66 self.with_temperature = temperature
67 self.with_ambient_temperature = ambient_temperature
69 self.comedi_device = comedi_device
70 self.data_stream = data_stream
72 self._plotter = plotter
73 self.temperature = None
75 def log(self, level=_logging.DEBUG, msg=None):
76 assert msg is not None
78 self._logger.log(level, msg)
88 self.log(msg='setup slow bend monitor')
89 self.log(msg='chan {}, dt {}, data_stream {}'.format(
90 self.channel_indexes, self.dt, self.data_stream))
91 self._setup_channels()
92 if self.with_temperature or self.with_ambient_temperature:
93 self._setup_temperature()
96 def _setup_channels(self):
97 self.log(msg='setup input channels')
98 self.device = _pycomedi_device.Device(self.comedi_device)
100 self.subdevice = self.device.find_subdevice_by_type(
101 _pycomedi_constant.SUBDEVICE_TYPE.ai,
102 factory=_pycomedi_subdevice.StreamingSubdevice)
104 for c_index in self.channel_indexes:
105 c = self.subdevice.channel(
107 factory=_pycomedi_channel.AnalogChannel,
108 aref=_pycomedi_constant.AREF.diff)
109 c.range = c.find_range(
110 unit=_pycomedi_constant.UNIT.volt,
112 self.channels.append(c)
113 self.converters = [c.get_converter() for c in self.channels]
115 def _setup_temperature(self):
116 self.log(msg='setup temperature channel')
117 if _TemperatureBackend is None:
118 raise pypid_import_error
119 self.temperature = _TemperatureBackend()
122 self.log(msg='teardown slow bend monitor')
123 self._teardown_channels()
124 self._teardown_temperature()
126 def _teardown_channels(self):
127 self.log(msg='teardown input channels')
128 if hasattr(self, 'device') and self.device is not None:
130 self.device = self.subdevice = self.channels = None
132 def _teardown_temperature(self):
133 self.log(msg='teardown temperature channel')
134 if self.temperature is not None:
135 self.temperature.cleanup()
136 self.temperature = None
139 self.log(msg='running slow bend monitor')
140 self.start_time = _time.time()
141 next_read_time = self.start_time
142 self.log(_logging.INFO, 'press Control-c to stop acquisition')
145 tm,bitvals,physicals = self._take_and_save_reading()
146 next_read_time += self.dt
147 self._wait_until(next_read_time)
148 except KeyboardInterrupt:
151 def _take_and_save_reading(self):
153 bitvals,physicals = self._measure_voltage()
154 if self.with_temperature:
155 bitval,physical = self._measure_temperature()
156 bitvals.insert(0, bitval)
157 physicals.insert(0, physical)
158 if self.with_ambient_temperature:
159 bitval,physical = self._measure_ambient_temperature()
160 bitvals.insert(0, bitval)
161 physicals.insert(0, physical)
162 self._save_reading(tm, bitvals, physicals)
163 return (tm, bitvals, physicals)
165 def _measure_voltage(self):
166 self.log(msg='measure voltage')
167 bitvals = [c.data_read_delayed(nano_sec=1e3) for c in self.channels]
168 self.log(msg='read bit values: {}'.format(
169 '\t'.join('{:g}'.format(b) for b in bitvals)))
170 physicals = [cv.to_physical(b)
171 for cv,b in zip(self.converters, bitvals)]
172 return (bitvals, physicals)
174 def _measure_temperature(self):
175 self.log(msg='measure temperature')
176 physical = self.temperature.get_pv()
177 bitvalue = self.temperature._read('CHANNEL_1_A_D_COUNTS')
178 return (bitvalue, physical)
180 def _measure_ambient_temperature(self):
181 self.log(msg='measure ambient temperature')
182 physical = self.temperature.get_ambient_pv()
183 bitvalue = self.temperature._read('AMBIENT_A_D_COUNTS')
184 return (bitvalue, physical)
186 def _make_header(self):
187 'Create the save the data header'
188 fields = ['time (second)']
189 name_units = [('chan {}'.format(c.index), c.range.unit.name)
190 for c in self.channels]
191 if self.with_temperature:
192 name_units.insert(0, ('temperature', self.temperature.pv_units))
193 if self.with_ambient_temperature:
194 name_units.insert(0, ('ambient temperature', self.temperature.pv_units))
195 for name,unit in name_units:
196 fields.extend(['{} ({})'.format(name, u) for u in ['bit', unit]])
197 headline = '#{}'.format('\t'.join(fields))
198 self.data_stream.write(headline + '\n')
199 self.data_stream.flush()
200 self.log(_logging.INFO, headline)
202 self._plotter.add_header(fields)
204 def _save_reading(self, time, bitvals, physicals):
205 self.log(msg='save measurement')
206 dataline = '{:g}\t{}'.format(
207 time - self.start_time,
208 '\t'.join('{:d}\t{:g}'.format(b, p)
209 for b,p in zip(bitvals, physicals)))
210 self.data_stream.write(dataline + '\n')
211 self.data_stream.flush()
212 self.log(_logging.INFO, dataline)
214 self._plotter.add_points(time, bitvals, physicals)
216 def _wait_until(self, target_time):
217 self.log(msg='sleep until {}'.format(target_time))
218 dt = target_time - _time.time()
223 class Plotter (object):
224 """Matplotlib-based strip-chart
226 def __init__(self, count=100):
229 raise _matplotlib_import_error
232 def add_header(self, fields):
233 self.figure = _pyplot.figure()
234 self.channels = len(fields)/2 # integer division
237 for i in range(self.channels):
238 self.axes.append(self.figure.add_subplot(self.channels, 1, i+1))
239 self.axes[i].set_title(fields[2*i+1])
240 self.lines.append(self.axes[i].plot(
242 _numpy.zeros((self.count,), dtype=_numpy.float), 'r.'))
243 #_pyplot.show(block=False) # block is an experimental kwarg
246 def add_points(self, time, bit_values, physical_values):
247 for i in range(self.channels):
248 phys_values = self.lines[i][0].get_ydata()
249 phys_values = _numpy.roll(phys_values, -1)
250 phys_values[-1] = physical_values[i]
251 self.lines[i][0].set_ydata(phys_values)
253 self.axes[i].autoscale(axis='y')
254 self.figure.canvas.draw()
256 def _get_data_stream(data_dir=None, logger=None):
260 timestamp = _time.strftime('%Y-%m-%d_%H-%M-%S')
261 data_path = _os_path.join(args.data_dir, timestamp)
262 if not _os_path.exists(data_path):
266 '{} already exists, wait a second and try again'.format(
268 _time.sleep(1) # try the next second
269 return open(data_path, 'w')
271 def _get_logger(level=_logging.DEBUG):
272 logger = _logging.getLogger('slow_bend')
273 logger.setLevel(level)
274 ch = _logging.StreamHandler()
276 formatter = _logging.Formatter(
277 '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
278 ch.setFormatter(formatter)
279 logger.addHandler(ch)
283 if __name__ == '__main__':
284 from argparse import ArgumentParser
286 parser = ArgumentParser(description=__doc__)
288 '--version', action='version', version=__version__)
290 '-t', '--timestep', dest='dt', type=float, default=4.0,
291 help='Measure voltage evert DT seconds (default: %(default)s)')
293 dest='channels', nargs='+', metavar='CHANNEL', type=int,
294 help='Input voltage CHANNEL(S)')
296 '-d', '--data-directory', dest='data_dir', metavar='DATA_DIR',
297 help='Write output to subdir of DATA_DIR')
299 '-T', '--temperature', dest='temperature',
300 default=False, action='store_const', const=True,
301 help='Also record the temperature (thermocouple)')
303 '-a', '--ambient-temperature', dest='ambient_temperature',
304 default=False, action='store_const', const=True,
305 help='Also record the ambient (room) temperature')
307 '-p', '--plot', dest='plot',
308 default=False, action='store_const', const=True,
309 help='Display recorded physical values on a strip-chart')
311 '-v', '--verbose', dest='verbose',
312 default=0, action='count',
313 help='Print debugging information (repeat to increase verbosity)')
315 args = parser.parse_args()
319 level = _logging.DEBUG
321 level = _logging.INFO
322 logger = _get_logger(level=level)
330 data_stream = _get_data_stream(data_dir=args.data_dir, logger=logger)
333 channels=args.channels, temperature=args.temperature,
334 ambient_temperature=args.ambient_temperature, dt=args.dt,
335 data_stream=data_stream, logger=logger, plotter=plotter)
338 if data_stream != _sys.stdout: