308 lines
11 KiB
Python
308 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
# *****************************************************************************
|
|
# This program is free software; you can redistribute it and/or modify it under
|
|
# the terms of the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 2 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Module authors:
|
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
# *****************************************************************************
|
|
"""senis hall sensor"""
|
|
|
|
|
|
import threading
|
|
import time
|
|
|
|
import numpy as np
|
|
from serial import Serial
|
|
|
|
from frappy.core import Attached, BoolType, FloatRange, IntRange, \
|
|
Parameter, Property, Readable, StringType, TupleOf
|
|
|
|
|
|
class Temperature(Readable):
|
|
enablePoll = False
|
|
|
|
value = Parameter(datatype=FloatRange(unit='degC'))
|
|
|
|
|
|
class Bcomp(Readable):
|
|
enablePoll = False
|
|
|
|
value = Parameter(datatype=FloatRange(unit='T'))
|
|
range = Parameter('working range', FloatRange(unit='T'), default=0)
|
|
|
|
|
|
class Raw(Readable):
|
|
enablePoll = False
|
|
|
|
value = Parameter(datatype=FloatRange())
|
|
|
|
|
|
class TeslameterBase(Readable):
|
|
"""code for both models
|
|
|
|
the protocol is somewhat weird as the read command 'B' initiates a permanent update
|
|
which has to be stopped in between the value polls and for other communication.
|
|
|
|
the B components (and temperatures for 3MH6) are implemented as separate modules
|
|
"""
|
|
x = Attached()
|
|
y = Attached()
|
|
z = Attached()
|
|
|
|
value = Parameter('B vector',
|
|
datatype=TupleOf(FloatRange(unit='T'), FloatRange(unit='T'), FloatRange(unit='T')))
|
|
usb = Parameter('usb device', StringType(), readonly=False)
|
|
enabled = Parameter('enable data acq', datatype=BoolType(), readonly=False, default=True)
|
|
nsample = Parameter('number of samples for average', datatype=IntRange(1, 1000), readonly=False, default=1)
|
|
|
|
def init_serial(self, baud):
|
|
self._conn = Serial(self.usb, baud, timeout=0.1)
|
|
self._lock = threading.Lock()
|
|
self.stop_reading()
|
|
|
|
def write_bytes(self, msg):
|
|
with self._lock:
|
|
self._conn.write(msg)
|
|
|
|
def read_bytes(self, cnt):
|
|
with self._lock:
|
|
return self._conn.read(cnt)
|
|
|
|
def stop_reading(self):
|
|
self.write_bytes(b'S')
|
|
self.read_bytes(9999) # swallow bytes until timeout
|
|
|
|
def write_enabled(self, value):
|
|
if value:
|
|
self.status = self.Status.IDLE, ''
|
|
else:
|
|
self.status = self.Status.DISABLED, 'disabled'
|
|
self._x.status = self._y.status = self._z.status = self.status
|
|
return value
|
|
|
|
|
|
class Teslameter3MH3(TeslameterBase):
|
|
"""simpler model without temperature and auto range
|
|
|
|
remark: no query for the sample rate is possible, therefore set always to
|
|
a default rate (therefore initwrite=True on the rate parameter)
|
|
"""
|
|
range = Property('full scale', datatype=FloatRange(), default=2)
|
|
|
|
def earlyInit(self):
|
|
self.init_serial(115200)
|
|
self.write_bytes(b'C') # put into calibrated mode
|
|
if self.read_bytes(1) != b'!':
|
|
self.log.error('missing response to C command')
|
|
self.write_bytes(b'A\x80\r') # set to 90 SPS
|
|
self.read_bytes(1) # wait 0.1 sec as we get no reply
|
|
|
|
def read_value(self):
|
|
if not self.enabled:
|
|
return self.value
|
|
s = self._conn
|
|
s.timeout = 0.1 + 0.02 * self.nsample
|
|
for _ in range(2):
|
|
self.write_bytes(b'B')
|
|
# t = time.time()
|
|
reply = self.read_bytes(8 * self.nsample)
|
|
s.timeout = 0.1
|
|
self.stop_reading()
|
|
remainder = len(reply) % 8
|
|
if remainder:
|
|
reply = reply[:-remainder]
|
|
if not reply:
|
|
continue
|
|
data = np.frombuffer(reply, dtype='i1,3<i2,i1')
|
|
# first byte must be 'B' and last byte must be CR
|
|
if np.all(data['f0'] == ord(b'B')) and np.all(data['f2'] == 13):
|
|
break
|
|
else:
|
|
self.status = self.Status.ERROR, 'bad reply'
|
|
raise ValueError('bad reply')
|
|
self.status = self.Status.IDLE, ''
|
|
value = np.average(data['f1'], axis=0) * self.range / 20000.
|
|
self._x.value, self._y.value, self._z.value = value
|
|
self._x.range = self._y.range =self._z.range = self.range
|
|
return value
|
|
|
|
|
|
class Teslameter3MH6(TeslameterBase):
|
|
"""luxury model with probe and box temperature and autorange"""
|
|
x_direct = Attached()
|
|
y_direct = Attached()
|
|
z_direct = Attached()
|
|
probe_temp = Attached()
|
|
box_temp = Attached()
|
|
probe_temp_direct = Attached()
|
|
box_temp_direct = Attached()
|
|
|
|
range = Parameter('range or 0 for autorange', FloatRange(0, 20, unit='T'), readonly=False, default=0)
|
|
rate = Parameter('sampling rate', datatype=FloatRange(10, 15000, unit='Hz'),
|
|
readonly=False)
|
|
avtime = Parameter('data acquisition time', FloatRange(), default=0)
|
|
|
|
SAMPLING_RATES = {0xe0: 15000, 0xd0: 7500, 0xc0: 3750, 0xb0: 2000, 0xa1: 1000,
|
|
0x92: 500, 0x82: 100, 0x72: 60, 0x63: 50, 0x53: 30, 0x23: 10}
|
|
RANGES = dict(zip(b'1234', [0.1, 0.5, 2, 20]))
|
|
|
|
def earlyInit(self):
|
|
self.init_serial(3000000)
|
|
self.write_rate(10)
|
|
|
|
def get_data(self):
|
|
for _ in range(2):
|
|
self.write_bytes(b'B')
|
|
reply = self.read_bytes(25 * self.nsample)
|
|
self.stop_reading()
|
|
remainder = len(reply) % 25
|
|
if remainder:
|
|
reply = reply[:-remainder]
|
|
if not reply:
|
|
continue
|
|
chk = np.frombuffer(reply, dtype='i1,23i1,i1')
|
|
if not np.all(np.sum(chk['f1'], axis=1) % 256 == 0):
|
|
status = 'checksum error'
|
|
continue
|
|
# first byte must be 'B' and last byte must be CR
|
|
if np.all(chk['f0'] == ord(b'B')) and np.all(chk['f2'] == 13):
|
|
break
|
|
status = 'bad reply'
|
|
else:
|
|
self.status = self.Status.ERROR, status
|
|
raise ValueError(status)
|
|
self.status = self.Status.IDLE, ''
|
|
return reply
|
|
|
|
def read_value(self):
|
|
if not self.enabled:
|
|
return self.value
|
|
t0 = time.time()
|
|
s = self._conn
|
|
s.timeout = 0.1 + self.nsample / self.rate
|
|
self.write_bytes(b'C') # put into calibrated mode
|
|
if self.read_bytes(1) != b'c':
|
|
self.log.error('missing response to C command')
|
|
reply = self.get_data()
|
|
data = np.frombuffer(reply,
|
|
dtype=[('_head', 'i1'),
|
|
('x', '>f4'), ('thc', '>f4'), ('y', '>f4'), ('z', '>f4'),
|
|
('_ted', '>i2'), ('tec', '>f4'), ('_tail', 'i2')])
|
|
|
|
mean = {}
|
|
for key in data.dtype.fields:
|
|
if not key.startswith('_'):
|
|
mean[key] = np.average(data[key])
|
|
|
|
self._x.value = mean['x'] * 0.001
|
|
self._y.value = mean['y'] * 0.001
|
|
self._z.value = mean['z'] * 0.001
|
|
self._probe_temp.value = mean['thc']
|
|
self._box_temp.value = mean['tec']
|
|
|
|
self.write_bytes(b'D') # put into NONcalibrated mode
|
|
if self.read_bytes(1) != b'd':
|
|
self.log.error('missing response to D command')
|
|
reply = self.get_data()
|
|
data = np.frombuffer(reply,
|
|
dtype=[('_head', 'i1'),
|
|
('x', '>i4'), ('thc', '>i4'), ('y', '>i4'), ('z', '>i4'),
|
|
('_ted', '>i2'), ('tec', '>i4'), ('_tail', 'i2')])
|
|
for key in data.dtype.fields:
|
|
if not key.startswith('_'):
|
|
mean[key] = np.average(data[key])
|
|
|
|
self._x_direct.value = mean['x']
|
|
self._y_direct.value = mean['y']
|
|
self._z_direct.value = mean['z']
|
|
self._probe_temp_direct.value = mean['thc']
|
|
self._box_temp_direct.value = mean['tec'] * 0.01
|
|
|
|
self.avtime = time.time() - t0
|
|
return self._x.value, self._y.value, self._z.value
|
|
|
|
def get_rate_code(self, value):
|
|
for rate_code, sr in sorted(self.SAMPLING_RATES.items(), key=lambda kv: kv[1]):
|
|
if value < sr * 1.1:
|
|
break
|
|
return sr, rate_code
|
|
|
|
def write_rate(self, value):
|
|
sr, code = self.get_rate_code(value)
|
|
for _ in range(2):
|
|
self.write_bytes(b'K%2.2x' % code)
|
|
if self.read_bytes(2) == b'k%c' % code:
|
|
break
|
|
self.stop_reading()
|
|
else:
|
|
raise ValueError('bad response from rate command')
|
|
return sr
|
|
|
|
def read_rate(self):
|
|
self.write_bytes(b'K?')
|
|
reply = self.read_bytes(2)
|
|
if reply[0:1] != b'k':
|
|
raise ValueError('bad response from rate query')
|
|
return self.SAMPLING_RATES[reply[1]]
|
|
|
|
def read_range(self):
|
|
self.write_bytes(b'amr?')
|
|
reply = self.read_bytes(5)
|
|
if reply == b'arng:':
|
|
ranges = [self.RANGES[c] for c in self.read_bytes(3)]
|
|
result = 0
|
|
elif reply == b'mrng:':
|
|
ranges = [self.RANGES[self.read_bytes(1)[0]]] * 3
|
|
result = ranges[0]
|
|
else:
|
|
raise ValueError('bad reply to range query %s' % repr(reply))
|
|
self._x.range, self._y.range, self._z.range = ranges
|
|
return result
|
|
|
|
def write_range(self, value):
|
|
status = None
|
|
for _ in range(2):
|
|
if status:
|
|
self.stop_reading()
|
|
try:
|
|
rng = self.read_range()
|
|
except ValueError:
|
|
status = 'can not read range'
|
|
continue
|
|
if value == rng:
|
|
return value
|
|
if value == 0:
|
|
self.write_bytes(b'T')
|
|
if self.read_bytes(3) != b'T-1':
|
|
status = 'bad reply to auto range command'
|
|
continue
|
|
return 0
|
|
if rng == 0:
|
|
self.write_bytes(b'T')
|
|
if self.read_bytes(3) != b'T-0':
|
|
status = 'bad reply to toggle manual range command'
|
|
continue
|
|
for code, rng in sorted(self.RANGES.items()):
|
|
if value < rng * 1.1:
|
|
break
|
|
self.write_bytes(b'mr%c' % code)
|
|
reply = self.read_bytes(6)
|
|
if reply != b'mrng:%c' % code:
|
|
status = 'bad reply to manual range command %s' % repr(reply)
|
|
continue
|
|
return rng
|
|
raise ValueError(status)
|