mkogg.py: Fix 'self.get_mp4_metadata(self, source)'
[blog.git] / posts / slow_bend / slow_bend.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2008-2011 W. Trevor King <wking@drexel.edu>
4 #
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.
9 #
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.
14 #
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/>.
18
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.
25
26 """Simple analog channel polling script.
27
28 Spends most of its time sleeping, waking up every `dt` seconds to
29 record the current voltage.
30 """
31
32 import logging as _logging
33 import os.path as _os_path
34 import sys as _sys
35 import time as _time
36
37 try:
38     import numpy as _numpy
39     from matplotlib import pyplot as _pyplot
40 except (ImportError, RuntimeError), _matplotlib_import_error:
41     _pyplot = None
42
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
47 try:
48     from pypid.backend.melcor import MelcorBackend as _TemperatureBackend
49 except ImportError, pypid_import_error:
50     _TemperatureBackend = None
51
52
53 __version__ = '0.4'
54
55
56 class Monitor (object):
57     """Take measurements on a single channel every dt seconds.
58
59     Save '<time>\t<bit_val>\t<physical_val>\n' records.
60     """
61
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
68         self.dt = dt
69         self.comedi_device = comedi_device
70         self.data_stream = data_stream
71         self._logger = logger
72         self._plotter = plotter
73         self.temperature = None
74
75     def log(self, level=_logging.DEBUG, msg=None):
76         assert msg is not None
77         if self._logger:
78             self._logger.log(level, msg)
79
80     def run(self):
81         try:
82             self._setup()
83             self._run()
84         finally:
85             self._teardown()
86
87     def _setup(self):
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()
94         self._make_header()
95
96     def _setup_channels(self):
97         self.log(msg='setup input channels')
98         self.device = _pycomedi_device.Device(self.comedi_device)
99         self.device.open()
100         self.subdevice = self.device.find_subdevice_by_type(
101             _pycomedi_constant.SUBDEVICE_TYPE.ai,
102             factory=_pycomedi_subdevice.StreamingSubdevice)
103         self.channels = []
104         for c_index in self.channel_indexes:
105             c = self.subdevice.channel(
106                 c_index,
107                 factory=_pycomedi_channel.AnalogChannel,
108                 aref=_pycomedi_constant.AREF.diff)
109             c.range = c.find_range(
110                 unit=_pycomedi_constant.UNIT.volt,
111                 min=-10, max=10)
112             self.channels.append(c)
113         self.converters = [c.get_converter() for c in self.channels]
114
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()
120
121     def _teardown(self):
122         self.log(msg='teardown slow bend monitor')
123         self._teardown_channels()
124         self._teardown_temperature()
125
126     def _teardown_channels(self):
127         self.log(msg='teardown input channels')
128         if hasattr(self, 'device') and self.device is not None:
129             self.device.close()
130             self.device = self.subdevice = self.channels = None
131
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
137
138     def _run(self):
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')
143         try:
144             while True:
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:
149             pass
150
151     def _take_and_save_reading(self):
152         tm = _time.time()
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)
164
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)
173
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)
179
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)
185
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)
201         if self._plotter:
202             self._plotter.add_header(fields)
203
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)
213         if self._plotter:
214             self._plotter.add_points(time, bitvals, physicals)
215
216     def _wait_until(self, target_time):
217         self.log(msg='sleep until {}'.format(target_time))
218         dt = target_time - _time.time()
219         if dt > 0:
220             _time.sleep(dt)
221
222
223 class Plotter (object):
224     """Matplotlib-based strip-chart
225     """
226     def __init__(self, count=100):
227         self.count = count
228         if _pyplot is None:
229             raise _matplotlib_import_error
230         _pyplot.ion()
231
232     def add_header(self, fields):
233         self.figure = _pyplot.figure()
234         self.channels = len(fields)/2  # integer division
235         self.axes = []
236         self.lines = []
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(
241                     range(self.count),
242                     _numpy.zeros((self.count,), dtype=_numpy.float), 'r.'))
243         #_pyplot.show(block=False)  # block is an experimental kwarg
244         _pyplot.show()
245
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)
252             self.axes[i].relim()
253             self.axes[i].autoscale(axis='y')
254         self.figure.canvas.draw()
255
256 def _get_data_stream(data_dir=None, logger=None):
257     if data_dir is None:
258         return _sys.stdout
259     while True:
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):
263             break
264         if logger:
265             logger.warning(
266                 '{} already exists, wait a second and try again'.format(
267                     data_path))
268         _time.sleep(1)  # try the next second
269     return open(data_path, 'w')
270
271 def _get_logger(level=_logging.DEBUG):
272     logger = _logging.getLogger('slow_bend')
273     logger.setLevel(level)
274     ch = _logging.StreamHandler()
275     ch.setLevel(level)
276     formatter = _logging.Formatter(
277         '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
278     ch.setFormatter(formatter)
279     logger.addHandler(ch)
280     return logger
281
282
283 if __name__ == '__main__':
284     from argparse import ArgumentParser
285
286     parser = ArgumentParser(description=__doc__)
287     parser.add_argument(
288         '--version', action='version', version=__version__)
289     parser.add_argument(
290         '-t', '--timestep', dest='dt', type=float, default=4.0,
291         help='Measure voltage evert DT seconds (default: %(default)s)')
292     parser.add_argument(
293         dest='channels', nargs='+', metavar='CHANNEL', type=int,
294         help='Input voltage CHANNEL(S)')
295     parser.add_argument(
296         '-d', '--data-directory', dest='data_dir', metavar='DATA_DIR',
297         help='Write output to subdir of DATA_DIR')
298     parser.add_argument(
299         '-T', '--temperature', dest='temperature',
300         default=False, action='store_const', const=True,
301         help='Also record the temperature (thermocouple)')
302     parser.add_argument(
303         '-a', '--ambient-temperature', dest='ambient_temperature',
304         default=False, action='store_const', const=True,
305         help='Also record the ambient (room) temperature')
306     parser.add_argument(
307         '-p', '--plot', dest='plot',
308         default=False, action='store_const', const=True,
309         help='Display recorded physical values on a strip-chart')
310     parser.add_argument(
311         '-v', '--verbose', dest='verbose',
312         default=0, action='count',
313         help='Print debugging information (repeat to increase verbosity)')
314
315     args = parser.parse_args()
316
317     if args.verbose:
318         if args.verbose > 1:
319             level = _logging.DEBUG
320         else:
321             level = _logging.INFO
322         logger = _get_logger(level=level)
323     else:
324         logger = None
325     if args.plot:
326         plotter = Plotter()
327     else:
328         plotter = None
329
330     data_stream = _get_data_stream(data_dir=args.data_dir, logger=logger)
331     try:
332         m = Monitor(
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)
336         m.run()
337     finally:
338         if data_stream != _sys.stdout:
339             data_stream.close()