1# Copyright 2015 ARM Limited 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# 15from __future__ import division 16import csv 17import logging 18import collections 19 20from devlib.utils.types import numeric 21 22 23# Channel modes describe what sort of measurement the instrument supports. 24# Values must be powers of 2 25INSTANTANEOUS = 1 26CONTINUOUS = 2 27 28MEASUREMENT_TYPES = {} # populated further down 29 30 31class MeasurementType(object): 32 33 def __init__(self, name, units, category=None, conversions=None): 34 self.name = name 35 self.units = units 36 self.category = category 37 self.conversions = {} 38 if conversions is not None: 39 for key, value in conversions.iteritems(): 40 if not callable(value): 41 msg = 'Converter must be callable; got {} "{}"' 42 raise ValueError(msg.format(type(value), value)) 43 self.conversions[key] = value 44 45 def convert(self, value, to): 46 if isinstance(to, basestring) and to in MEASUREMENT_TYPES: 47 to = MEASUREMENT_TYPES[to] 48 if not isinstance(to, MeasurementType): 49 msg = 'Unexpected conversion target: "{}"' 50 raise ValueError(msg.format(to)) 51 if not to.name in self.conversions: 52 msg = 'No conversion from {} to {} available' 53 raise ValueError(msg.format(self.name, to.name)) 54 return self.conversions[to.name](value) 55 56 def __cmp__(self, other): 57 if isinstance(other, MeasurementType): 58 other = other.name 59 return cmp(self.name, other) 60 61 def __str__(self): 62 return self.name 63 64 def __repr__(self): 65 if self.category: 66 text = 'MeasurementType({}, {}, {})' 67 return text.format(self.name, self.units, self.category) 68 else: 69 text = 'MeasurementType({}, {})' 70 return text.format(self.name, self.units) 71 72 73# Standard measures 74_measurement_types = [ 75 MeasurementType('unknown', None), 76 MeasurementType('time', 'seconds', 77 conversions={ 78 'time_us': lambda x: x * 1000, 79 } 80 ), 81 MeasurementType('time_us', 'microseconds', 82 conversions={ 83 'time': lambda x: x / 1000, 84 } 85 ), 86 MeasurementType('temperature', 'degrees'), 87 88 MeasurementType('power', 'watts', 'power/energy'), 89 MeasurementType('voltage', 'volts', 'power/energy'), 90 MeasurementType('current', 'amps', 'power/energy'), 91 MeasurementType('energy', 'joules', 'power/energy'), 92 93 MeasurementType('tx', 'bytes', 'data transfer'), 94 MeasurementType('rx', 'bytes', 'data transfer'), 95 MeasurementType('tx/rx', 'bytes', 'data transfer'), 96 97 MeasurementType('frames', 'frames', 'ui render'), 98] 99for m in _measurement_types: 100 MEASUREMENT_TYPES[m.name] = m 101 102 103class Measurement(object): 104 105 __slots__ = ['value', 'channel'] 106 107 @property 108 def name(self): 109 return '{}_{}'.format(self.channel.site, self.channel.kind) 110 111 @property 112 def units(self): 113 return self.channel.units 114 115 def __init__(self, value, channel): 116 self.value = value 117 self.channel = channel 118 119 def __cmp__(self, other): 120 if isinstance(other, Measurement): 121 return cmp(self.value, other.value) 122 else: 123 return cmp(self.value, other) 124 125 def __str__(self): 126 if self.units: 127 return '{}: {} {}'.format(self.name, self.value, self.units) 128 else: 129 return '{}: {}'.format(self.name, self.value) 130 131 __repr__ = __str__ 132 133 134class MeasurementsCsv(object): 135 136 def __init__(self, path, channels=None): 137 self.path = path 138 self.channels = channels 139 self._fh = open(path, 'rb') 140 if self.channels is None: 141 self._load_channels() 142 143 def measurements(self): 144 return list(self.itermeasurements()) 145 146 def itermeasurements(self): 147 self._fh.seek(0) 148 reader = csv.reader(self._fh) 149 reader.next() # headings 150 for row in reader: 151 values = map(numeric, row) 152 yield [Measurement(v, c) for (v, c) in zip(values, self.channels)] 153 154 def _load_channels(self): 155 self._fh.seek(0) 156 reader = csv.reader(self._fh) 157 header = reader.next() 158 self._fh.seek(0) 159 160 self.channels = [] 161 for entry in header: 162 for mt in MEASUREMENT_TYPES: 163 suffix = '_{}'.format(mt) 164 if entry.endswith(suffix): 165 site = entry[:-len(suffix)] 166 measure = mt 167 name = '{}_{}'.format(site, measure) 168 break 169 else: 170 site = entry 171 measure = 'unknown' 172 name = entry 173 174 chan = InstrumentChannel(name, site, measure) 175 self.channels.append(chan) 176 177 178class InstrumentChannel(object): 179 180 @property 181 def label(self): 182 return '{}_{}'.format(self.site, self.kind) 183 184 @property 185 def kind(self): 186 return self.measurement_type.name 187 188 @property 189 def units(self): 190 return self.measurement_type.units 191 192 def __init__(self, name, site, measurement_type, **attrs): 193 self.name = name 194 self.site = site 195 if isinstance(measurement_type, MeasurementType): 196 self.measurement_type = measurement_type 197 else: 198 try: 199 self.measurement_type = MEASUREMENT_TYPES[measurement_type] 200 except KeyError: 201 raise ValueError('Unknown measurement type: {}'.format(measurement_type)) 202 for atname, atvalue in attrs.iteritems(): 203 setattr(self, atname, atvalue) 204 205 def __str__(self): 206 if self.name == self.label: 207 return 'CHAN({})'.format(self.label) 208 else: 209 return 'CHAN({}, {})'.format(self.name, self.label) 210 211 __repr__ = __str__ 212 213 214class Instrument(object): 215 216 mode = 0 217 218 def __init__(self, target): 219 self.target = target 220 self.logger = logging.getLogger(self.__class__.__name__) 221 self.channels = collections.OrderedDict() 222 self.active_channels = [] 223 self.sample_rate_hz = None 224 225 # channel management 226 227 def list_channels(self): 228 return self.channels.values() 229 230 def get_channels(self, measure): 231 if hasattr(measure, 'name'): 232 measure = measure.name 233 return [c for c in self.list_channels() if c.kind == measure] 234 235 def add_channel(self, site, measure, name=None, **attrs): 236 if name is None: 237 name = '{}_{}'.format(site, measure) 238 chan = InstrumentChannel(name, site, measure, **attrs) 239 self.channels[chan.label] = chan 240 241 # initialization and teardown 242 243 def setup(self, *args, **kwargs): 244 pass 245 246 def teardown(self): 247 pass 248 249 def reset(self, sites=None, kinds=None, channels=None): 250 if kinds is None and sites is None and channels is None: 251 self.active_channels = sorted(self.channels.values(), key=lambda x: x.label) 252 else: 253 if isinstance(sites, basestring): 254 sites = [sites] 255 if isinstance(kinds, basestring): 256 kinds = [kinds] 257 self.active_channels = [] 258 for chan_name in (channels or []): 259 try: 260 self.active_channels.append(self.channels[chan_name]) 261 except KeyError: 262 msg = 'Unexpected channel "{}"; must be in {}' 263 raise ValueError(msg.format(chan_name, self.channels.keys())) 264 for chan in self.channels.values(): 265 if (kinds is None or chan.kind in kinds) and \ 266 (sites is None or chan.site in sites): 267 self.active_channels.append(chan) 268 269 # instantaneous 270 271 def take_measurement(self): 272 pass 273 274 # continuous 275 276 def start(self): 277 pass 278 279 def stop(self): 280 pass 281 282 def get_data(self, outfile): 283 pass 284