├── .bandit ├── .gitignore ├── .travis.yml ├── 99-pslab.rules ├── LICENSE ├── PSL ├── Peripherals.py ├── README.md ├── SENSORS │ ├── AD7718_class.py │ ├── AD9833.py │ ├── ADS1115.py │ ├── BH1750.py │ ├── BMP180.py │ ├── ComplementaryFilter.py │ ├── HMC5883L.py │ ├── Kalman.py │ ├── MF522.py │ ├── MLX90614.py │ ├── MPU6050.py │ ├── MPU925x.py │ ├── SHT21.py │ ├── SSD1306.py │ ├── Sx1276.py │ ├── TSL2561.py │ ├── __init__.py │ └── supported.py ├── __init__.py ├── achan.py ├── analyticsClass.py ├── commands_proto.py ├── digital_channel.py ├── logic_analyzer.py ├── oscilloscope.py ├── packet_handler.py ├── sciencelab.py └── sensorlist.py ├── README.md ├── docs ├── Makefile ├── PSL.SENSORS.rst ├── PSL.rst ├── conf.py ├── images │ ├── IMG_20160618_011156_HDR.jpg │ ├── SplashNotConnected.png │ ├── advanced controls.png │ ├── controlPanelNotConnected.png │ ├── controlpanel.png │ ├── datastreaming.png │ ├── lissajous1.png │ ├── lissajous2.png │ ├── logicanalyzer.png │ ├── psl2.jpg │ ├── pslab.png │ ├── pslab.svg │ ├── pslaboscilloscope.png │ ├── pslpcb.jpg │ ├── sensordataloger.png │ ├── sinewaveonoscilloscope.png │ ├── splash.png │ ├── squarewave.png │ ├── wiki_images │ │ ├── lubuntu_logo.png │ │ ├── ubuntu_logo.png │ │ ├── window_logo.png │ │ └── xubuntu_logo.png │ ├── wirelesssensordataloger.png │ └── with fossasia logo sticker .jpg ├── index.rst └── make.bat ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── recordings └── logic_analyzer │ ├── test_capture_four_channels.json │ ├── test_capture_four_low_frequency.json │ ├── test_capture_four_lower_frequency.json │ ├── test_capture_four_lowest_frequency.json │ ├── test_capture_four_rising_edges.json │ ├── test_capture_four_too_low_frequency.json │ ├── test_capture_nonblocking.json │ ├── test_capture_one_channel.json │ ├── test_capture_rising_edges.json │ ├── test_capture_sixteen_rising_edges.json │ ├── test_capture_too_many_channels.json │ ├── test_capture_too_many_events.json │ ├── test_capture_two_channels.json │ ├── test_count_pulses.json │ ├── test_get_states.json │ ├── test_get_xy_falling_capture.json │ ├── test_get_xy_falling_trigger.json │ ├── test_get_xy_rising_capture.json │ ├── test_get_xy_rising_trigger.json │ ├── test_measure_duty_cycle.json │ ├── test_measure_frequency.json │ ├── test_measure_frequency_firmware.json │ ├── test_measure_interval.json │ ├── test_measure_interval_same_channel.json │ ├── test_measure_interval_same_channel_any.json │ ├── test_measure_interval_same_channel_four_rising.json │ ├── test_measure_interval_same_channel_same_event.json │ ├── test_measure_interval_same_channel_sixteen_rising.json │ └── test_stop.json ├── test_achan.py ├── test_analytics.py ├── test_logic_analyzer.py ├── test_oscilloscope.py └── test_packet_handler.py /.bandit: -------------------------------------------------------------------------------- 1 | # codacy: 2 | # Don't flag valid Python "assert" statements 3 | skips: ['B101'] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | __pycache__ 4 | __PYCACHE__ 5 | build 6 | _* 7 | PSL.egg-info 8 | .idea/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | - "3.7" 6 | - "3.8" 7 | 8 | before_install: 9 | - sudo apt-get -qq update 10 | - sudo mkdir -p /builds 11 | - sudo chmod a+rw /builds 12 | 13 | install: 14 | - pip3 install coverage flake8 -r requirements.txt 15 | - flake8 . --select=E9,F63,F7,F82 --show-source --statistics 16 | - sudo python setup.py install 17 | 18 | script: coverage run -m pytest tests 19 | -------------------------------------------------------------------------------- /99-pslab.rules: -------------------------------------------------------------------------------- 1 | #Rules for TestBench 2 | 3 | SUBSYSTEM=="tty",ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="00df", MODE="666",SYMLINK+="TestBench" 4 | ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="00df", ENV{ID_MM_DEVICE_IGNORE}="1" 5 | -------------------------------------------------------------------------------- /PSL/SENSORS/AD7718_class.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import time 4 | 5 | import numpy as np 6 | 7 | ''' 8 | The running directory must have caldata.py containing dictionary called 'calibs' in it. 9 | The entries of the dictionary will be of the form 10 | calibs={ 11 | 'AIN6AINCOM':[6.993123e-07,-1.563294e-06,9.994211e-01,-4.596018e-03], 12 | ... 13 | } 14 | 15 | ''' 16 | 17 | 18 | def _bv(x): 19 | return 1 << x 20 | 21 | 22 | class AD7718: 23 | VREF = 3.3 24 | 25 | STATUS = 0 26 | MODE = 1 27 | ADCCON = 2 28 | FILTER = 3 29 | ADCDATA = 4 30 | ADCOFFSET = 5 31 | ADCGAIN = 6 32 | IOCON = 7 33 | TEST1 = 12 34 | TEST2 = 13 35 | ID = 15 36 | # bit definitions 37 | MODE_PD = 0 38 | MODE_IDLE = 1 39 | MODE_SINGLE = 2 40 | MODE_CONT = 3 41 | MODE_INT_ZEROCAL = 4 42 | MODE_INT_FULLCAL = 5 43 | MODE_SYST_ZEROCAL = 6 44 | MODE_SYST_FULLCAL = 7 45 | 46 | MODE_OSCPD = _bv(3) 47 | MODE_CHCON = _bv(4) 48 | MODE_REFSEL = _bv(5) 49 | MODE_NEGBUF = _bv(6) 50 | MODE_NOCHOP = _bv(7) 51 | 52 | CON_AIN1AINCOM = 0 << 4 53 | CON_AIN2AINCOM = 1 << 4 54 | CON_AIN3AINCOM = 2 << 4 55 | CON_AIN4AINCOM = 3 << 4 56 | CON_AIN5AINCOM = 4 << 4 57 | CON_AIN6AINCOM = 5 << 4 58 | CON_AIN7AINCOM = 6 << 4 59 | CON_AIN8AINCOM = 7 << 4 60 | CON_AIN1AIN2 = 8 << 4 61 | CON_AIN3AIN4 = 9 << 4 62 | CON_AIN5AIN6 = 10 << 4 63 | CON_AIN7AIN8 = 11 << 4 64 | CON_AIN2AIN2 = 12 << 4 65 | CON_AINCOMAINCOM = 13 << 4 66 | CON_REFINREFIN = 14 << 4 67 | CON_OPEN = 15 << 4 68 | CON_UNIPOLAR = _bv(3) 69 | 70 | CON_RANGE0 = 0 # +-20mV 71 | CON_RANGE1 = 1 # +-40mV 72 | CON_RANGE2 = 2 # +-80mV 73 | CON_RANGE3 = 3 # +-160mV 74 | CON_RANGE4 = 4 # +-320mV 75 | CON_RANGE5 = 5 # +-640mV 76 | CON_RANGE6 = 6 # +-1280mV 77 | CON_RANGE7 = 7 # +-2560mV 78 | gain = 1 79 | CHAN_NAMES = ['AIN1AINCOM', 'AIN2AINCOM', 'AIN3AINCOM', 'AIN4AINCOM', 'AIN5AINCOM', 80 | 'AIN6AINCOM', 'AIN7AINCOM', 'AIN8AINCOM'] 81 | 82 | def __init__(self, I, calibs): 83 | self.cs = 'CS1' 84 | self.I = I 85 | self.calibs = calibs 86 | self.I.SPI.set_parameters(2, 1, 0, 1) 87 | self.writeRegister(self.FILTER, 20) 88 | self.writeRegister(self.MODE, self.MODE_SINGLE | self.MODE_CHCON | self.MODE_REFSEL) 89 | self.caldata = {} 90 | for a in calibs.keys(): 91 | self.caldata[a] = np.poly1d(calibs[a]) 92 | print('Loaded calibration', self.caldata) 93 | 94 | def start(self): 95 | self.I.SPI.start(self.cs) 96 | 97 | def stop(self): 98 | self.I.SPI.stop(self.cs) 99 | 100 | def send8(self, val): 101 | return self.I.SPI.send8(val) 102 | 103 | def send16(self, val): 104 | return self.I.SPI.send16(val) 105 | 106 | def write(self, regname, value): 107 | pass 108 | 109 | def readRegister(self, regname): 110 | self.start() 111 | val = self.send16(0x4000 | (regname << 8)) 112 | self.stop() 113 | # print (regname,val) 114 | val &= 0x00FF 115 | return val 116 | 117 | def readData(self): 118 | self.start() 119 | val = self.send16(0x4000 | (self.ADCDATA << 8)) 120 | val &= 0xFF 121 | val <<= 16 122 | val |= self.send16(0x0000) 123 | self.stop() 124 | return val 125 | 126 | def writeRegister(self, regname, value): 127 | self.start() 128 | val = self.send16(0x0000 | (regname << 8) | value) 129 | self.stop() 130 | return val 131 | 132 | def internalCalibration(self, chan=1): 133 | self.start() 134 | val = self.send16(0x0000 | (self.ADCCON << 8) | (chan << 4) | 7) # range=7 135 | 136 | start_time = time.time() 137 | caldone = False 138 | val = self.send16(0x0000 | (self.MODE << 8) | 4) 139 | while caldone != 1: 140 | time.sleep(0.5) 141 | caldone = self.send16(0x4000 | (self.MODE << 8)) & 7 142 | print('waiting for zero scale calibration... %.2f S, %d' % (time.time() - start_time, caldone)) 143 | 144 | print('\n') 145 | caldone = False 146 | val = self.send16(0x0000 | (self.MODE << 8) | 5) 147 | while caldone != 1: 148 | time.sleep(0.5) 149 | caldone = self.send16(0x4000 | (self.MODE << 8)) & 7 150 | print('waiting for full scale calibration... %.2f S %d' % (time.time() - start_time, caldone)) 151 | 152 | print('\n') 153 | 154 | self.stop() 155 | 156 | def readCalibration(self): 157 | self.start() 158 | off = self.send16(0x4000 | (self.ADCOFFSET << 8)) 159 | off &= 0xFF 160 | off <<= 16 161 | off |= self.send16(0x0000) 162 | 163 | gn = self.send16(0x4000 | (self.ADCGAIN << 8)) 164 | gn &= 0xFF 165 | gn <<= 16 166 | gn |= self.send16(0x0000) 167 | self.stop() 168 | return off, gn 169 | 170 | def configADC(self, adccon): 171 | self.writeRegister(self.ADCCON, adccon) # unipolar channels , range 172 | self.gain = 2 ** (7 - adccon & 3) 173 | 174 | def printstat(self): 175 | stat = self.readRegister(self.STATUS) 176 | P = ['PLL LOCKED', 'RES', 'RES', 'ADC ERROR', 'RES', 'CAL DONE', 'RES', 'READY'] 177 | N = ['PLL ERROR', 'RES', 'RES', 'ADC OKAY', 'RES', 'CAL LOW', 'RES', 'NOT READY'] 178 | s = '' 179 | for a in range(8): 180 | if stat & (1 << a): 181 | s += '\t' + P[a] 182 | else: 183 | s += '\t' + N[a] 184 | print(stat, s) 185 | 186 | def convert_unipolar(self, x): 187 | return (1.024 * self.VREF * x) / (self.gain * 2 ** 24) 188 | 189 | def convert_bipolar(self, x): 190 | return ((x / 2 ** 24) - 1) * (1.024 * self.VREF) / (self.gain) 191 | 192 | def __startRead__(self, chan): 193 | if chan not in self.CHAN_NAMES: 194 | print('invalid channel name. try AIN1AINCOM') 195 | return False 196 | chanid = self.CHAN_NAMES.index(chan) 197 | self.configADC(self.CON_RANGE7 | self.CON_UNIPOLAR | (chanid << 4)) 198 | self.writeRegister(self.MODE, self.MODE_SINGLE | self.MODE_CHCON | self.MODE_REFSEL) 199 | return True 200 | 201 | def __fetchData__(self, chan): 202 | while True: 203 | stat = self.readRegister(self.STATUS) 204 | if stat & 0x80: 205 | data = float(self.readData()) 206 | data = self.convert_unipolar(data) 207 | if int(chan[3]) > 4: data = (data - 3.3 / 2) * 4 208 | return self.caldata[chan](data) 209 | else: 210 | time.sleep(0.1) 211 | print('increase delay') 212 | return False 213 | 214 | def readVoltage(self, chan): 215 | if not self.__startRead__(chan): 216 | return False 217 | time.sleep(0.15) 218 | return self.__fetchData__(chan) 219 | 220 | def __fetchRawData__(self, chan): 221 | while True: 222 | stat = self.readRegister(self.STATUS) 223 | if stat & 0x80: 224 | data = float(self.readData()) 225 | return self.convert_unipolar(data) 226 | else: 227 | time.sleep(0.01) 228 | print('increase delay') 229 | return False 230 | 231 | def readRawVoltage(self, chan): 232 | if not self.__startRead__(chan): 233 | return False 234 | time.sleep(0.15) 235 | return self.__fetchRawData__(chan) 236 | 237 | 238 | if __name__ == "__main__": 239 | from PSL import sciencelab 240 | 241 | I = sciencelab.connect() 242 | calibs = { 243 | 'AIN6AINCOM': [6.993123e-07, -1.563294e-06, 9.994211e-01, -4.596018e-03], 244 | 'AIN7AINCOM': [3.911521e-07, -1.706405e-06, 1.002294e+00, -1.286302e-02], 245 | 'AIN3AINCOM': [-3.455831e-06, 2.861689e-05, 1.000195e+00, 3.802349e-04], 246 | 'AIN1AINCOM': [8.220199e-05, -4.587100e-04, 1.001015e+00, -1.684517e-04], 247 | 'AIN5AINCOM': [-1.250787e-07, -9.203838e-07, 1.000299e+00, -1.262684e-03], 248 | 'AIN2AINCOM': [5.459186e-06, -1.749624e-05, 1.000268e+00, 1.907896e-04], 249 | 'AIN9AINCOM': [7.652808e+00, 1.479229e+00, 2.832601e-01, 4.495232e-02], 250 | 'AIN8AINCOM': [8.290843e-07, -7.129532e-07, 9.993159e-01, 3.307947e-03], 251 | 'AIN4AINCOM': [4.135213e-06, -1.973478e-05, 1.000277e+00, 2.115374e-04], } 252 | A = AD7718(I, calibs) 253 | for a in range(10): 254 | print(A.readRawVoltage('AIN1AINCOM')) 255 | time.sleep(0.3) 256 | -------------------------------------------------------------------------------- /PSL/SENSORS/AD9833.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | class AD9833: 5 | if sys.version.major == 3: 6 | DDS_MAX_FREQ = 0xFFFFFFF - 1 # 24 bit resolution 7 | else: 8 | DDS_MAX_FREQ = eval("0xFFFFFFFL-1") # 24 bit resolution 9 | # control bytes 10 | DDS_B28 = 13 11 | DDS_HLB = 12 12 | DDS_FSELECT = 11 13 | DDS_PSELECT = 10 14 | DDS_RESET = 8 15 | DDS_SLEEP1 = 7 16 | DDS_SLEEP12 = 6 17 | DDS_OPBITEN = 5 18 | DDS_DIV2 = 3 19 | DDS_MODE = 1 20 | 21 | DDS_FSYNC = 9 22 | 23 | DDS_SINE = (0) 24 | DDS_TRIANGLE = (1 << DDS_MODE) 25 | DDS_SQUARE = (1 << DDS_OPBITEN) 26 | DDS_RESERVED = (1 << DDS_OPBITEN) | (1 << DDS_MODE) 27 | clockScaler = 4 # 8MHz 28 | 29 | def __init__(self, I=None): 30 | self.CS = 9 31 | if I: 32 | self.I = I 33 | else: 34 | from PSL import sciencelab 35 | self.I = sciencelab.connect() 36 | self.I.SPI.set_parameters(2, 2, 1, 1, 0) 37 | self.I.map_reference_clock(self.clockScaler, 'WAVEGEN') 38 | print('clock set to ', self.I.DDS_CLOCK) 39 | 40 | self.waveform_mode = self.DDS_TRIANGLE; 41 | self.write(1 << self.DDS_RESET) 42 | self.write((1 << self.DDS_B28) | self.waveform_mode) # finished loading data 43 | self.active_channel = 0 44 | self.frequency = 1000 45 | 46 | def write(self, con): 47 | self.I.SPI.start(self.CS) 48 | self.I.SPI.send16(con) 49 | self.I.SPI.stop(self.CS) 50 | 51 | def set_frequency(self, freq, register=0, **args): 52 | self.active_channel = register 53 | self.frequency = freq 54 | 55 | freq_setting = int(round(freq * self.DDS_MAX_FREQ / self.I.DDS_CLOCK)) 56 | modebits = (1 << self.DDS_B28) | self.waveform_mode 57 | if register: 58 | modebits |= (1 << self.DDS_FSELECT) 59 | regsel = 0x8000 60 | else: 61 | regsel = 0x4000 62 | 63 | self.write((1 << self.DDS_RESET) | modebits) # Ready to load DATA 64 | self.write((regsel | (freq_setting & 0x3FFF)) & 0xFFFF) # LSB 65 | self.write((regsel | ((freq_setting >> 14) & 0x3FFF)) & 0xFFFF) # MSB 66 | phase = args.get('phase', 0) 67 | self.write(0xc000 | phase) # Phase 68 | self.write(modebits) # finished loading data 69 | 70 | def set_voltage(self, v): 71 | self.waveform_mode = self.DDS_TRIANGLE 72 | self.set_frequency(0, 0, phase=v) # 0xfff*v/.6) 73 | 74 | def select_frequency_register(self, register): 75 | self.active_channel = register 76 | modebits = self.waveform_mode 77 | if register: modebits |= (1 << self.DDS_FSELECT) 78 | self.write(modebits) 79 | 80 | def set_waveform_mode(self, mode): 81 | self.waveform_mode = mode 82 | modebits = mode 83 | if self.active_channel: modebits |= (1 << self.DDS_FSELECT) 84 | self.write(modebits) 85 | 86 | 87 | if __name__ == "__main__": 88 | from PSL import sciencelab 89 | 90 | I = sciencelab.connect() 91 | A = AD9833(I=I) 92 | A.set_waveform_mode(A.DDS_SINE) 93 | A.set_frequency(3600, 0) 94 | A.set_frequency(3600, 1) 95 | -------------------------------------------------------------------------------- /PSL/SENSORS/ADS1115.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: python; indent-tabs-mode: t; tab-width:4 -*- 2 | from __future__ import print_function 3 | 4 | import time 5 | 6 | from numpy import int16 7 | 8 | try: 9 | from collections import OrderedDict 10 | except ImportError: 11 | # fallback: try to use the ordereddict backport when using python 2.6 12 | from ordereddict import OrderedDict 13 | 14 | 15 | def connect(route, **args): 16 | return ADS1115(route, **args) 17 | 18 | 19 | class ADS1115: 20 | ADDRESS = 0x48 # addr pin grounded. floating 21 | 22 | REG_POINTER_MASK = 0x3 23 | REG_POINTER_CONVERT = 0 24 | REG_POINTER_CONFIG = 1 25 | REG_POINTER_LOWTHRESH = 2 26 | REG_POINTER_HITHRESH = 3 27 | 28 | REG_CONFIG_OS_MASK = 0x8000 29 | REG_CONFIG_OS_SINGLE = 0x8000 30 | REG_CONFIG_OS_BUSY = 0x0000 31 | REG_CONFIG_OS_NOTBUSY = 0x8000 32 | 33 | REG_CONFIG_MUX_MASK = 0x7000 34 | REG_CONFIG_MUX_DIFF_0_1 = 0x0000 # Differential P = AIN0, N = AIN1 =default) 35 | REG_CONFIG_MUX_DIFF_0_3 = 0x1000 # Differential P = AIN0, N = AIN3 36 | REG_CONFIG_MUX_DIFF_1_3 = 0x2000 # Differential P = AIN1, N = AIN3 37 | REG_CONFIG_MUX_DIFF_2_3 = 0x3000 # Differential P = AIN2, N = AIN3 38 | REG_CONFIG_MUX_SINGLE_0 = 0x4000 # Single-ended AIN0 39 | REG_CONFIG_MUX_SINGLE_1 = 0x5000 # Single-ended AIN1 40 | REG_CONFIG_MUX_SINGLE_2 = 0x6000 # Single-ended AIN2 41 | REG_CONFIG_MUX_SINGLE_3 = 0x7000 # Single-ended AIN3 42 | 43 | REG_CONFIG_PGA_MASK = 0x0E00 # bits 11:9 44 | REG_CONFIG_PGA_6_144V = (0 << 9) # +/-6.144V range = Gain 2/3 45 | REG_CONFIG_PGA_4_096V = (1 << 9) # +/-4.096V range = Gain 1 46 | REG_CONFIG_PGA_2_048V = (2 << 9) # +/-2.048V range = Gain 2 =default) 47 | REG_CONFIG_PGA_1_024V = (3 << 9) # +/-1.024V range = Gain 4 48 | REG_CONFIG_PGA_0_512V = (4 << 9) # +/-0.512V range = Gain 8 49 | REG_CONFIG_PGA_0_256V = (5 << 9) # +/-0.256V range = Gain 16 50 | 51 | REG_CONFIG_MODE_MASK = 0x0100 # bit 8 52 | REG_CONFIG_MODE_CONTIN = (0 << 8) # Continuous conversion mode 53 | REG_CONFIG_MODE_SINGLE = (1 << 8) # Power-down single-shot mode =default) 54 | 55 | REG_CONFIG_DR_MASK = 0x00E0 56 | REG_CONFIG_DR_8SPS = (0 << 5) # 8 SPS 57 | REG_CONFIG_DR_16SPS = (1 << 5) # 16 SPS 58 | REG_CONFIG_DR_32SPS = (2 << 5) # 32 SPS 59 | REG_CONFIG_DR_64SPS = (3 << 5) # 64 SPS 60 | REG_CONFIG_DR_128SPS = (4 << 5) # 128 SPS 61 | REG_CONFIG_DR_250SPS = (5 << 5) # 260 SPS 62 | REG_CONFIG_DR_475SPS = (6 << 5) # 475 SPS 63 | REG_CONFIG_DR_860SPS = (7 << 5) # 860 SPS 64 | 65 | REG_CONFIG_CMODE_MASK = 0x0010 66 | REG_CONFIG_CMODE_TRAD = 0x0000 67 | REG_CONFIG_CMODE_WINDOW = 0x0010 68 | 69 | REG_CONFIG_CPOL_MASK = 0x0008 70 | REG_CONFIG_CPOL_ACTVLOW = 0x0000 71 | REG_CONFIG_CPOL_ACTVHI = 0x0008 72 | 73 | REG_CONFIG_CLAT_MASK = 0x0004 74 | REG_CONFIG_CLAT_NONLAT = 0x0000 75 | REG_CONFIG_CLAT_LATCH = 0x0004 76 | 77 | REG_CONFIG_CQUE_MASK = 0x0003 78 | REG_CONFIG_CQUE_1CONV = 0x0000 79 | REG_CONFIG_CQUE_2CONV = 0x0001 80 | REG_CONFIG_CQUE_4CONV = 0x0002 81 | REG_CONFIG_CQUE_NONE = 0x0003 82 | gains = OrderedDict([('GAIN_TWOTHIRDS', REG_CONFIG_PGA_6_144V), ('GAIN_ONE', REG_CONFIG_PGA_4_096V), 83 | ('GAIN_TWO', REG_CONFIG_PGA_2_048V), ('GAIN_FOUR', REG_CONFIG_PGA_1_024V), 84 | ('GAIN_EIGHT', REG_CONFIG_PGA_0_512V), ('GAIN_SIXTEEN', REG_CONFIG_PGA_0_256V)]) 85 | gain_scaling = OrderedDict( 86 | [('GAIN_TWOTHIRDS', 0.1875), ('GAIN_ONE', 0.125), ('GAIN_TWO', 0.0625), ('GAIN_FOUR', 0.03125), 87 | ('GAIN_EIGHT', 0.015625), ('GAIN_SIXTEEN', 0.0078125)]) 88 | type_selection = OrderedDict( 89 | [('UNI_0', 0), ('UNI_1', 1), ('UNI_2', 2), ('UNI_3', 3), ('DIFF_01', '01'), ('DIFF_23', '23')]) 90 | sdr_selection = OrderedDict( 91 | [(8, REG_CONFIG_DR_8SPS), (16, REG_CONFIG_DR_16SPS), (32, REG_CONFIG_DR_32SPS), (64, REG_CONFIG_DR_64SPS), 92 | (128, REG_CONFIG_DR_128SPS), (250, REG_CONFIG_DR_250SPS), (475, REG_CONFIG_DR_475SPS), 93 | (860, REG_CONFIG_DR_860SPS)]) # sampling data rate 94 | 95 | NUMPLOTS = 1 96 | PLOTNAMES = ['mV'] 97 | 98 | def __init__(self, I2C, **args): 99 | self.ADDRESS = args.get('address', self.ADDRESS) 100 | self.I2C = I2C 101 | self.channel = 'UNI_0' 102 | self.gain = 'GAIN_ONE' 103 | self.rate = 128 104 | 105 | self.setGain('GAIN_ONE') 106 | self.setChannel('UNI_0') 107 | self.setDataRate(128) 108 | self.conversionDelay = 8 109 | self.name = 'ADS1115 16-bit ADC' 110 | self.params = {'setGain': self.gains.keys(), 'setChannel': self.type_selection.keys(), 111 | 'setDataRate': self.sdr_selection.keys()} 112 | 113 | def __readInt__(self, addr): 114 | return int16(self.__readUInt__(addr)) 115 | 116 | def __readUInt__(self, addr): 117 | vals = self.I2C.readBulk(self.ADDRESS, addr, 2) 118 | v = 1. * ((vals[0] << 8) | vals[1]) 119 | return v 120 | 121 | def initTemperature(self): 122 | self.I2C.writeBulk(self.ADDRESS, [self.REG_CONTROL, self.CMD_TEMP]) 123 | time.sleep(0.005) 124 | 125 | def readRegister(self, register): 126 | vals = self.I2C.readBulk(self.ADDRESS, register, 2) 127 | return (vals[0] << 8) | vals[1] 128 | 129 | def writeRegister(self, reg, value): 130 | self.I2C.writeBulk(self.ADDRESS, [reg, (value >> 8) & 0xFF, value & 0xFF]) 131 | 132 | def setGain(self, gain): 133 | ''' 134 | options : 'GAIN_TWOTHIRDS','GAIN_ONE','GAIN_TWO','GAIN_FOUR','GAIN_EIGHT','GAIN_SIXTEEN' 135 | ''' 136 | self.gain = gain 137 | 138 | def setChannel(self, channel): 139 | ''' 140 | options 'UNI_0','UNI_1','UNI_2','UNI_3','DIFF_01','DIFF_23' 141 | ''' 142 | self.channel = channel 143 | 144 | def setDataRate(self, rate): 145 | ''' 146 | data rate options 8,16,32,64,128,250,475,860 SPS 147 | ''' 148 | self.rate = rate 149 | 150 | def readADC_SingleEnded(self, chan): 151 | if chan > 3: return None 152 | # start with default values 153 | config = (self.REG_CONFIG_CQUE_NONE # Disable the comparator (default val) 154 | | self.REG_CONFIG_CLAT_NONLAT # Non-latching (default val) 155 | | self.REG_CONFIG_CPOL_ACTVLOW # Alert/Rdy active low (default val) 156 | | self.REG_CONFIG_CMODE_TRAD # Traditional comparator (default val) 157 | | self.sdr_selection[self.rate] # 1600 samples per second (default) 158 | | self.REG_CONFIG_MODE_SINGLE) # Single-shot mode (default) 159 | 160 | # Set PGA/voltage range 161 | config |= self.gains[self.gain] 162 | 163 | if chan == 0: 164 | config |= self.REG_CONFIG_MUX_SINGLE_0 165 | elif chan == 1: 166 | config |= self.REG_CONFIG_MUX_SINGLE_1 167 | elif chan == 2: 168 | config |= self.REG_CONFIG_MUX_SINGLE_2 169 | elif chan == 3: 170 | config |= self.REG_CONFIG_MUX_SINGLE_3 171 | # Set 'start single-conversion' bit 172 | config |= self.REG_CONFIG_OS_SINGLE 173 | self.writeRegister(self.REG_POINTER_CONFIG, config); 174 | time.sleep(1. / self.rate + .002) # convert to mS to S 175 | return self.readRegister(self.REG_POINTER_CONVERT) * self.gain_scaling[self.gain] 176 | 177 | def readADC_Differential(self, chan='01'): 178 | # start with default values 179 | config = (self.REG_CONFIG_CQUE_NONE # Disable the comparator (default val) 180 | | self.REG_CONFIG_CLAT_NONLAT # Non-latching (default val) 181 | | self.REG_CONFIG_CPOL_ACTVLOW # Alert/Rdy active low (default val) 182 | | self.REG_CONFIG_CMODE_TRAD # Traditional comparator (default val) 183 | | self.sdr_selection[self.rate] # samples per second 184 | | self.REG_CONFIG_MODE_SINGLE) # Single-shot mode (default) 185 | 186 | # Set PGA/voltage range 187 | config |= self.gains[self.gain] 188 | if chan == '01': 189 | config |= self.REG_CONFIG_MUX_DIFF_0_1 190 | elif chan == '23': 191 | config |= self.REG_CONFIG_MUX_DIFF_2_3 192 | # Set 'start single-conversion' bit 193 | config |= self.REG_CONFIG_OS_SINGLE 194 | self.writeRegister(self.REG_POINTER_CONFIG, config); 195 | time.sleep(1. / self.rate + .002) # convert to mS to S 196 | return int16(self.readRegister(self.REG_POINTER_CONVERT)) * self.gain_scaling[self.gain] 197 | 198 | def getLastResults(self): 199 | return int16(self.readRegister(self.REG_POINTER_CONVERT)) * self.gain_scaling[self.gain] 200 | 201 | def getRaw(self): 202 | ''' 203 | return values in mV 204 | ''' 205 | chan = self.type_selection[self.channel] 206 | if self.channel[:3] == 'UNI': 207 | return [self.readADC_SingleEnded(chan)] 208 | elif self.channel[:3] == 'DIF': 209 | return [self.readADC_Differential(chan)] 210 | -------------------------------------------------------------------------------- /PSL/SENSORS/BH1750.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | 4 | def connect(route, **args): 5 | return BRIDGE(route, **args) 6 | 7 | 8 | class BRIDGE(): 9 | POWER_ON = 0x01 10 | RESET = 0x07 11 | RES_1000mLx = 0x10 12 | RES_500mLx = 0x11 13 | RES_4000mLx = 0x13 14 | 15 | gain_choices = [RES_500mLx, RES_1000mLx, RES_4000mLx] 16 | gain_literal_choices = ['500mLx', '1000mLx', '4000mLx'] 17 | gain = 0 18 | scaling = [2, 1, .25] 19 | 20 | # --------------Parameters-------------------- 21 | # This must be defined in order to let GUIs automatically create menus 22 | # for changing various options of this sensor 23 | # It's a dictionary of the string representations of functions matched with an array 24 | # of options that each one can accept 25 | params = {'init': None, 26 | 'setRange': gain_literal_choices, 27 | } 28 | 29 | NUMPLOTS = 1 30 | PLOTNAMES = ['Lux'] 31 | ADDRESS = 0x23 32 | name = 'Luminosity' 33 | 34 | def __init__(self, I2C, **args): 35 | self.I2C = I2C 36 | self.ADDRESS = args.get('address', 0x23) 37 | self.init() 38 | 39 | def init(self): 40 | self.I2C.writeBulk(self.ADDRESS, [self.RES_500mLx]) 41 | 42 | def setRange(self, g): 43 | self.gain = self.gain_literal_choices.index(g) 44 | self.I2C.writeBulk(self.ADDRESS, [self.gain_choices[self.gain]]) 45 | 46 | def getVals(self, numbytes): 47 | vals = self.I2C.simpleRead(self.ADDRESS, numbytes) 48 | return vals 49 | 50 | def getRaw(self): 51 | vals = self.getVals(2) 52 | if vals: 53 | if len(vals) == 2: 54 | return [(vals[0] << 8 | vals[1]) / 1.2] # /self.scaling[self.gain] 55 | else: 56 | return False 57 | else: 58 | return False 59 | 60 | 61 | if __name__ == "__main__": 62 | from PSL import sciencelab 63 | 64 | I = sciencelab.connect() 65 | A = connect(I.I2C) 66 | print(A.getRaw()) 67 | -------------------------------------------------------------------------------- /PSL/SENSORS/BMP180.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import time 4 | 5 | from numpy import int16 6 | 7 | 8 | def connect(route, **args): 9 | return BMP180(route, **args) 10 | 11 | 12 | class BMP180: 13 | ADDRESS = 0x77 14 | REG_CONTROL = 0xF4 15 | REG_RESULT = 0xF6 16 | CMD_TEMP = 0x2E 17 | CMD_P0 = 0x34 18 | CMD_P1 = 0x74 19 | CMD_P2 = 0xB4 20 | CMD_P3 = 0xF4 21 | oversampling = 0 22 | NUMPLOTS = 3 23 | PLOTNAMES = ['Temperature', 'Pressure', 'Altitude'] 24 | name = 'Altimeter BMP180' 25 | 26 | def __init__(self, I2C, **args): 27 | self.ADDRESS = args.get('address', self.ADDRESS) 28 | 29 | self.I2C = I2C 30 | 31 | self.MB = self.__readInt__(0xBA) 32 | 33 | self.c3 = 160.0 * pow(2, -15) * self.__readInt__(0xAE) 34 | self.c4 = pow(10, -3) * pow(2, -15) * self.__readUInt__(0xB0) 35 | self.b1 = pow(160, 2) * pow(2, -30) * self.__readInt__(0xB6) 36 | self.c5 = (pow(2, -15) / 160) * self.__readUInt__(0xB2) 37 | self.c6 = self.__readUInt__(0xB4) 38 | self.mc = (pow(2, 11) / pow(160, 2)) * self.__readInt__(0xBC) 39 | self.md = self.__readInt__(0xBE) / 160.0 40 | self.x0 = self.__readInt__(0xAA) 41 | self.x1 = 160.0 * pow(2, -13) * self.__readInt__(0xAC) 42 | self.x2 = pow(160, 2) * pow(2, -25) * self.__readInt__(0xB8) 43 | self.y0 = self.c4 * pow(2, 15) 44 | self.y1 = self.c4 * self.c3 45 | self.y2 = self.c4 * self.b1 46 | self.p0 = (3791.0 - 8.0) / 1600.0 47 | self.p1 = 1.0 - 7357.0 * pow(2, -20) 48 | self.p2 = 3038.0 * 100.0 * pow(2, -36) 49 | self.T = 25 50 | print('calib:', self.c3, self.c4, self.b1, self.c5, self.c6, self.mc, self.md, self.x0, self.x1, self.x2, 51 | self.y0, self.y1, self.p0, self.p1, self.p2) 52 | self.params = {'setOversampling': [0, 1, 2, 3]} 53 | self.name = 'BMP180 Altimeter' 54 | self.initTemperature() 55 | self.readTemperature() 56 | self.initPressure() 57 | self.baseline = self.readPressure() 58 | 59 | def __readInt__(self, addr): 60 | return int16(self.__readUInt__(addr)) 61 | 62 | def __readUInt__(self, addr): 63 | vals = self.I2C.readBulk(self.ADDRESS, addr, 2) 64 | v = 1. * ((vals[0] << 8) | vals[1]) 65 | return v 66 | 67 | def initTemperature(self): 68 | self.I2C.writeBulk(self.ADDRESS, [self.REG_CONTROL, self.CMD_TEMP]) 69 | time.sleep(0.005) 70 | 71 | def readTemperature(self): 72 | vals = self.I2C.readBulk(self.ADDRESS, self.REG_RESULT, 2) 73 | if len(vals) == 2: 74 | T = (vals[0] << 8) + vals[1] 75 | a = self.c5 * (T - self.c6) 76 | self.T = a + (self.mc / (a + self.md)) 77 | return self.T 78 | else: 79 | return False 80 | 81 | def setOversampling(self, num): 82 | self.oversampling = num 83 | 84 | def initPressure(self): 85 | os = [0x34, 0x74, 0xb4, 0xf4] 86 | delays = [0.005, 0.008, 0.014, 0.026] 87 | self.I2C.writeBulk(self.ADDRESS, [self.REG_CONTROL, os[self.oversampling]]) 88 | time.sleep(delays[self.oversampling]) 89 | 90 | def readPressure(self): 91 | vals = self.I2C.readBulk(self.ADDRESS, self.REG_RESULT, 3) 92 | if len(vals) == 3: 93 | P = 1. * (vals[0] << 8) + vals[1] + (vals[2] / 256.0) 94 | s = self.T - 25.0 95 | x = (self.x2 * pow(s, 2)) + (self.x1 * s) + self.x0 96 | y = (self.y2 * pow(s, 2)) + (self.y1 * s) + self.y0 97 | z = (P - x) / y 98 | self.P = (self.p2 * pow(z, 2)) + (self.p1 * z) + self.p0 99 | return self.P 100 | else: 101 | return False 102 | 103 | def altitude(self): 104 | # baseline pressure needs to be provided 105 | return (44330.0 * (1 - pow(self.P / self.baseline, 1 / 5.255))) 106 | 107 | def sealevel(self, P, A): 108 | ''' 109 | given a calculated pressure and altitude, return the sealevel 110 | ''' 111 | return (P / pow(1 - (A / 44330.0), 5.255)) 112 | 113 | def getRaw(self): 114 | self.initTemperature() 115 | self.readTemperature() 116 | self.initPressure() 117 | self.readPressure() 118 | return [self.T, self.P, self.altitude()] 119 | -------------------------------------------------------------------------------- /PSL/SENSORS/ComplementaryFilter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class ComplementaryFilter: 5 | def __init__(self, ): 6 | self.pitch = 0 7 | self.roll = 0 8 | self.dt = 0.001 9 | 10 | def addData(self, accData, gyrData): 11 | self.pitch += (gyrData[0]) * self.dt # Angle around the X-axis 12 | self.roll -= (gyrData[1]) * self.dt # Angle around the Y-axis 13 | forceMagnitudeApprox = abs(accData[0]) + abs(accData[1]) + abs(accData[2]); 14 | pitchAcc = np.arctan2(accData[1], accData[2]) * 180 / np.pi 15 | self.pitch = self.pitch * 0.98 + pitchAcc * 0.02 16 | rollAcc = np.arctan2(accData[0], accData[2]) * 180 / np.pi 17 | self.roll = self.roll * 0.98 + rollAcc * 0.02 18 | 19 | def getData(self): 20 | return self.roll, self.pitch 21 | -------------------------------------------------------------------------------- /PSL/SENSORS/HMC5883L.py: -------------------------------------------------------------------------------- 1 | def connect(route, **args): 2 | return HMC5883L(route, **args) 3 | 4 | 5 | class HMC5883L(): 6 | CONFA = 0x00 7 | CONFB = 0x01 8 | MODE = 0x02 9 | STATUS = 0x09 10 | 11 | # --------CONFA register bits. 0x00----------- 12 | samplesToAverage = 0 13 | samplesToAverage_choices = [1, 2, 4, 8] 14 | 15 | dataOutputRate = 6 16 | dataOutputRate_choices = [0.75, 1.5, 3, 7.5, 15, 30, 75] 17 | 18 | measurementConf = 0 19 | 20 | # --------CONFB register bits. 0x01----------- 21 | gainValue = 7 # least sensitive 22 | gain_choices = [8, 7, 6, 5, 4, 3, 2, 1] 23 | scaling = [1370., 1090., 820., 660., 440., 390., 330., 230.] 24 | 25 | # --------------Parameters-------------------- 26 | # This must be defined in order to let GUIs automatically create menus 27 | # for changing various options of this sensor 28 | # It's a dictionary of the string representations of functions matched with an array 29 | # of options that each one can accept 30 | params = {'init': None, 31 | 'setSamplesToAverage': samplesToAverage_choices, 32 | 'setDataOutputRate': dataOutputRate_choices, 33 | 'setGain': gain_choices, 34 | } 35 | ADDRESS = 0x1E 36 | name = 'Magnetometer' 37 | NUMPLOTS = 3 38 | PLOTNAMES = ['Bx', 'By', 'Bz'] 39 | 40 | def __init__(self, I2C, **args): 41 | self.I2C = I2C 42 | self.ADDRESS = args.get('address', self.ADDRESS) 43 | self.name = 'Magnetometer' 44 | ''' 45 | try: 46 | print 'switching baud to 400k' 47 | self.I2C.configI2C(400e3) 48 | except: 49 | print 'FAILED TO CHANGE BAUD RATE' 50 | ''' 51 | self.init() 52 | 53 | def init(self): 54 | self.__writeCONFA__() 55 | self.__writeCONFB__() 56 | self.I2C.writeBulk(self.ADDRESS, [self.MODE, 0]) # enable continuous measurement mode 57 | 58 | def __writeCONFB__(self): 59 | self.I2C.writeBulk(self.ADDRESS, [self.CONFB, self.gainValue << 5]) # set gain 60 | 61 | def __writeCONFA__(self): 62 | self.I2C.writeBulk(self.ADDRESS, [self.CONFA, (self.dataOutputRate << 2) | (self.samplesToAverage << 5) | ( 63 | self.measurementConf)]) 64 | 65 | def setSamplesToAverage(self, num): 66 | self.samplesToAverage = self.samplesToAverage_choices.index(num) 67 | self.__writeCONFA__() 68 | 69 | def setDataOutputRate(self, rate): 70 | self.dataOutputRate = self.dataOutputRate_choices.index(rate) 71 | self.__writeCONFA__() 72 | 73 | def setGain(self, gain): 74 | self.gainValue = self.gain_choices.index(gain) 75 | self.__writeCONFB__() 76 | 77 | def getVals(self, addr, numbytes): 78 | vals = self.I2C.readBulk(self.ADDRESS, addr, numbytes) 79 | return vals 80 | 81 | def getRaw(self): 82 | vals = self.getVals(0x03, 6) 83 | if vals: 84 | if len(vals) == 6: 85 | return [int16(vals[a * 2] << 8 | vals[a * 2 + 1]) / self.scaling[self.gainValue] for a in range(3)] 86 | else: 87 | return False 88 | else: 89 | return False 90 | 91 | 92 | if __name__ == "__main__": 93 | from PSL import sciencelab 94 | 95 | I = sciencelab.connect() 96 | I.set_sine1(.5) 97 | A = connect(I.I2C) 98 | A.setGain(2) 99 | t, x, y, z = I.I2C.capture(A.ADDRESS, 0x03, 6, 400, 10000, 'int') 100 | # print (t,x,y,z) 101 | from pylab import * 102 | 103 | plot(t, x) 104 | plot(t, y) 105 | plot(t, z) 106 | show() 107 | -------------------------------------------------------------------------------- /PSL/SENSORS/Kalman.py: -------------------------------------------------------------------------------- 1 | class KalmanFilter(object): 2 | ''' 3 | Credits:http://scottlobdell.me/2014/08/kalman-filtering-python-reading-sensor-input/ 4 | ''' 5 | 6 | def __init__(self, process_variance, estimated_measurement_variance): 7 | self.process_variance = process_variance 8 | self.estimated_measurement_variance = estimated_measurement_variance 9 | self.posteri_estimate = 0.0 10 | self.posteri_error_estimate = 1.0 11 | 12 | def input_latest_noisy_measurement(self, measurement): 13 | priori_estimate = self.posteri_estimate 14 | priori_error_estimate = self.posteri_error_estimate + self.process_variance 15 | 16 | blending_factor = priori_error_estimate / (priori_error_estimate + self.estimated_measurement_variance) 17 | self.posteri_estimate = priori_estimate + blending_factor * (measurement - priori_estimate) 18 | self.posteri_error_estimate = (1 - blending_factor) * priori_error_estimate 19 | 20 | def get_latest_estimated_measurement(self): 21 | return self.posteri_estimate 22 | -------------------------------------------------------------------------------- /PSL/SENSORS/MLX90614.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | 4 | def connect(route, **args): 5 | return MLX90614(route, **args) 6 | 7 | 8 | class MLX90614(): 9 | NUMPLOTS = 1 10 | PLOTNAMES = ['Temp'] 11 | ADDRESS = 0x5A 12 | name = 'PIR temperature' 13 | 14 | def __init__(self, I2C, **args): 15 | self.I2C = I2C 16 | self.ADDRESS = args.get('address', self.ADDRESS) 17 | self.OBJADDR = 0x07 18 | self.AMBADDR = 0x06 19 | 20 | self.source = self.OBJADDR 21 | 22 | self.name = 'Passive IR temperature sensor' 23 | self.params = {'readReg': {'dataType': 'integer', 'min': 0, 'max': 0x20, 'prefix': 'Addr: '}, 24 | 'select_source': ['object temperature', 'ambient temperature']} 25 | 26 | try: 27 | print('switching baud to 100k') 28 | self.I2C.configI2C(100e3) 29 | except Exception as e: 30 | print('FAILED TO CHANGE BAUD RATE', e.message) 31 | 32 | def select_source(self, source): 33 | if source == 'object temperature': 34 | self.source = self.OBJADDR 35 | elif source == 'ambient temperature': 36 | self.source = self.AMBADDR 37 | 38 | def readReg(self, addr): 39 | x = self.getVals(addr, 2) 40 | print(hex(addr), hex(x[0] | (x[1] << 8))) 41 | 42 | def getVals(self, addr, numbytes): 43 | vals = self.I2C.readBulk(self.ADDRESS, addr, numbytes) 44 | return vals 45 | 46 | def getRaw(self): 47 | vals = self.getVals(self.source, 3) 48 | if vals: 49 | if len(vals) == 3: 50 | return [((((vals[1] & 0x007f) << 8) + vals[0]) * 0.02) - 0.01 - 273.15] 51 | else: 52 | return False 53 | else: 54 | return False 55 | 56 | def getObjectTemperature(self): 57 | self.source = self.OBJADDR 58 | val = self.getRaw() 59 | if val: 60 | return val[0] 61 | else: 62 | return False 63 | 64 | def getAmbientTemperature(self): 65 | self.source = self.AMBADDR 66 | val = self.getRaw() 67 | if val: 68 | return val[0] 69 | else: 70 | return False 71 | -------------------------------------------------------------------------------- /PSL/SENSORS/MPU6050.py: -------------------------------------------------------------------------------- 1 | from numpy import int16, std 2 | from PSL.SENSORS.Kalman import KalmanFilter 3 | 4 | 5 | def connect(route, **args): 6 | return MPU6050(route, **args) 7 | 8 | 9 | class MPU6050(): 10 | ''' 11 | Mandatory members: 12 | GetRaw : Function called by Graphical apps. Must return values stored in a list 13 | NUMPLOTS : length of list returned by GetRaw. Even single datapoints need to be stored in a list before returning 14 | PLOTNAMES : a list of strings describing each element in the list returned by GetRaw. len(PLOTNAMES) = NUMPLOTS 15 | name : the name of the sensor shown to the user 16 | params: 17 | A dictionary of function calls(single arguments only) paired with list of valid argument values. (Primitive. I know.) 18 | These calls can be used for one time configuration settings 19 | 20 | ''' 21 | GYRO_CONFIG = 0x1B 22 | ACCEL_CONFIG = 0x1C 23 | GYRO_SCALING = [131, 65.5, 32.8, 16.4] 24 | ACCEL_SCALING = [16384, 8192, 4096, 2048] 25 | AR = 3 26 | GR = 3 27 | NUMPLOTS = 7 28 | PLOTNAMES = ['Ax', 'Ay', 'Az', 'Temp', 'Gx', 'Gy', 'Gz'] 29 | ADDRESS = 0x68 30 | name = 'Accel/gyro' 31 | 32 | def __init__(self, I2C, **args): 33 | self.I2C = I2C 34 | self.ADDRESS = args.get('address', self.ADDRESS) 35 | self.name = 'Accel/gyro' 36 | self.params = {'powerUp': None, 'setGyroRange': [250, 500, 1000, 2000], 'setAccelRange': [2, 4, 8, 16], 37 | 'KalmanFilter': {'dataType': 'double', 'min': 0, 'max': 1000, 'prefix': 'value: '}} 38 | self.setGyroRange(2000) 39 | self.setAccelRange(16) 40 | self.powerUp() 41 | self.K = None 42 | 43 | def KalmanFilter(self, opt): 44 | if opt == 0: 45 | self.K = None 46 | return 47 | noise = [[]] * self.NUMPLOTS 48 | for a in range(500): 49 | vals = self.getRaw() 50 | for b in range(self.NUMPLOTS): noise[b].append(vals[b]) 51 | 52 | self.K = [None] * 7 53 | for a in range(self.NUMPLOTS): 54 | sd = std(noise[a]) 55 | self.K[a] = KalmanFilter(1. / opt, sd ** 2) 56 | 57 | def getVals(self, addr, numbytes): 58 | vals = self.I2C.readBulk(self.ADDRESS, addr, numbytes) 59 | return vals 60 | 61 | def powerUp(self): 62 | self.I2C.writeBulk(self.ADDRESS, [0x6B, 0]) 63 | 64 | def setGyroRange(self, rs): 65 | self.GR = self.params['setGyroRange'].index(rs) 66 | self.I2C.writeBulk(self.ADDRESS, [self.GYRO_CONFIG, self.GR << 3]) 67 | 68 | def setAccelRange(self, rs): 69 | self.AR = self.params['setAccelRange'].index(rs) 70 | self.I2C.writeBulk(self.ADDRESS, [self.ACCEL_CONFIG, self.AR << 3]) 71 | 72 | def getRaw(self): 73 | ''' 74 | This method must be defined if you want GUIs to use this class to generate 75 | plots on the fly. 76 | It must return a set of different values read from the sensor. such as X,Y,Z acceleration. 77 | The length of this list must not change, and must be defined in the variable NUMPLOTS. 78 | 79 | GUIs will generate as many plots, and the data returned from this method will be appended appropriately 80 | ''' 81 | vals = self.getVals(0x3B, 14) 82 | if vals: 83 | if len(vals) == 14: 84 | raw = [0] * 7 85 | for a in range(3): raw[a] = 1. * int16(vals[a * 2] << 8 | vals[a * 2 + 1]) / self.ACCEL_SCALING[self.AR] 86 | for a in range(4, 7): raw[a] = 1. * int16(vals[a * 2] << 8 | vals[a * 2 + 1]) / self.GYRO_SCALING[ 87 | self.GR] 88 | raw[3] = int16(vals[6] << 8 | vals[7]) / 340. + 36.53 89 | if not self.K: 90 | return raw 91 | else: 92 | for b in range(self.NUMPLOTS): 93 | self.K[b].input_latest_noisy_measurement(raw[b]) 94 | raw[b] = self.K[b].get_latest_estimated_measurement() 95 | return raw 96 | 97 | else: 98 | return False 99 | else: 100 | return False 101 | 102 | def getAccel(self): 103 | vals = self.getVals(0x3B, 6) 104 | ax = int16(vals[0] << 8 | vals[1]) 105 | ay = int16(vals[2] << 8 | vals[3]) 106 | az = int16(vals[4] << 8 | vals[5]) 107 | return [ax / 65535., ay / 65535., az / 65535.] 108 | 109 | def getTemp(self): 110 | vals = self.getVals(0x41, 6) 111 | t = int16(vals[0] << 8 | vals[1]) 112 | return t / 65535. 113 | 114 | def getGyro(self): 115 | vals = self.getVals(0x43, 6) 116 | ax = int16(vals[0] << 8 | vals[1]) 117 | ay = int16(vals[2] << 8 | vals[3]) 118 | az = int16(vals[4] << 8 | vals[5]) 119 | return [ax / 65535., ay / 65535., az / 65535.] 120 | 121 | 122 | if __name__ == "__main__": 123 | from PSL import sciencelab 124 | 125 | I = sciencelab.connect() 126 | A = connect(I.I2C) 127 | t, x, y, z = I.I2C.capture(A.ADDRESS, 0x43, 6, 5000, 1000, 'int') 128 | # print (t,x,y,z) 129 | from pylab import * 130 | 131 | plot(t, x) 132 | plot(t, y) 133 | plot(t, z) 134 | show() 135 | -------------------------------------------------------------------------------- /PSL/SENSORS/MPU925x.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: python; indent-tabs-mode: t; tab-width:4 -*- 2 | from Kalman import KalmanFilter 3 | 4 | 5 | def connect(route, **args): 6 | return MPU925x(route, **args) 7 | 8 | 9 | class MPU925x(): 10 | ''' 11 | Mandatory members: 12 | GetRaw : Function called by Graphical apps. Must return values stored in a list 13 | NUMPLOTS : length of list returned by GetRaw. Even single datapoints need to be stored in a list before returning 14 | PLOTNAMES : a list of strings describing each element in the list returned by GetRaw. len(PLOTNAMES) = NUMPLOTS 15 | name : the name of the sensor shown to the user 16 | params: 17 | A dictionary of function calls(single arguments only) paired with list of valid argument values. (Primitive. I know.) 18 | These calls can be used for one time configuration settings 19 | 20 | ''' 21 | INT_PIN_CFG = 0x37 22 | GYRO_CONFIG = 0x1B 23 | ACCEL_CONFIG = 0x1C 24 | GYRO_SCALING = [131, 65.5, 32.8, 16.4] 25 | ACCEL_SCALING = [16384, 8192, 4096, 2048] 26 | AR = 3 27 | GR = 3 28 | NUMPLOTS = 7 29 | PLOTNAMES = ['Ax', 'Ay', 'Az', 'Temp', 'Gx', 'Gy', 'Gz'] 30 | ADDRESS = 0x68 31 | AK8963_ADDRESS = 0x0C 32 | AK8963_CNTL = 0x0A 33 | name = 'Accel/gyro' 34 | 35 | def __init__(self, I2C, **args): 36 | self.I2C = I2C 37 | self.ADDRESS = args.get('address', self.ADDRESS) 38 | self.name = 'Accel/gyro' 39 | self.params = {'powerUp': None, 'setGyroRange': [250, 500, 1000, 2000], 'setAccelRange': [2, 4, 8, 16], 40 | 'KalmanFilter': [.01, .1, 1, 10, 100, 1000, 10000, 'OFF']} 41 | self.setGyroRange(2000) 42 | self.setAccelRange(16) 43 | self.powerUp() 44 | self.K = None 45 | 46 | def KalmanFilter(self, opt): 47 | if opt == 'OFF': 48 | self.K = None 49 | return 50 | noise = [[]] * self.NUMPLOTS 51 | for a in range(500): 52 | vals = self.getRaw() 53 | for b in range(self.NUMPLOTS): noise[b].append(vals[b]) 54 | 55 | self.K = [None] * 7 56 | for a in range(self.NUMPLOTS): 57 | sd = std(noise[a]) 58 | self.K[a] = KalmanFilter(1. / opt, sd ** 2) 59 | 60 | def getVals(self, addr, numbytes): 61 | return self.I2C.readBulk(self.ADDRESS, addr, numbytes) 62 | 63 | def powerUp(self): 64 | self.I2C.writeBulk(self.ADDRESS, [0x6B, 0]) 65 | 66 | def setGyroRange(self, rs): 67 | self.GR = self.params['setGyroRange'].index(rs) 68 | self.I2C.writeBulk(self.ADDRESS, [self.GYRO_CONFIG, self.GR << 3]) 69 | 70 | def setAccelRange(self, rs): 71 | self.AR = self.params['setAccelRange'].index(rs) 72 | self.I2C.writeBulk(self.ADDRESS, [self.ACCEL_CONFIG, self.AR << 3]) 73 | 74 | def getRaw(self): 75 | ''' 76 | This method must be defined if you want GUIs to use this class to generate plots on the fly. 77 | It must return a set of different values read from the sensor. such as X,Y,Z acceleration. 78 | The length of this list must not change, and must be defined in the variable NUMPLOTS. 79 | 80 | GUIs will generate as many plots, and the data returned from this method will be appended appropriately 81 | ''' 82 | vals = self.getVals(0x3B, 14) 83 | if vals: 84 | if len(vals) == 14: 85 | raw = [0] * 7 86 | for a in range(3): raw[a] = 1. * int16(vals[a * 2] << 8 | vals[a * 2 + 1]) / self.ACCEL_SCALING[self.AR] 87 | for a in range(4, 7): raw[a] = 1. * int16(vals[a * 2] << 8 | vals[a * 2 + 1]) / self.GYRO_SCALING[ 88 | self.GR] 89 | raw[3] = int16(vals[6] << 8 | vals[7]) / 340. + 36.53 90 | if not self.K: 91 | return raw 92 | else: 93 | for b in range(self.NUMPLOTS): 94 | self.K[b].input_latest_noisy_measurement(raw[b]) 95 | raw[b] = self.K[b].get_latest_estimated_measurement() 96 | return raw 97 | 98 | else: 99 | return False 100 | else: 101 | return False 102 | 103 | def getAccel(self): 104 | ''' 105 | Return a list of 3 values for acceleration vector 106 | 107 | ''' 108 | vals = self.getVals(0x3B, 6) 109 | ax = int16(vals[0] << 8 | vals[1]) 110 | ay = int16(vals[2] << 8 | vals[3]) 111 | az = int16(vals[4] << 8 | vals[5]) 112 | return [ax / 65535., ay / 65535., az / 65535.] 113 | 114 | def getTemp(self): 115 | ''' 116 | Return temperature 117 | ''' 118 | vals = self.getVals(0x41, 6) 119 | t = int16(vals[0] << 8 | vals[1]) 120 | return t / 65535. 121 | 122 | def getGyro(self): 123 | ''' 124 | Return a list of 3 values for angular velocity vector 125 | 126 | ''' 127 | vals = self.getVals(0x43, 6) 128 | ax = int16(vals[0] << 8 | vals[1]) 129 | ay = int16(vals[2] << 8 | vals[3]) 130 | az = int16(vals[4] << 8 | vals[5]) 131 | return [ax / 65535., ay / 65535., az / 65535.] 132 | 133 | def getMag(self): 134 | ''' 135 | Return a list of 3 values for magnetic field vector 136 | 137 | ''' 138 | vals = self.I2C.readBulk(self.AK8963_ADDRESS, 0x03, 139 | 7) # 6+1 . 1(ST2) should not have bit 4 (0x8) true. It's ideally 16 . overflow bit 140 | ax = int16(vals[0] << 8 | vals[1]) 141 | ay = int16(vals[2] << 8 | vals[3]) 142 | az = int16(vals[4] << 8 | vals[5]) 143 | if not vals[6] & 0x08: 144 | return [ax / 65535., ay / 65535., az / 65535.] 145 | else: 146 | return None 147 | 148 | def WhoAmI(self): 149 | ''' 150 | Returns the ID. 151 | It is 71 for MPU9250. 152 | ''' 153 | v = self.I2C.readBulk(self.ADDRESS, 0x75, 1)[0] 154 | if v not in [0x71, 0x73]: return 'Error %s' % hex(v) 155 | 156 | if v == 0x73: 157 | return 'MPU9255 %s' % hex(v) 158 | elif v == 0x71: 159 | return 'MPU9250 %s' % hex(v) 160 | 161 | def WhoAmI_AK8963(self): 162 | ''' 163 | Returns the ID fo magnetometer AK8963 if found. 164 | It should be 0x48. 165 | ''' 166 | self.initMagnetometer() 167 | v = self.I2C.readBulk(self.AK8963_ADDRESS, 0, 1)[0] 168 | if v == 0x48: 169 | return 'AK8963 at %s' % hex(v) 170 | else: 171 | return 'AK8963 not found. returned :%s' % hex(v) 172 | 173 | def initMagnetometer(self): 174 | ''' 175 | For MPU925x with integrated magnetometer. 176 | It's called a 10 DoF sensor, but technically speaking , 177 | the 3-axis Accel , 3-Axis Gyro, temperature sensor are integrated in one IC, and the 3-axis magnetometer is implemented in a 178 | separate IC which can be accessed via an I2C passthrough. 179 | Therefore , in order to detect the magnetometer via an I2C scan, the passthrough must first be enabled on IC#1 (Accel,gyro,temp) 180 | ''' 181 | self.I2C.writeBulk(self.ADDRESS, [self.INT_PIN_CFG, 0x22]) # I2C passthrough 182 | self.I2C.writeBulk(self.AK8963_ADDRESS, [self.AK8963_CNTL, 0]) # power down mag 183 | self.I2C.writeBulk(self.AK8963_ADDRESS, 184 | [self.AK8963_CNTL, (1 << 4) | 6]) # mode (0=14bits,1=16bits) <<4 | (2=8Hz , 6=100Hz) 185 | 186 | 187 | if __name__ == "__main__": 188 | from PSL import sciencelab 189 | 190 | I = sciencelab.connect() 191 | A = connect(I.I2C) 192 | t, x, y, z = I.I2C.capture(A.ADDRESS, 0x43, 6, 5000, 1000, 'int') 193 | # print (t,x,y,z) 194 | from pylab import * 195 | 196 | plot(t, x) 197 | plot(t, y) 198 | plot(t, z) 199 | show() 200 | -------------------------------------------------------------------------------- /PSL/SENSORS/SHT21.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import time 3 | 4 | 5 | def connect(route, **args): 6 | ''' 7 | route can either be I.I2C , or a radioLink instance 8 | ''' 9 | return SHT21(route, **args) 10 | 11 | 12 | def rawToTemp(vals): 13 | if vals: 14 | if len(vals): 15 | v = (vals[0] << 8) | (vals[1] & 0xFC) # make integer & remove status bits 16 | v *= 175.72 17 | v /= (1 << 16) 18 | v -= 46.85 19 | return [v] 20 | return False 21 | 22 | 23 | def rawToRH(vals): 24 | if vals: 25 | if len(vals): 26 | v = (vals[0] << 8) | (vals[1] & 0xFC) # make integer & remove status bits 27 | v *= 125. 28 | v /= (1 << 16) 29 | v -= 6 30 | return [v] 31 | return False 32 | 33 | 34 | class SHT21(): 35 | RESET = 0xFE 36 | TEMP_ADDRESS = 0xF3 37 | HUMIDITY_ADDRESS = 0xF5 38 | selected = 0xF3 39 | NUMPLOTS = 1 40 | PLOTNAMES = ['Data'] 41 | ADDRESS = 0x40 42 | name = 'Humidity/Temperature' 43 | 44 | def __init__(self, I2C, **args): 45 | self.I2C = I2C 46 | self.ADDRESS = args.get('address', self.ADDRESS) 47 | self.name = 'Humidity/Temperature' 48 | ''' 49 | try: 50 | print ('switching baud to 400k') 51 | self.I2C.configI2C(400e3) 52 | except: 53 | print ('FAILED TO CHANGE BAUD RATE') 54 | ''' 55 | self.params = {'selectParameter': ['temperature', 'humidity'], 'init': None} 56 | self.init() 57 | 58 | def init(self): 59 | self.I2C.writeBulk(self.ADDRESS, [self.RESET]) # soft reset 60 | time.sleep(0.1) 61 | 62 | @staticmethod 63 | def _calculate_checksum(data, number_of_bytes): 64 | """5.7 CRC Checksum using the polynomial given in the datasheet 65 | Credits: https://github.com/jaques/sht21_python/blob/master/sht21.py 66 | """ 67 | # CRC 68 | POLYNOMIAL = 0x131 # //P(x)=x^8+x^5+x^4+1 = 100110001 69 | crc = 0 70 | # calculates 8-Bit checksum with given polynomial 71 | for byteCtr in range(number_of_bytes): 72 | crc ^= (data[byteCtr]) 73 | for _ in range(8, 0, -1): 74 | if crc & 0x80: 75 | crc = (crc << 1) ^ POLYNOMIAL 76 | else: 77 | crc = (crc << 1) 78 | return crc 79 | 80 | def selectParameter(self, param): 81 | if param == 'temperature': 82 | self.selected = self.TEMP_ADDRESS 83 | elif param == 'humidity': 84 | self.selected = self.HUMIDITY_ADDRESS 85 | 86 | def getRaw(self): 87 | self.I2C.writeBulk(self.ADDRESS, [self.selected]) 88 | if self.selected == self.TEMP_ADDRESS: 89 | time.sleep(0.1) 90 | elif self.selected == self.HUMIDITY_ADDRESS: 91 | time.sleep(0.05) 92 | 93 | vals = self.I2C.simpleRead(self.ADDRESS, 3) 94 | if vals: 95 | if self._calculate_checksum(vals, 2) != vals[2]: 96 | print(vals) 97 | return False 98 | if self.selected == self.TEMP_ADDRESS: 99 | return rawToTemp(vals) 100 | elif self.selected == self.HUMIDITY_ADDRESS: 101 | return rawToRH(vals) 102 | -------------------------------------------------------------------------------- /PSL/SENSORS/Sx1276.py: -------------------------------------------------------------------------------- 1 | # Registers adapted from sample code for SEMTECH SX1276 2 | from __future__ import print_function 3 | 4 | import time 5 | 6 | 7 | def connect(SPI, frq, **kwargs): 8 | return SX1276(SPI, frq, **kwargs) 9 | 10 | 11 | class SX1276(): 12 | name = 'SX1276' 13 | # registers 14 | REG_FIFO = 0x00 15 | REG_OP_MODE = 0x01 16 | REG_FRF_MSB = 0x06 17 | REG_FRF_MID = 0x07 18 | REG_FRF_LSB = 0x08 19 | REG_PA_CONFIG = 0x09 20 | REG_LNA = 0x0c 21 | REG_FIFO_ADDR_PTR = 0x0d 22 | REG_FIFO_TX_BASE_ADDR = 0x0e 23 | REG_FIFO_RX_BASE_ADDR = 0x0f 24 | REG_FIFO_RX_CURRENT_ADDR = 0x10 25 | REG_IRQ_FLAGS = 0x12 26 | REG_RX_NB_BYTES = 0x13 27 | REG_PKT_RSSI_VALUE = 0x1a 28 | REG_PKT_SNR_VALUE = 0x1b 29 | REG_MODEM_CONFIG_1 = 0x1d 30 | REG_MODEM_CONFIG_2 = 0x1e 31 | REG_PREAMBLE_MSB = 0x20 32 | REG_PREAMBLE_LSB = 0x21 33 | REG_PAYLOAD_LENGTH = 0x22 34 | REG_MODEM_CONFIG_3 = 0x26 35 | REG_RSSI_WIDEBAND = 0x2c 36 | REG_DETECTION_OPTIMIZE = 0x31 37 | REG_DETECTION_THRESHOLD = 0x37 38 | REG_SYNC_WORD = 0x39 39 | REG_DIO_MAPPING_1 = 0x40 40 | REG_VERSION = 0x42 41 | REG_PA_DAC = 0x4D 42 | # modes 43 | MODE_LONG_RANGE_MODE = 0x80 44 | MODE_SLEEP = 0x00 45 | MODE_STDBY = 0x01 46 | MODE_TX = 0x03 47 | MODE_RX_CONTINUOUS = 0x05 48 | MODE_RX_SINGLE = 0x06 49 | 50 | # PA config 51 | PA_BOOST = 0x80 52 | 53 | # IRQ masks 54 | IRQ_TX_DONE_MASK = 0x08 55 | IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20 56 | IRQ_RX_DONE_MASK = 0x40 57 | 58 | MAX_PKT_LENGTH = 255 59 | 60 | PA_OUTPUT_RFO_PIN = 0 61 | PA_OUTPUT_PA_BOOST_PIN = 1 62 | _onReceive = 0 63 | _frequency = 10 64 | _packetIndex = 0 65 | packetLength = 0 66 | 67 | def __init__(self, SPI, frq, **kwargs): 68 | self.SPI = SPI 69 | self.SPI.set_parameters(2, 6, 1, 0) 70 | self.name = 'SX1276' 71 | self.frequency = frq 72 | 73 | self.reset() 74 | self.version = self.SPIRead(self.REG_VERSION, 1)[0] 75 | if self.version != 0x12: 76 | print('version error', self.version) 77 | self.sleep() 78 | self.setFrequency(self.frequency) 79 | 80 | # set base address 81 | self.SPIWrite(self.REG_FIFO_TX_BASE_ADDR, [0]) 82 | self.SPIWrite(self.REG_FIFO_RX_BASE_ADDR, [0]) 83 | 84 | # set LNA boost 85 | self.SPIWrite(self.REG_LNA, [self.SPIRead(self.REG_LNA)[0] | 0x03]) 86 | 87 | # set auto ADC 88 | self.SPIWrite(self.REG_MODEM_CONFIG_3, [0x04]) 89 | 90 | # output power 17dbm 91 | self.setTxPower(kwargs.get('power', 17), 92 | self.PA_OUTPUT_PA_BOOST_PIN if kwargs.get('boost', True) else self.PA_OUTPUT_RFO_PIN) 93 | self.idle() 94 | 95 | # set bandwidth 96 | self.setSignalBandwidth(kwargs.get('BW', 125e3)) 97 | self.setSpreadingFactor(kwargs.get('SF', 12)) 98 | self.setCodingRate4(kwargs.get('CF', 5)) 99 | 100 | def beginPacket(self, implicitHeader=False): 101 | self.idle() 102 | if implicitHeader: 103 | self.implicitHeaderMode() 104 | else: 105 | self.explicitHeaderMode() 106 | 107 | # reset FIFO & payload length 108 | self.SPIWrite(self.REG_FIFO_ADDR_PTR, [0]) 109 | self.SPIWrite(self.REG_PAYLOAD_LENGTH, [0]) 110 | 111 | def endPacket(self): 112 | # put in TX mode 113 | self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_TX]) 114 | while 1: # Wait for TX done 115 | if self.SPIRead(self.REG_IRQ_FLAGS, 1)[0] & self.IRQ_TX_DONE_MASK: 116 | break 117 | else: 118 | print('wait...') 119 | time.sleep(0.1) 120 | self.SPIWrite(self.REG_IRQ_FLAGS, [self.IRQ_TX_DONE_MASK]) 121 | 122 | def parsePacket(self, size=0): 123 | self.packetLength = 0 124 | irqFlags = self.SPIRead(self.REG_IRQ_FLAGS, 1)[0] 125 | if size > 0: 126 | self.implicitHeaderMode() 127 | self.SPIWrite(self.REG_PAYLOAD_LENGTH, [size & 0xFF]) 128 | else: 129 | self.explicitHeaderMode() 130 | self.SPIWrite(self.REG_IRQ_FLAGS, [irqFlags]) 131 | if (irqFlags & self.IRQ_RX_DONE_MASK) and (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK) == 0: 132 | self._packetIndex = 0 133 | if self._implicitHeaderMode: 134 | self.packetLength = self.SPIRead(self.REG_PAYLOAD_LENGTH, 1)[0] 135 | else: 136 | self.packetLength = self.SPIRead(self.REG_RX_NB_BYTES, 1)[0] 137 | self.SPIWrite(self.REG_FIFO_ADDR_PTR, self.SPIRead(self.REG_FIFO_RX_CURRENT_ADDR, 1)) 138 | self.idle() 139 | elif self.SPIRead(self.REG_OP_MODE)[0] != (self.MODE_LONG_RANGE_MODE | self.MODE_RX_SINGLE): 140 | self.SPIWrite(self.REG_FIFO_ADDR_PTR, [0]) 141 | self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_RX_SINGLE]) 142 | return self.packetLength 143 | 144 | def packetRssi(self): 145 | return self.SPIRead(self.REG_PKT_RSSI_VALUE)[0] - (164 if self._frequency < 868e6 else 157) 146 | 147 | def packetSnr(self): 148 | return self.SPIRead(self.REG_PKT_SNR_VALUE)[0] * 0.25 149 | 150 | def write(self, byteArray): 151 | size = len(byteArray) 152 | currentLength = self.SPIRead(self.REG_PAYLOAD_LENGTH)[0] 153 | if (currentLength + size) > self.MAX_PKT_LENGTH: 154 | size = self.MAX_PKT_LENGTH - currentLength 155 | self.SPIWrite(self.REG_FIFO, byteArray[:size]) 156 | self.SPIWrite(self.REG_PAYLOAD_LENGTH, [currentLength + size]) 157 | return size 158 | 159 | def available(self): 160 | return self.SPIRead(self.REG_RX_NB_BYTES)[0] - self._packetIndex 161 | 162 | def checkRx(self): 163 | irqFlags = self.SPIRead(self.REG_IRQ_FLAGS, 1)[0] 164 | if (irqFlags & self.IRQ_RX_DONE_MASK) and (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK) == 0: 165 | return 1 166 | return 0; 167 | 168 | def read(self): 169 | if not self.available(): return -1 170 | self._packetIndex += 1 171 | return self.SPIRead(self.REG_FIFO)[0] 172 | 173 | def readAll(self): 174 | p = [] 175 | while self.available(): 176 | p.append(self.read()) 177 | return p 178 | 179 | def peek(self): 180 | if not self.available(): return -1 181 | self.currentAddress = self.SPIRead(self.REG_FIFO_ADDR_PTR) 182 | val = self.SPIRead(self.REG_FIFO)[0] 183 | self.SPIWrite(self.REG_FIFO_ADDR_PTR, self.currentAddress) 184 | return val 185 | 186 | def flush(self): 187 | pass 188 | 189 | def receive(self, size): 190 | if size > 0: 191 | self.implicitHeaderMode() 192 | self.SPIWrite(self.REG_PAYLOAD_LENGTH, [size & 0xFF]) 193 | else: 194 | self.explicitHeaderMode() 195 | 196 | self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_RX_SINGLE]) 197 | 198 | def reset(self): 199 | pass 200 | 201 | def idle(self): 202 | self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_STDBY]) 203 | 204 | def sleep(self): 205 | self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_SLEEP]) 206 | 207 | def setTxPower(self, level, pin): 208 | if pin == self.PA_OUTPUT_RFO_PIN: 209 | if level < 0: 210 | level = 0 211 | elif level > 14: 212 | level = 14 213 | self.SPIWrite(self.REG_PA_CONFIG, [0x70 | level]) 214 | else: 215 | if level < 2: 216 | level = 2 217 | elif level > 17: 218 | level = 17 219 | if level == 17: 220 | print('max power output') 221 | self.SPIWrite(self.REG_PA_DAC, [0x87]) 222 | else: 223 | self.SPIWrite(self.REG_PA_DAC, [0x84]) 224 | self.SPIWrite(self.REG_PA_CONFIG, [self.PA_BOOST | 0x70 | (level - 2)]) 225 | 226 | print('power', hex(self.SPIRead(self.REG_PA_CONFIG)[0])) 227 | 228 | def setFrequency(self, frq): 229 | self._frequency = frq 230 | frf = (int(frq) << 19) / 32000000 231 | print('frf', frf) 232 | print('freq', (frf >> 16) & 0xFF, (frf >> 8) & 0xFF, (frf) & 0xFF) 233 | self.SPIWrite(self.REG_FRF_MSB, [(frf >> 16) & 0xFF]) 234 | self.SPIWrite(self.REG_FRF_MID, [(frf >> 8) & 0xFF]) 235 | self.SPIWrite(self.REG_FRF_LSB, [frf & 0xFF]) 236 | 237 | def setSpreadingFactor(self, sf): 238 | if sf < 6: 239 | sf = 6 240 | elif sf > 12: 241 | sf = 12 242 | 243 | if sf == 6: 244 | self.SPIWrite(self.REG_DETECTION_OPTIMIZE, [0xc5]) 245 | self.SPIWrite(self.REG_DETECTION_THRESHOLD, [0x0c]) 246 | else: 247 | self.SPIWrite(self.REG_DETECTION_OPTIMIZE, [0xc3]) 248 | self.SPIWrite(self.REG_DETECTION_THRESHOLD, [0x0a]) 249 | self.SPIWrite(self.REG_MODEM_CONFIG_2, [(self.SPIRead(self.REG_MODEM_CONFIG_2)[0] & 0x0F) | ((sf << 4) & 0xF0)]) 250 | 251 | def setSignalBandwidth(self, sbw): 252 | bw = 9 253 | num = 0 254 | for a in [7.8e3, 10.4e3, 15.6e3, 20.8e3, 31.25e3, 41.7e3, 62.5e3, 125e3, 250e3]: 255 | if sbw <= a: 256 | bw = num 257 | break 258 | num += 1 259 | print('bandwidth: ', bw) 260 | self.SPIWrite(self.REG_MODEM_CONFIG_1, [(self.SPIRead(self.REG_MODEM_CONFIG_1)[0] & 0x0F) | (bw << 4)]) 261 | 262 | def setCodingRate4(self, denominator): 263 | if denominator < 5: 264 | denominator = 5 265 | elif denominator > 8: 266 | denominator = 8 267 | self.SPIWrite(self.REG_MODEM_CONFIG_1, 268 | [(self.SPIRead(self.REG_MODEM_CONFIG_1)[0] & 0xF1) | ((denominator - 4) << 4)]) 269 | 270 | def setPreambleLength(self, length): 271 | self.SPIWrite(self.REG_PREAMBLE_MSB, [(length >> 8) & 0xFF]) 272 | self.SPIWrite(self.REG_PREAMBLE_LSB, [length & 0xFF]) 273 | 274 | def setSyncWord(self, sw): 275 | self.SPIWrite(self.REG_SYNC_WORD, [sw]) 276 | 277 | def crc(self): 278 | self.SPIWrite(self.REG_MODEM_CONFIG_2, [self.SPIRead(self.REG_MODEM_CONFIG_2)[0] | 0x04]) 279 | 280 | def noCrc(self): 281 | self.SPIWrite(self.REG_MODEM_CONFIG_2, [self.SPIRead(self.REG_MODEM_CONFIG_2)[0] & 0xFB]) 282 | 283 | def random(self): 284 | return self.SPIRead(self.REG_RSSI_WIDEBAND)[0] 285 | 286 | def explicitHeaderMode(self): 287 | self._implicitHeaderMode = 0 288 | self.SPIWrite(self.REG_MODEM_CONFIG_1, [self.SPIRead(self.REG_MODEM_CONFIG_1)[0] & 0xFE]) 289 | 290 | def implicitHeaderMode(self): 291 | self._implicitHeaderMode = 1 292 | self.SPIWrite(self.REG_MODEM_CONFIG_1, [self.SPIRead(self.REG_MODEM_CONFIG_1)[0] | 0x01]) 293 | 294 | def handleDio0Rise(self): 295 | irqFlags = self.SPIRead(self.REG_IRQ_FLAGS, 1)[0] 296 | self.SPIWrite(self.REG_IRQ_FLAGS, [irqFlags]) 297 | 298 | if (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK) == 0: 299 | self._packetIndex = 0 300 | if self._implicitHeaderMode: 301 | self.packetLength = self.SPIRead(self.REG_PAYLOAD_LENGTH, 1)[0] 302 | else: 303 | self.packetLength = self.SPIRead(self.REG_RX_NB_BYTES, 1)[0] 304 | 305 | self.SPIWrite(self.REG_FIFO_ADDR_PTR, self.SPIRead(self.REG_FIFO_RX_CURRENT_ADDR, 1)) 306 | if self._onReceive: 307 | print(self.packetLength) 308 | # self._onReceive(self.packetLength) 309 | 310 | self.SPIWrite(self.REG_FIFO_ADDR_PTR, [0]) 311 | 312 | def SPIWrite(self, adr, byteArray): 313 | return self.SPI.xfer('CS1', [0x80 | adr] + byteArray)[1:] 314 | 315 | def SPIRead(self, adr, total_bytes=1): 316 | return self.SPI.xfer('CS1', [adr] + [0] * total_bytes)[1:] 317 | 318 | def getRaw(self): 319 | val = self.SPIRead(0x02, 1) 320 | return val 321 | 322 | 323 | if __name__ == "__main__": 324 | RX = 0; 325 | TX = 1 326 | mode = RX 327 | from PSL import sciencelab 328 | 329 | I = sciencelab.connect() 330 | lora = SX1276(I.SPI, 434e6, boost=True, power=17, BW=125e3, SF=12, CR=5) # settings for maximum range 331 | lora.crc() 332 | cntr = 0 333 | while 1: 334 | time.sleep(0.01) 335 | if mode == TX: 336 | lora.beginPacket() 337 | lora.write([cntr]) 338 | # lora.write([ord(a) for a in ":"]+[cntr]) 339 | print(time.ctime(), [ord(a) for a in ":"] + [cntr], hex(lora.SPIRead(lora.REG_OP_MODE)[0])) 340 | lora.endPacket() 341 | cntr += 1 342 | if cntr == 255: cntr = 0 343 | elif mode == RX: 344 | packet_size = lora.parsePacket() 345 | if packet_size: 346 | print('data', lora.readAll()) 347 | print('Rssi', lora.packetRssi(), lora.packetSnr()) 348 | -------------------------------------------------------------------------------- /PSL/SENSORS/TSL2561.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Adapted from https://github.com/janheise/TSL2561 3 | ''' 4 | 5 | from __future__ import print_function 6 | import time 7 | 8 | 9 | def connect(route, **args): 10 | return TSL2561(route, **args) 11 | 12 | 13 | class TSL2561: 14 | VISIBLE = 2 # channel 0 - channel 1 15 | INFRARED = 1 # channel 1 16 | FULLSPECTRUM = 0 # channel 0 17 | 18 | READBIT = 0x01 19 | COMMAND_BIT = 0x80 # Must be 1 20 | 21 | CONTROL_POWERON = 0x03 22 | CONTROL_POWEROFF = 0x00 23 | 24 | REGISTER_CONTROL = 0x00 25 | REGISTER_TIMING = 0x01 26 | REGISTER_ID = 0x0A 27 | 28 | INTEGRATIONTIME_13MS = 0x00 # 13.7ms 29 | INTEGRATIONTIME_101MS = 0x01 # 101ms 30 | INTEGRATIONTIME_402MS = 0x02 # 402ms 31 | 32 | GAIN_1X = 0x00 # No gain 33 | GAIN_16X = 0x10 # 16x gain 34 | 35 | ADDRESS = 0x39 # addr normal 36 | timing = INTEGRATIONTIME_13MS 37 | gain = GAIN_16X 38 | name = 'TSL2561 Luminosity' 39 | ADDRESS = 0x39 40 | NUMPLOTS = 3 41 | PLOTNAMES = ['Full', 'IR', 'Visible'] 42 | 43 | def __init__(self, I2C, **args): 44 | self.ADDRESS = args.get('address', 0x39) 45 | self.I2C = I2C 46 | # set timing 101ms & 16x gain 47 | self.enable() 48 | self.wait() 49 | self.I2C.writeBulk(self.ADDRESS, [0x80 | 0x01, 0x01 | 0x10]) 50 | # full scale luminosity 51 | infra = self.I2C.readBulk(self.ADDRESS, 0x80 | 0x20 | 0x0E, 2) 52 | full = self.I2C.readBulk(self.ADDRESS, 0x80 | 0x20 | 0x0C, 2) 53 | full = (full[1] << 8) | full[0] 54 | infra = (infra[1] << 8) | infra[0] 55 | 56 | print("Full: %04x" % full) 57 | print("Infrared: %04x" % infra) 58 | print("Visible: %04x" % (full - infra)) 59 | 60 | # self.I2C.writeBulk(self.ADDRESS,[0x80,0x00]) 61 | 62 | self.params = {'setGain': ['1x', '16x'], 'setTiming': [0, 1, 2]} 63 | 64 | def getID(self): 65 | ID = self.I2C.readBulk(self.ADDRESS, self.REGISTER_ID, 1) 66 | print(hex(ID)) 67 | return ID 68 | 69 | def getRaw(self): 70 | infra = self.I2C.readBulk(self.ADDRESS, 0x80 | 0x20 | 0x0E, 2) 71 | full = self.I2C.readBulk(self.ADDRESS, 0x80 | 0x20 | 0x0C, 2) 72 | if infra and full: 73 | full = (full[1] << 8) | full[0] 74 | infra = (infra[1] << 8) | infra[0] 75 | return [full, infra, full - infra] 76 | else: 77 | return False 78 | 79 | def setGain(self, gain): 80 | if (gain == '1x'): 81 | self.gain = self.GAIN_1X 82 | elif (gain == '16x'): 83 | self.gain = self.GAIN_16X 84 | else: 85 | self.gain = self.GAIN_0X 86 | 87 | self.I2C.writeBulk(self.ADDRESS, [self.COMMAND_BIT | self.REGISTER_TIMING, self.gain | self.timing]) 88 | 89 | def setTiming(self, timing): 90 | print([13, 101, 402][timing], 'mS') 91 | self.timing = timing 92 | self.I2C.writeBulk(self.ADDRESS, [self.COMMAND_BIT | self.REGISTER_TIMING, self.gain | self.timing]) 93 | 94 | def enable(self): 95 | self.I2C.writeBulk(self.ADDRESS, [self.COMMAND_BIT | self.REGISTER_CONTROL, self.CONTROL_POWERON]) 96 | 97 | def disable(self): 98 | self.I2C.writeBulk(self.ADDRESS, [self.COMMAND_BIT | self.REGISTER_CONTROL, self.CONTROL_POWEROFF]) 99 | 100 | def wait(self): 101 | if self.timing == self.INTEGRATIONTIME_13MS: 102 | time.sleep(0.014) 103 | if self.timing == self.INTEGRATIONTIME_101MS: 104 | time.sleep(0.102) 105 | if self.timing == self.INTEGRATIONTIME_402MS: 106 | time.sleep(0.403) 107 | -------------------------------------------------------------------------------- /PSL/SENSORS/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PSL/SENSORS/supported.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from PSL.SENSORS import HMC5883L 4 | from PSL.SENSORS import MPU6050 5 | from PSL.SENSORS import MLX90614 6 | from PSL.SENSORS import BMP180 7 | from PSL.SENSORS import TSL2561 8 | from PSL.SENSORS import SHT21 9 | from PSL.SENSORS import BH1750 10 | from PSL.SENSORS import SSD1306 11 | 12 | supported = { 13 | 0x68: MPU6050, # 3-axis gyro,3-axis accel,temperature 14 | 0x1E: HMC5883L, # 3-axis magnetometer 15 | 0x5A: MLX90614, # Passive IR temperature sensor 16 | 0x77: BMP180, # Pressure, Temperature, altitude 17 | 0x39: TSL2561, # Luminosity 18 | 0x40: SHT21, # Temperature, Humidity 19 | 0x23: BH1750, # Luminosity 20 | # 0x3C:SSD1306, #OLED display 21 | } 22 | 23 | # auto generated map of names to classes 24 | nameMap = {} 25 | for a in supported: 26 | nameMap[supported[a].__name__.split('.')[-1]] = (supported[a]) 27 | -------------------------------------------------------------------------------- /PSL/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PSL/achan.py: -------------------------------------------------------------------------------- 1 | """Objects related to the PSLab's analog input channels. 2 | 3 | This module contains several module level variables with details on the analog inputs' 4 | capabilities, including possible gain values, voltage ranges, and firmware-interal 5 | enumeration. 6 | 7 | This module also contains the AnalogInput class, an instance of which functions as a 8 | model of a particular analog input. 9 | """ 10 | 11 | from typing import List, Union 12 | 13 | import numpy as np 14 | 15 | import PSL.commands_proto as CP 16 | from PSL import packet_handler 17 | 18 | GAIN_VALUES = (1, 2, 4, 5, 8, 10, 16, 32) 19 | 20 | ANALOG_CHANNELS = ( 21 | "CH1", 22 | "CH2", 23 | "CH3", 24 | "MIC", 25 | "CAP", 26 | "SEN", 27 | "AN8", 28 | ) 29 | 30 | INPUT_RANGES = { 31 | "CH1": (16.5, -16.5), # Specify inverted channels explicitly by reversing range! 32 | "CH2": (16.5, -16.5), 33 | "CH3": (-3.3, 3.3), # external gain control analog input 34 | "MIC": (-3.3, 3.3), # connected to MIC amplifier 35 | "CAP": (0, 3.3), 36 | "SEN": (0, 3.3), 37 | "AN8": (0, 3.3), 38 | } 39 | 40 | PIC_ADC_MULTIPLEX = { 41 | "CH1": 3, 42 | "CH2": 0, 43 | "CH3": 1, 44 | "MIC": 2, 45 | "AN4": 4, 46 | "SEN": 7, 47 | "CAP": 5, 48 | "AN8": 8, 49 | } 50 | 51 | 52 | class AnalogInput: 53 | """Model of the PSLab's analog inputs, used to scale raw values to voltages. 54 | 55 | Parameters 56 | ---------- 57 | name : {'CH1', 'CH2', 'CH3', 'MIC', 'CAP', 'SEN', 'AN8'} 58 | Name of the analog channel to model. 59 | device : :class:`Handler` 60 | Serial interface for communicating with the PSLab device. 61 | 62 | Attributes 63 | ---------- 64 | gain 65 | resolution 66 | samples_in_buffer : int 67 | Number of samples collected on this channel currently being held in the 68 | device's ADC buffer. 69 | buffer_idx : Union[int, None] 70 | Location in the device's ADC buffer where the samples are stored. None if no 71 | samples captured by this channel are currently held in the buffer. 72 | chosa : int 73 | Number used to refer to this channel in the firmware. 74 | """ 75 | 76 | def __init__(self, name: str, device: packet_handler.Handler): 77 | self._name = name 78 | self._device = device 79 | 80 | if self._name == "CH1": 81 | self._programmable_gain_amplifier = 1 82 | elif self._name == "CH2": 83 | self._programmable_gain_amplifier = 2 84 | else: 85 | self._programmable_gain_amplifier = None 86 | 87 | self._gain = 1 88 | self._resolution = 2 ** 10 - 1 89 | self.samples_in_buffer = 0 90 | self.buffer_idx = None 91 | self._scale = np.poly1d(0) 92 | self._unscale = np.poly1d(0) 93 | self.chosa = PIC_ADC_MULTIPLEX[self._name] 94 | self._calibrate() 95 | 96 | @property 97 | def gain(self) -> Union[int, float, None]: 98 | """Get or set the analog gain. 99 | 100 | Setting a new gain level will automatically recalibrate the channel. 101 | On channels other than CH1 and CH2 gain is None. 102 | 103 | Raises 104 | ------ 105 | TypeError 106 | If gain is set on a channel which does not support it. 107 | ValueError 108 | If a gain value other than 1, 2, 4, 5, 8, 10, 16, 32 is set. 109 | """ 110 | if self._name in ("CH1", "CH2"): 111 | return self._gain 112 | else: 113 | return None 114 | 115 | @gain.setter 116 | def gain(self, value: Union[int, float]): 117 | if self._name not in ("CH1", "CH2"): 118 | raise TypeError(f"Analog gain is not available on {self._name}.") 119 | 120 | if value not in GAIN_VALUES: 121 | raise ValueError(f"Invalid gain. Valid values are {GAIN_VALUES}.") 122 | 123 | gain_idx = GAIN_VALUES.index(value) 124 | self._device.send_byte(CP.ADC) 125 | self._device.send_byte(CP.SET_PGA_GAIN) 126 | self._device.send_byte(self._programmable_gain_amplifier) 127 | self._device.send_byte(gain_idx) 128 | self._device.get_ack() 129 | self._gain = value 130 | self._calibrate() 131 | 132 | @property 133 | def resolution(self) -> int: 134 | """Get or set the resolution in bits. 135 | 136 | Setting a new resolution will automatically recalibrate the channel. 137 | 138 | Raises 139 | ------ 140 | ValueError 141 | If a resolution other than 10 or 12 is set. 142 | """ 143 | return int(np.log2((self._resolution + 1))) 144 | 145 | @resolution.setter 146 | def resolution(self, value: int): 147 | if value not in (10, 12): 148 | raise ValueError("Resolution must be 10 or 12 bits.") 149 | self._resolution = 2 ** value - 1 150 | self._calibrate() 151 | 152 | def _calibrate(self): 153 | A = INPUT_RANGES[self._name][0] / self._gain 154 | B = INPUT_RANGES[self._name][1] / self._gain 155 | slope = B - A 156 | intercept = A 157 | self._scale = np.poly1d([slope / self._resolution, intercept]) 158 | self._unscale = np.poly1d( 159 | [self._resolution / slope, -self._resolution * intercept / slope] 160 | ) 161 | 162 | def scale(self, raw: Union[int, List[int]]) -> float: 163 | """Translate raw integer value from device to corresponding voltage. 164 | 165 | Inverse of :meth:`unscale. `. 166 | 167 | Parameters 168 | ---------- 169 | raw : int, List[int] 170 | An integer, or a list of integers, received from the device. 171 | 172 | Returns 173 | ------- 174 | float 175 | Voltage, translated from raw based on channel range, gain, and resolution. 176 | """ 177 | return self._scale(raw) 178 | 179 | def unscale(self, voltage: float) -> int: 180 | """Translate a voltage to a raw integer value interpretable by the device. 181 | 182 | Inverse of :meth:`scale. `. 183 | 184 | Parameters 185 | ---------- 186 | voltage : float 187 | Voltage in volts. 188 | 189 | Returns 190 | ------- 191 | int 192 | Corresponding integer value, adjusted for resolution and gain and clipped 193 | to the channel's range. 194 | """ 195 | level = self._unscale(voltage) 196 | level = np.clip(level, 0, self._resolution) 197 | level = np.round(level) 198 | return int(level) 199 | -------------------------------------------------------------------------------- /PSL/analyticsClass.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import Tuple 3 | 4 | import numpy as np 5 | 6 | 7 | class analyticsClass(): 8 | """ 9 | This class contains methods that allow mathematical analysis such as curve fitting 10 | """ 11 | 12 | def __init__(self): 13 | try: 14 | import scipy.optimize as optimize 15 | except ImportError: 16 | self.optimize = None 17 | else: 18 | self.optimize = optimize 19 | 20 | try: 21 | import scipy.fftpack as fftpack 22 | except ImportError: 23 | self.fftpack = None 24 | else: 25 | self.fftpack = fftpack 26 | 27 | try: 28 | from scipy.optimize import leastsq 29 | except ImportError: 30 | self.leastsq = None 31 | else: 32 | self.leastsq = leastsq 33 | 34 | try: 35 | import scipy.signal as signal 36 | except ImportError: 37 | self.signal = None 38 | else: 39 | self.signal = signal 40 | 41 | def sineFunc(self, x, a1, a2, a3, a4): 42 | return a4 + a1 * np.sin(abs(a2 * (2 * np.pi)) * x + a3) 43 | 44 | def squareFunc(self, x, amp, freq, phase, dc, offset): 45 | return offset + amp * self.signal.square(2 * np.pi * freq * (x - phase), duty=dc) 46 | 47 | # -------------------------- Exponential Fit ---------------------------------------- 48 | 49 | def func(self, x, a, b, c): 50 | return a * np.exp(-x / b) + c 51 | 52 | def fit_exp(self, t, v): # accepts numpy arrays 53 | size = len(t) 54 | v80 = v[0] * 0.8 55 | for k in range(size - 1): 56 | if v[k] < v80: 57 | rc = t[k] / .223 58 | break 59 | pg = [v[0], rc, 0] 60 | po, err = self.optimize.curve_fit(self.func, t, v, pg) 61 | if abs(err[0][0]) > 0.1: 62 | return None, None 63 | vf = po[0] * np.exp(-t / po[1]) + po[2] 64 | return po, vf 65 | 66 | def squareFit(self, xReal, yReal): 67 | N = len(xReal) 68 | mx = yReal.max() 69 | mn = yReal.min() 70 | OFFSET = (mx + mn) / 2. 71 | amplitude = (np.average(yReal[yReal > OFFSET]) - np.average(yReal[yReal < OFFSET])) / 2.0 72 | yTmp = np.select([yReal < OFFSET, yReal > OFFSET], [0, 2]) 73 | bools = abs(np.diff(yTmp)) > 1 74 | edges = xReal[bools] 75 | levels = yTmp[bools] 76 | frequency = 1. / (edges[2] - edges[0]) 77 | 78 | phase = edges[0] # .5*np.pi*((yReal[0]-offset)/amplitude) 79 | dc = 0.5 80 | if len(edges) >= 4: 81 | if levels[0] == 0: 82 | dc = (edges[1] - edges[0]) / (edges[2] - edges[0]) 83 | else: 84 | dc = (edges[2] - edges[1]) / (edges[3] - edges[1]) 85 | phase = edges[1] 86 | 87 | guess = [amplitude, frequency, phase, dc, 0] 88 | 89 | try: 90 | (amplitude, frequency, phase, dc, offset), pcov = self.optimize.curve_fit(self.squareFunc, xReal, 91 | yReal - OFFSET, guess) 92 | offset += OFFSET 93 | 94 | if (frequency < 0): 95 | # print ('negative frq') 96 | return False 97 | 98 | freq = 1e6 * abs(frequency) 99 | amp = abs(amplitude) 100 | pcov[0] *= 1e6 101 | # print (pcov) 102 | if (abs(pcov[-1][0]) > 1e-6): 103 | False 104 | return [amp, freq, phase, dc, offset] 105 | except: 106 | return False 107 | 108 | def sineFit(self, xReal, yReal, **kwargs): 109 | N = len(xReal) 110 | OFFSET = (yReal.max() + yReal.min()) / 2. 111 | yhat = self.fftpack.rfft(yReal - OFFSET) 112 | idx = (yhat ** 2).argmax() 113 | freqs = self.fftpack.rfftfreq(N, d=(xReal[1] - xReal[0]) / (2 * np.pi)) 114 | frequency = kwargs.get('freq', freqs[idx]) 115 | frequency /= (2 * np.pi) # Convert angular velocity to freq 116 | amplitude = kwargs.get('amp', (yReal.max() - yReal.min()) / 2.0) 117 | phase = kwargs.get('phase', 0) # .5*np.pi*((yReal[0]-offset)/amplitude) 118 | guess = [amplitude, frequency, phase, 0] 119 | try: 120 | (amplitude, frequency, phase, offset), pcov = self.optimize.curve_fit(self.sineFunc, xReal, yReal - OFFSET, 121 | guess) 122 | offset += OFFSET 123 | ph = ((phase) * 180 / (np.pi)) 124 | if (frequency < 0): 125 | # print ('negative frq') 126 | return False 127 | 128 | if (amplitude < 0): 129 | ph -= 180 130 | 131 | if (ph < 0): 132 | ph = (ph + 720) % 360 133 | 134 | freq = 1e6 * abs(frequency) 135 | amp = abs(amplitude) 136 | pcov[0] *= 1e6 137 | # print (pcov) 138 | if (abs(pcov[-1][0]) > 1e-6): 139 | return False 140 | return [amp, freq, offset, ph] 141 | except: 142 | return False 143 | 144 | def find_frequency(self, v, si): # voltages, samplimg interval is seconds 145 | from numpy import fft 146 | NP = len(v) 147 | v = v - v.mean() # remove DC component 148 | frq = fft.fftfreq(NP, si)[:NP / 2] # take only the +ive half of the frequncy array 149 | amp = abs(fft.fft(v)[:NP / 2]) / NP # and the fft result 150 | index = amp.argmax() # search for the tallest peak, the fundamental 151 | return frq[index] 152 | 153 | def sineFit2(self, x, y, t, v): 154 | freq = self.find_frequency(y, x[1] - x[0]) 155 | amp = (y.max() - y.min()) / 2.0 156 | guess = [amp, freq, 0, 0] # amplitude, freq, phase,offset 157 | # print (guess) 158 | OS = y.mean() 159 | try: 160 | par, pcov = self.optimize.curve_fit(self.sineFunc, x, y - OS, guess) 161 | except: 162 | return None 163 | vf = self.sineFunc(t, par[0], par[1], par[2], par[3]) 164 | diff = sum((v - vf) ** 2) / max(v) 165 | if diff > self.error_limit: 166 | guess[2] += np.pi / 2 # try an out of phase 167 | try: 168 | # print 'L1: diff = %5.0f frset= %6.3f fr = %6.2f phi = %6.2f'%(diff, res,par[1]*1e6,par[2]) 169 | par, pcov = self.optimize.curve_fit(self.sineFunc, x, y, guess) 170 | except: 171 | return None 172 | vf = self.sineFunc(t, par[0], par[1], par[2], par[3]) 173 | diff = sum((v - vf) ** 2) / max(v) 174 | if diff > self.error_limit: 175 | # print 'L2: diff = %5.0f frset= %6.3f fr = %6.2f phi = %6.2f'%(diff, res,par[1]*1e6,par[2]) 176 | return None 177 | else: 178 | pass 179 | # print 'fixed ',par[1]*1e6 180 | return par, vf 181 | 182 | def amp_spectrum(self, v, si, nhar=8): 183 | # voltages, samplimg interval is seconds, number of harmonics to retain 184 | from numpy import fft 185 | NP = len(v) 186 | frq = fft.fftfreq(NP, si)[:NP / 2] # take only the +ive half of the frequncy array 187 | amp = abs(fft.fft(v)[:NP / 2]) / NP # and the fft result 188 | index = amp.argmax() # search for the tallest peak, the fundamental 189 | if index == 0: # DC component is dominating 190 | index = amp[4:].argmax() # skip frequencies close to zero 191 | return frq[:index * nhar], amp[:index * nhar] # restrict to 'nhar' harmonics 192 | 193 | def dampedSine(self, x, amp, freq, phase, offset, damp): 194 | """ 195 | A damped sine wave function 196 | 197 | """ 198 | return offset + amp * np.exp(-damp * x) * np.sin(abs(freq) * x + phase) 199 | 200 | def getGuessValues(self, xReal, yReal, func='sine'): 201 | if (func == 'sine' or func == 'damped sine'): 202 | N = len(xReal) 203 | offset = np.average(yReal) 204 | yhat = self.fftpack.rfft(yReal - offset) 205 | idx = (yhat ** 2).argmax() 206 | freqs = self.fftpack.rfftfreq(N, d=(xReal[1] - xReal[0]) / (2 * np.pi)) 207 | frequency = freqs[idx] 208 | 209 | amplitude = (yReal.max() - yReal.min()) / 2.0 210 | phase = 0. 211 | if func == 'sine': 212 | return amplitude, frequency, phase, offset 213 | if func == 'damped sine': 214 | return amplitude, frequency, phase, offset, 0 215 | 216 | def arbitFit(self, xReal, yReal, func, **args): 217 | N = len(xReal) 218 | guess = args.get('guess', []) 219 | try: 220 | results, pcov = self.optimize.curve_fit(func, xReal, yReal, guess) 221 | pcov[0] *= 1e6 222 | return True, results, pcov 223 | except: 224 | return False, [], [] 225 | 226 | def fft(self, ya, si): 227 | ''' 228 | Returns positive half of the Fourier transform of the signal ya. 229 | Sampling interval 'si', in milliseconds 230 | ''' 231 | ns = len(ya) 232 | if ns % 2 == 1: # odd values of np give exceptions 233 | ns -= 1 # make it even 234 | ya = ya[:-1] 235 | v = np.array(ya) 236 | tr = abs(np.fft.fft(v)) / ns 237 | frq = np.fft.fftfreq(ns, si) 238 | x = frq.reshape(2, ns // 2) 239 | y = tr.reshape(2, ns // 2) 240 | return x[0], y[0] 241 | 242 | def sineFitAndDisplay(self, chan, displayObject): 243 | ''' 244 | chan : an object containing a get_xaxis, and a get_yaxis method. 245 | displayObject : an object containing a setValue method 246 | 247 | Fits against a sine function, and writes to the object 248 | ''' 249 | fitres = None 250 | fit = '' 251 | try: 252 | fitres = self.sineFit(chan.get_xaxis(), chan.get_yaxis()) 253 | if fitres: 254 | amp, freq, offset, phase = fitres 255 | if amp > 0.05: fit = 'Voltage=%s\nFrequency=%s' % ( 256 | apply_si_prefix(amp, 'V'), apply_si_prefix(freq, 'Hz')) 257 | except Exception as e: 258 | fitres = None 259 | 260 | if not fitres or len(fit) == 0: fit = 'Voltage=%s\n' % (apply_si_prefix(np.average(chan.get_yaxis()), 'V')) 261 | displayObject.setValue(fit) 262 | if fitres: 263 | return fitres 264 | else: 265 | return 0, 0, 0, 0 266 | 267 | def rmsAndDisplay(self, data, displayObject): 268 | ''' 269 | data : an array of numbers 270 | displayObject : an object containing a setValue method 271 | 272 | Fits against a sine function, and writes to the object 273 | ''' 274 | rms = self.RMS(data) 275 | displayObject.setValue('Voltage=%s' % (apply_si_prefix(rms, 'V'))) 276 | return rms 277 | 278 | def RMS(self, data): 279 | data = np.array(data) 280 | return np.sqrt(np.average(data * data)) 281 | 282 | def butter_notch(self, lowcut, highcut, fs, order=5): 283 | from scipy.signal import butter 284 | nyq = 0.5 * fs 285 | low = lowcut / nyq 286 | high = highcut / nyq 287 | b, a = butter(order, [low, high], btype='bandstop') 288 | return b, a 289 | 290 | def butter_notch_filter(self, data, lowcut, highcut, fs, order=5): 291 | from scipy.signal import lfilter 292 | b, a = self.butter_notch(lowcut, highcut, fs, order=order) 293 | y = lfilter(b, a, data) 294 | return y 295 | 296 | 297 | SI_PREFIXES = {k: v for k, v in zip(range(-24, 25, 3), "yzafpnµm kMGTPEZY")} 298 | SI_PREFIXES[0] = "" 299 | 300 | 301 | def frexp10(x: float) -> Tuple[float, int]: 302 | """Return the base 10 fractional coefficient and exponent of x, as pair (m, e). 303 | 304 | This function is analogous to math.frexp, only for base 10 instead of base 2. 305 | If x is 0, m and e are both 0. Else 1 <= abs(m) < 10. Note that m * 10**e is not 306 | guaranteed to be exactly equal to x. 307 | 308 | Parameters 309 | ---------- 310 | x : float 311 | Number to be split into base 10 fractional coefficient and exponent. 312 | 313 | Returns 314 | ------- 315 | (float, int) 316 | Base 10 fractional coefficient and exponent of x. 317 | 318 | Examples 319 | -------- 320 | >>> frexp10(37) 321 | (3.7, 1) 322 | """ 323 | if x == 0: 324 | coefficient, exponent = 0.0, 0 325 | else: 326 | log10x = math.log10(abs(x)) 327 | exponent = int(math.copysign(math.floor(log10x), log10x)) 328 | coefficient = x / 10 ** exponent 329 | 330 | return coefficient, exponent 331 | 332 | 333 | def apply_si_prefix(value: float, unit: str, precision: int = 2) -> str: 334 | """Scale :value: and apply appropriate SI prefix to :unit:. 335 | 336 | Parameters 337 | ---------- 338 | value : float 339 | Number to be scaled. 340 | unit : str 341 | Base unit of :value: (without prefix). 342 | precision : int, optional 343 | :value: will be rounded to :precision: decimal places. The default value is 2. 344 | 345 | Returns 346 | ------- 347 | str 348 | " ", such that 1 <= < 1000. 349 | 350 | Examples 351 | ------- 352 | apply_si_prefix(0.03409, "V") 353 | '34.09 mV' 354 | """ 355 | coefficient, exponent = frexp10(value) 356 | si_exponent = exponent - (exponent % 3) 357 | si_coefficient = coefficient * 10 ** (exponent % 3) 358 | 359 | if abs(si_exponent) > max(SI_PREFIXES): 360 | raise ValueError("Exponent out of range of available prefixes.") 361 | 362 | return f"{si_coefficient:.{precision}f} {SI_PREFIXES[si_exponent]}{unit}" 363 | -------------------------------------------------------------------------------- /PSL/commands_proto.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | 4 | # allows to pack numeric values into byte strings 5 | Byte = struct.Struct("B") # size 1 6 | ShortInt = struct.Struct("H") # size 2 7 | Integer = struct.Struct("I") # size 4 8 | 9 | ACKNOWLEDGE = Byte.pack(254) 10 | MAX_SAMPLES = 10000 11 | DATA_SPLITTING = 200 12 | 13 | # /*----flash memory----*/ 14 | FLASH = Byte.pack(1) 15 | READ_FLASH = Byte.pack(1) 16 | WRITE_FLASH = Byte.pack(2) 17 | WRITE_BULK_FLASH = Byte.pack(3) 18 | READ_BULK_FLASH = Byte.pack(4) 19 | 20 | # /*-----ADC------*/ 21 | ADC = Byte.pack(2) 22 | CAPTURE_ONE = Byte.pack(1) 23 | CAPTURE_TWO = Byte.pack(2) 24 | CAPTURE_DMASPEED = Byte.pack(3) 25 | CAPTURE_FOUR = Byte.pack(4) 26 | CONFIGURE_TRIGGER = Byte.pack(5) 27 | GET_CAPTURE_STATUS = Byte.pack(6) 28 | GET_CAPTURE_CHANNEL = Byte.pack(7) 29 | SET_PGA_GAIN = Byte.pack(8) 30 | GET_VOLTAGE = Byte.pack(9) 31 | GET_VOLTAGE_SUMMED = Byte.pack(10) 32 | START_ADC_STREAMING = Byte.pack(11) 33 | SELECT_PGA_CHANNEL = Byte.pack(12) 34 | CAPTURE_12BIT = Byte.pack(13) 35 | CAPTURE_MULTIPLE = Byte.pack(14) 36 | SET_HI_CAPTURE = Byte.pack(15) 37 | SET_LO_CAPTURE = Byte.pack(16) 38 | 39 | MULTIPOINT_CAPACITANCE = Byte.pack(20) 40 | SET_CAP = Byte.pack(21) 41 | PULSE_TRAIN = Byte.pack(22) 42 | 43 | # /*-----SPI--------*/ 44 | SPI_HEADER = Byte.pack(3) 45 | START_SPI = Byte.pack(1) 46 | SEND_SPI8 = Byte.pack(2) 47 | SEND_SPI16 = Byte.pack(3) 48 | STOP_SPI = Byte.pack(4) 49 | SET_SPI_PARAMETERS = Byte.pack(5) 50 | SEND_SPI8_BURST = Byte.pack(6) 51 | SEND_SPI16_BURST = Byte.pack(7) 52 | # /*------I2C-------*/ 53 | I2C_HEADER = Byte.pack(4) 54 | I2C_START = Byte.pack(1) 55 | I2C_SEND = Byte.pack(2) 56 | I2C_STOP = Byte.pack(3) 57 | I2C_RESTART = Byte.pack(4) 58 | I2C_READ_END = Byte.pack(5) 59 | I2C_READ_MORE = Byte.pack(6) 60 | I2C_WAIT = Byte.pack(7) 61 | I2C_SEND_BURST = Byte.pack(8) 62 | I2C_CONFIG = Byte.pack(9) 63 | I2C_STATUS = Byte.pack(10) 64 | I2C_READ_BULK = Byte.pack(11) 65 | I2C_WRITE_BULK = Byte.pack(12) 66 | I2C_ENABLE_SMBUS = Byte.pack(13) 67 | I2C_INIT = Byte.pack(14) 68 | I2C_PULLDOWN_SCL = Byte.pack(15) 69 | I2C_DISABLE_SMBUS = Byte.pack(16) 70 | I2C_START_SCOPE = Byte.pack(17) 71 | 72 | # /*------UART2--------*/ 73 | UART_2 = Byte.pack(5) 74 | SEND_BYTE = Byte.pack(1) 75 | SEND_INT = Byte.pack(2) 76 | SEND_ADDRESS = Byte.pack(3) 77 | SET_BAUD = Byte.pack(4) 78 | SET_MODE = Byte.pack(5) 79 | READ_BYTE = Byte.pack(6) 80 | READ_INT = Byte.pack(7) 81 | READ_UART2_STATUS = Byte.pack(8) 82 | 83 | # /*-----------DAC--------*/ 84 | DAC = Byte.pack(6) 85 | SET_DAC = Byte.pack(1) 86 | SET_CALIBRATED_DAC = Byte.pack(2) 87 | 88 | # /*--------WAVEGEN-----*/ 89 | WAVEGEN = Byte.pack(7) 90 | SET_WG = Byte.pack(1) 91 | SET_SQR1 = Byte.pack(3) 92 | SET_SQR2 = Byte.pack(4) 93 | SET_SQRS = Byte.pack(5) 94 | TUNE_SINE_OSCILLATOR = Byte.pack(6) 95 | SQR4 = Byte.pack(7) 96 | MAP_REFERENCE = Byte.pack(8) 97 | SET_BOTH_WG = Byte.pack(9) 98 | SET_WAVEFORM_TYPE = Byte.pack(10) 99 | SELECT_FREQ_REGISTER = Byte.pack(11) 100 | DELAY_GENERATOR = Byte.pack(12) 101 | SET_SINE1 = Byte.pack(13) 102 | SET_SINE2 = Byte.pack(14) 103 | 104 | LOAD_WAVEFORM1 = Byte.pack(15) 105 | LOAD_WAVEFORM2 = Byte.pack(16) 106 | SQR1_PATTERN = Byte.pack(17) 107 | # /*-----digital outputs----*/ 108 | DOUT = Byte.pack(8) 109 | SET_STATE = Byte.pack(1) 110 | 111 | # /*-----digital inputs-----*/ 112 | DIN = Byte.pack(9) 113 | GET_STATE = Byte.pack(1) 114 | GET_STATES = Byte.pack(2) 115 | 116 | ID1 = Byte.pack(0) 117 | ID2 = Byte.pack(1) 118 | ID3 = Byte.pack(2) 119 | ID4 = Byte.pack(3) 120 | LMETER = Byte.pack(4) 121 | 122 | # /*------TIMING FUNCTIONS-----*/ 123 | TIMING = Byte.pack(10) 124 | GET_TIMING = Byte.pack(1) 125 | GET_PULSE_TIME = Byte.pack(2) 126 | GET_DUTY_CYCLE = Byte.pack(3) 127 | START_ONE_CHAN_LA = Byte.pack(4) 128 | START_TWO_CHAN_LA = Byte.pack(5) 129 | START_FOUR_CHAN_LA = Byte.pack(6) 130 | FETCH_DMA_DATA = Byte.pack(7) 131 | FETCH_INT_DMA_DATA = Byte.pack(8) 132 | FETCH_LONG_DMA_DATA = Byte.pack(9) 133 | GET_LA_PROGRESS = Byte.pack(10) 134 | GET_INITIAL_DIGITAL_STATES = Byte.pack(11) 135 | 136 | TIMING_MEASUREMENTS = Byte.pack(12) 137 | INTERVAL_MEASUREMENTS = Byte.pack(13) 138 | CONFIGURE_COMPARATOR = Byte.pack(14) 139 | START_ALTERNATE_ONE_CHAN_LA = Byte.pack(15) 140 | START_THREE_CHAN_LA = Byte.pack(16) 141 | STOP_LA = Byte.pack(17) 142 | 143 | # /*--------MISCELLANEOUS------*/ 144 | COMMON = Byte.pack(11) 145 | 146 | GET_CTMU_VOLTAGE = Byte.pack(1) 147 | GET_CAPACITANCE = Byte.pack(2) 148 | GET_FREQUENCY = Byte.pack(3) 149 | GET_INDUCTANCE = Byte.pack(4) 150 | 151 | GET_VERSION = Byte.pack(5) 152 | 153 | RETRIEVE_BUFFER = Byte.pack(8) 154 | GET_HIGH_FREQUENCY = Byte.pack(9) 155 | CLEAR_BUFFER = Byte.pack(10) 156 | SET_RGB1 = Byte.pack(11) 157 | READ_PROGRAM_ADDRESS = Byte.pack(12) 158 | WRITE_PROGRAM_ADDRESS = Byte.pack(13) 159 | READ_DATA_ADDRESS = Byte.pack(14) 160 | WRITE_DATA_ADDRESS = Byte.pack(15) 161 | 162 | GET_CAP_RANGE = Byte.pack(16) 163 | SET_RGB2 = Byte.pack(17) 164 | READ_LOG = Byte.pack(18) 165 | RESTORE_STANDALONE = Byte.pack(19) 166 | GET_ALTERNATE_HIGH_FREQUENCY = Byte.pack(20) 167 | SET_RGB3 = Byte.pack(22) 168 | 169 | START_CTMU = Byte.pack(23) 170 | STOP_CTMU = Byte.pack(24) 171 | 172 | START_COUNTING = Byte.pack(25) 173 | FETCH_COUNT = Byte.pack(26) 174 | FILL_BUFFER = Byte.pack(27) 175 | 176 | # /*---------- BAUDRATE for main comm channel----*/ 177 | SETBAUD = Byte.pack(12) 178 | BAUD9600 = Byte.pack(1) 179 | BAUD14400 = Byte.pack(2) 180 | BAUD19200 = Byte.pack(3) 181 | BAUD28800 = Byte.pack(4) 182 | BAUD38400 = Byte.pack(5) 183 | BAUD57600 = Byte.pack(6) 184 | BAUD115200 = Byte.pack(7) 185 | BAUD230400 = Byte.pack(8) 186 | BAUD1000000 = Byte.pack(9) 187 | 188 | # /*-----------NRFL01 radio module----------*/ 189 | NRFL01 = Byte.pack(13) 190 | NRF_SETUP = Byte.pack(1) 191 | NRF_RXMODE = Byte.pack(2) 192 | NRF_TXMODE = Byte.pack(3) 193 | NRF_POWER_DOWN = Byte.pack(4) 194 | NRF_RXCHAR = Byte.pack(5) 195 | NRF_TXCHAR = Byte.pack(6) 196 | NRF_HASDATA = Byte.pack(7) 197 | NRF_FLUSH = Byte.pack(8) 198 | NRF_WRITEREG = Byte.pack(9) 199 | NRF_READREG = Byte.pack(10) 200 | NRF_GETSTATUS = Byte.pack(11) 201 | NRF_WRITECOMMAND = Byte.pack(12) 202 | NRF_WRITEPAYLOAD = Byte.pack(13) 203 | NRF_READPAYLOAD = Byte.pack(14) 204 | NRF_WRITEADDRESS = Byte.pack(15) 205 | NRF_TRANSACTION = Byte.pack(16) 206 | NRF_START_TOKEN_MANAGER = Byte.pack(17) 207 | NRF_STOP_TOKEN_MANAGER = Byte.pack(18) 208 | NRF_TOTAL_TOKENS = Byte.pack(19) 209 | NRF_REPORTS = Byte.pack(20) 210 | NRF_WRITE_REPORT = Byte.pack(21) 211 | NRF_DELETE_REPORT_ROW = Byte.pack(22) 212 | 213 | NRF_WRITEADDRESSES = Byte.pack(23) 214 | 215 | # ---------Non standard IO protocols-------- 216 | 217 | NONSTANDARD_IO = Byte.pack(14) 218 | HX711_HEADER = Byte.pack(1) 219 | HCSR04_HEADER = Byte.pack(2) 220 | AM2302_HEADER = Byte.pack(3) 221 | TCD1304_HEADER = Byte.pack(4) 222 | STEPPER_MOTOR = Byte.pack(5) 223 | 224 | # --------COMMUNICATION PASSTHROUGHS-------- 225 | # Data sent to the device is directly routed to output ports such as (SCL, SDA for UART) 226 | 227 | PASSTHROUGHS = Byte.pack(15) 228 | PASS_UART = Byte.pack(1) 229 | 230 | # /*--------STOP STREAMING------*/ 231 | STOP_STREAMING = Byte.pack(253) 232 | 233 | # /*------INPUT CAPTURE---------*/ 234 | # capture modes 235 | EVERY_SIXTEENTH_RISING_EDGE = Byte.pack(0b101) 236 | EVERY_FOURTH_RISING_EDGE = Byte.pack(0b100) 237 | EVERY_RISING_EDGE = Byte.pack(0b011) 238 | EVERY_FALLING_EDGE = Byte.pack(0b010) 239 | EVERY_EDGE = Byte.pack(0b001) 240 | DISABLED = Byte.pack(0b000) 241 | 242 | # /*--------Chip selects-----------*/ 243 | CSA1 = Byte.pack(1) 244 | CSA2 = Byte.pack(2) 245 | CSA3 = Byte.pack(3) 246 | CSA4 = Byte.pack(4) 247 | CSA5 = Byte.pack(5) 248 | CS1 = Byte.pack(6) 249 | CS2 = Byte.pack(7) 250 | 251 | # resolutions 252 | TEN_BIT = Byte.pack(10) 253 | TWELVE_BIT = Byte.pack(12) 254 | 255 | 256 | ''' 257 | def reverse_bits(x): 258 | return int('{:08b}'.format(x)[::-1], 2) 259 | 260 | def InttoString(val): 261 | return ShortInt.pack(int(val)) 262 | 263 | def StringtoInt(string): 264 | return ShortInt.unpack(string)[0] 265 | 266 | def StringtoLong(string): 267 | return Integer.unpack(string)[0] 268 | 269 | def getval12(val): 270 | return val*3.3/4095 271 | 272 | def getval10(val): 273 | return val*3.3/1023 274 | 275 | 276 | def getL(F,C): 277 | return 1.0/(C*4*math.pi*math.pi*F*F) 278 | 279 | def getF(L,C): 280 | return 1.0/(2*math.pi*math.sqrt(L*C)) 281 | 282 | def getLx(f1,f2,f3,Ccal): 283 | a=(f1/f3)**2 284 | b=(f1/f2)**2 285 | c=(2*math.pi*f1)**2 286 | return (a-1)*(b-1)/(Ccal*c) 287 | 288 | ''' 289 | -------------------------------------------------------------------------------- /PSL/digital_channel.py: -------------------------------------------------------------------------------- 1 | """Objects related to the PSLab's digital input channels. 2 | """ 3 | 4 | import numpy as np 5 | 6 | DIGITAL_INPUTS = ("ID1", "ID2", "ID3", "ID4", "SEN", "EXT", "CNTR") 7 | 8 | MODES = { 9 | "sixteen rising": 5, 10 | "four rising": 4, 11 | "rising": 3, 12 | "falling": 2, 13 | "any": 1, 14 | "disabled": 0, 15 | } 16 | 17 | 18 | class DigitalInput: 19 | """Model of the PSLab's digital inputs. 20 | 21 | Parameters 22 | ---------- 23 | name : {"ID1", "ID2", "ID3", "ID4", "SEN", "EXT", "CNTR"} 24 | Name of the digital channel to model. 25 | 26 | Attributes 27 | ---------- 28 | name : str 29 | One of {"ID1", "ID2", "ID3", "ID4", "SEN", "EXT", "CNTR"}. 30 | number : int 31 | Number used to refer to this channel in the firmware. 32 | datatype : str 33 | Either "int" or "long", depending on if a 16 or 32-bit counter is used to 34 | capture timestamps for this channel. 35 | events_in_buffer : int 36 | Number of logic events detected on this channel, the timestamps of which are 37 | currently being held in the device's ADC buffer. 38 | buffer_idx : Union[int, None] 39 | Location in the device's ADC buffer where the events are stored. None if no 40 | events captured by this channel are currently held in the buffer. 41 | logic_mode 42 | """ 43 | 44 | def __init__(self, name: str): 45 | self.name = name 46 | self.number = DIGITAL_INPUTS.index(self.name) 47 | self.datatype = "long" 48 | self.events_in_buffer = 0 49 | self._events_in_buffer = 0 50 | self.buffer_idx = None 51 | self._logic_mode = MODES["any"] 52 | 53 | @property 54 | def logic_mode(self) -> str: 55 | """Get or set the type of logic event which should be captured on this channel. 56 | 57 | The options are: 58 | any: Capture every edge. 59 | rising: Capture every rising edge. 60 | falling: Capture every falling edge. 61 | four rising: Capture every fourth rising edge. 62 | sixteen rising: Capture every fourth rising edge. 63 | """ 64 | return {v: k for k, v in MODES.items()}[self._logic_mode] 65 | 66 | def _get_xy(self, initial_state: bool, timestamps: np.ndarray): 67 | x = np.repeat(timestamps, 3) 68 | x = np.insert(x, 0, 0) 69 | x[0] = 0 70 | y = np.array(len(x) * [False]) 71 | 72 | if self.logic_mode == "any": 73 | y[0] = initial_state 74 | for i in range(1, len(x), 3): 75 | y[i] = y[i - 1] # Value before this timestamp. 76 | y[i + 1] = not y[i] # Value at this timestamp. 77 | y[i + 2] = y[i + 1] # Value leaving this timetamp. 78 | elif self.logic_mode == "falling": 79 | y[0] = True 80 | for i in range(1, len(x), 3): 81 | y[i] = True # Value before this timestamp. 82 | y[i + 1] = False # Value at this timestamp. 83 | y[i + 2] = True # Value leaving this timetamp. 84 | else: 85 | y[0] = False 86 | for i in range(1, len(x), 3): 87 | y[i] = False # Value before this timestamp. 88 | y[i + 1] = True # Value at this timestamp. 89 | y[i + 2] = False # Value leaving this timetamp. 90 | 91 | return x, y 92 | -------------------------------------------------------------------------------- /PSL/oscilloscope.py: -------------------------------------------------------------------------------- 1 | """Classes and functions related to the PSLab's oscilloscope instrument. 2 | 3 | Example 4 | ------- 5 | >>> from PSL.oscilloscope import Oscilloscope 6 | >>> scope = Oscilloscope() 7 | >>> x, y1, y2, y3, y4 = scope.capture(channels=4, samples=1600, timegap=2) 8 | """ 9 | 10 | import time 11 | from typing import Tuple, Union 12 | 13 | import numpy as np 14 | 15 | import PSL.commands_proto as CP 16 | from PSL import achan, packet_handler 17 | 18 | 19 | class Oscilloscope: 20 | """Capture varying voltage signals on up to four channels simultaneously. 21 | 22 | Parameters 23 | ---------- 24 | device : :class:`Handler`, optional 25 | Serial interface for communicating with the PSLab device. If not provided, a 26 | new one will be created. 27 | 28 | Attributes 29 | ---------- 30 | channel_one_map : str 31 | Remap the first sampled channel. The default value is "CH1". 32 | trigger_enabled 33 | """ 34 | 35 | MAX_SAMPLES = 10000 36 | CH234 = ["CH2", "CH3", "MIC"] 37 | 38 | def __init__(self, device: packet_handler.Handler = None): 39 | self._device = packet_handler.Handler() if device is None else device 40 | self._channels = { 41 | a: achan.AnalogInput(a, self._device) for a in achan.ANALOG_CHANNELS 42 | } 43 | self.channel_one_map = "CH1" 44 | self._trigger_voltage = 0 45 | self._trigger_enabled = False 46 | self._trigger_channel = "CH1" 47 | 48 | def capture(self, channels: int, samples: int, timegap: float,) -> np.ndarray: 49 | """Capture an oscilloscope trace from the specified input channels. 50 | 51 | This is a blocking call. 52 | 53 | Parameters 54 | ---------- 55 | channels : {1, 2, 4} 56 | Number of channels to sample from simultaneously. By default, samples are 57 | captured from CH1, CH2, CH3 and MIC. CH1 can be remapped to any other 58 | channel (CH2, CH3, MIC, CAP, SEN, AN8) by setting the channel_one_map 59 | attribute of the Oscilloscope instance to the desired channel. 60 | samples : int 61 | Number of samples to fetch. Maximum 10000 divided by number of channels. 62 | timegap : float 63 | Time gap between samples in microseconds. Will be rounded to the closest 64 | 1 / 8 µs. The minimum time gap depends on the type of measurement: 65 | +--------------+------------+----------+------------+ 66 | | Simultaneous | No trigger | Trigger | No trigger | 67 | | channels | (10-bit) | (10-bit) | (12-bit) | 68 | +==============+============+==========+============+ 69 | | 1 | 0.5 µs | 0.75 µs | 1 µs | 70 | +--------------+------------+----------+------------+ 71 | | 2 | 0.875 µs | 0.875 µs | N/A | 72 | +--------------+------------+----------+------------+ 73 | | 4 | 1.75 µs | 1.75 µs | N/A | 74 | +--------------+------------+----------+------------+ 75 | Sample resolution is set automatically based on the above limitations; i.e. 76 | to get 12-bit samples only one channel may be sampled, there must be no 77 | active trigger, and the time gap must be 1 µs or greater. 78 | 79 | Example 80 | ------- 81 | >>> from PSL.oscilloscope import Oscilloscope 82 | >>> scope = Oscilloscope() 83 | >>> x, y = scope.capture(1, 3200, 1) 84 | 85 | Returns 86 | ------- 87 | numpy.ndarray 88 | (:channels:+1)-dimensional array with timestamps in the first dimension 89 | and corresponding voltages in the following dimensions. 90 | 91 | Raises 92 | ------ 93 | ValueError 94 | If :channels: is not 1, 2 or 4, or 95 | :samples: > 10000 / :channels:, or 96 | :channel_one_map: is not one of CH1, CH2, CH3, MIC, CAP, SEN, AN8, or 97 | :timegap: is too low. 98 | """ 99 | xy = np.zeros([channels + 1, samples]) 100 | xy[0] = self.capture_nonblocking(channels, samples, timegap) 101 | time.sleep(1e-6 * samples * timegap + 0.01) 102 | 103 | while not self.progress()[0]: 104 | pass 105 | 106 | active_channels = ([self.channel_one_map] + self.CH234)[:channels] 107 | for e, c in enumerate(active_channels): 108 | xy[e + 1] = self.fetch_data(c) 109 | 110 | return xy 111 | 112 | def capture_nonblocking( 113 | self, channels: int, samples: int, timegap: float 114 | ) -> np.ndarray: 115 | """Tell the pslab to start sampling the specified input channels. 116 | 117 | This method is identical to 118 | :meth:`capture `, 119 | except it does not block while the samples are being captured. Collected 120 | samples must be manually fetched by calling 121 | :meth:`fetch_data`. 122 | 123 | Parameters 124 | ---------- 125 | See :meth:`capture `. 126 | 127 | Example 128 | ------- 129 | >>> import time 130 | >>> from PSL.oscilloscope import Oscilloscope 131 | >>> scope = Oscilloscope() 132 | >>> x = scope.capture_nonblocking(1, 3200, 1) 133 | >>> time.sleep(3200 * 1e-6) 134 | >>> y = scope.fetch_data("CH1") 135 | 136 | Returns 137 | ------- 138 | numpy.ndarray 139 | One-dimensional array of timestamps. 140 | 141 | Raises 142 | ------ 143 | See :meth:`capture `. 144 | """ 145 | self._check_args(channels, samples, timegap) 146 | timegap = int(timegap * 8) / 8 147 | self._capture(channels, samples, timegap) 148 | return timegap * np.arange(samples) 149 | 150 | def _check_args(self, channels: int, samples: int, timegap: float): 151 | if channels not in (1, 2, 4): 152 | raise ValueError("Number of channels to sample must be 1, 2, or 4.") 153 | 154 | max_samples = self.MAX_SAMPLES // channels 155 | if not 0 < samples <= max_samples: 156 | e1 = f"Cannot collect more than {max_samples} when sampling from " 157 | e2 = f"{channels} channels." 158 | raise ValueError(e1 + e2) 159 | 160 | min_timegap = self._lookup_mininum_timegap(channels) 161 | if timegap < min_timegap: 162 | raise ValueError(f"timegap must be at least {min_timegap}.") 163 | 164 | if self.channel_one_map not in self._channels: 165 | e1 = f"{self.channel_one_map} is not a valid channel. " 166 | e2 = f"Valid channels are {list(self._channels.keys())}." 167 | raise ValueError(e1 + e2) 168 | 169 | def _lookup_mininum_timegap(self, channels: int) -> float: 170 | channels_idx = { 171 | 1: 0, 172 | 2: 1, 173 | 4: 2, 174 | } 175 | min_timegaps = [[0.5, 0.75], [0.875, 0.875], [1.75, 1.75]] 176 | 177 | return min_timegaps[channels_idx[channels]][self.trigger_enabled] 178 | 179 | def _capture(self, channels: int, samples: int, timegap: float): 180 | self._invalidate_buffer() 181 | chosa = self._channels[self.channel_one_map].chosa 182 | self._channels[self.channel_one_map].resolution = 10 183 | self._device.send_byte(CP.ADC) 184 | 185 | CH123SA = 0 # TODO what is this? 186 | chosa = self._channels[self.channel_one_map].chosa 187 | self._channels[self.channel_one_map].samples_in_buffer = samples 188 | self._channels[self.channel_one_map].buffer_idx = 0 189 | if channels == 1: 190 | if self.trigger_enabled: 191 | self._device.send_byte(CP.CAPTURE_ONE) 192 | self._device.send_byte(chosa | 0x80) # Trigger 193 | elif timegap >= 1: 194 | self._channels[self.channel_one_map].resolution = 12 195 | self._device.send_byte(CP.CAPTURE_DMASPEED) 196 | self._device.send_byte(chosa | 0x80) # 12-bit mode 197 | else: 198 | self._device.send_byte(CP.CAPTURE_DMASPEED) 199 | self._device.send_byte(chosa) # 10-bit mode 200 | elif channels == 2: 201 | self._channels["CH2"].resolution = 10 202 | self._channels["CH2"].samples_in_buffer = samples 203 | self._channels["CH2"].buffer_idx = 1 * samples 204 | self._device.send_byte(CP.CAPTURE_TWO) 205 | self._device.send_byte(chosa | (0x80 * self.trigger_enabled)) 206 | else: 207 | for e, c in enumerate(self.CH234): 208 | self._channels[c].resolution = 10 209 | self._channels[c].samples_in_buffer = samples 210 | self._channels[c].buffer_idx = (e + 1) * samples 211 | self._device.send_byte(CP.CAPTURE_FOUR) 212 | self._device.send_byte( 213 | chosa | (CH123SA << 4) | (0x80 * self.trigger_enabled) 214 | ) 215 | 216 | self._device.send_int(samples) 217 | self._device.send_int(int(timegap * 8)) # 8 MHz clock 218 | self._device.get_ack() 219 | 220 | def _invalidate_buffer(self): 221 | for c in self._channels.values(): 222 | c.samples_in_buffer = 0 223 | c.buffer_idx = None 224 | 225 | def fetch_data(self, channel: str) -> np.ndarray: 226 | """Fetch samples captured from specified channel. 227 | 228 | Parameters 229 | ---------- 230 | channel : {'CH1', 'CH2', 'CH3', 'MIC', 'CAP', 'SEN', 'AN8'} 231 | Name of the channel from which to fetch captured data. 232 | 233 | Example 234 | ------- 235 | >>> from PSL.oscilloscope import Oscilloscope 236 | >>> scope = Oscilloscope() 237 | >>> scope.capture_nonblocking(channels=2, samples=1600, timegap=1) 238 | >>> y1 = scope.fetch_data("CH1") 239 | >>> y2 = scope.fetch_data("CH2") 240 | 241 | Returns 242 | ------- 243 | numpy.ndarray 244 | One-dimensional array holding the requested voltages. 245 | """ 246 | data = bytearray() 247 | channel = self._channels[channel] 248 | samples = channel.samples_in_buffer 249 | 250 | for i in range(int(np.ceil(samples / CP.DATA_SPLITTING))): 251 | self._device.send_byte(CP.COMMON) 252 | self._device.send_byte(CP.RETRIEVE_BUFFER) 253 | offset = channel.buffer_idx + i * CP.DATA_SPLITTING 254 | self._device.send_int(offset) 255 | self._device.send_int(CP.DATA_SPLITTING) # Ints to read 256 | # Reading int by int sometimes causes a communication error. 257 | data += self._device.interface.read(CP.DATA_SPLITTING * 2) 258 | self._device.get_ack() 259 | 260 | data = [CP.ShortInt.unpack(data[s * 2 : s * 2 + 2])[0] for s in range(samples)] 261 | 262 | return channel.scale(np.array(data)) 263 | 264 | def progress(self) -> Tuple[bool, int]: 265 | """Return the status of a capture call. 266 | 267 | Returns 268 | ------- 269 | bool, int 270 | A boolean indicating whether the capture is complete, followed by the 271 | number of samples currently held in the buffer. 272 | """ 273 | self._device.send_byte(CP.ADC) 274 | self._device.send_byte(CP.GET_CAPTURE_STATUS) 275 | conversion_done = self._device.get_byte() 276 | samples = self._device.get_int() 277 | self._device.get_ack() 278 | 279 | return bool(conversion_done), samples 280 | 281 | def configure_trigger(self, channel: str, voltage: float, prescaler: int = 0): 282 | """Configure trigger parameters for 10-bit capture routines. 283 | 284 | The capture routines will wait until a rising edge of the input signal crosses 285 | the specified level. The trigger will timeout within 8 ms, and capture will 286 | start regardless. 287 | 288 | To disable the trigger after configuration, set the trigger_enabled attribute 289 | of the Oscilloscope instance to False. 290 | 291 | Parameters 292 | ---------- 293 | channel : {'CH1', 'CH2', 'CH3', 'MIC', 'CAP', 'SEN', 'AN8'} 294 | The name of the trigger channel. 295 | voltage : float 296 | The trigger voltage in volts. 297 | prescaler : int, optional 298 | The default value is 0. 299 | 300 | Examples 301 | -------- 302 | >>> from PSL.oscilloscope import Oscilloscope 303 | >>> scope = Oscilloscope() 304 | >>> scope.configure_trigger(channel='CH1', voltage=1.1) 305 | >>> xy = scope.capture(channels=1, samples=800, timegap=2) 306 | >>> diff = abs(xy[1, 0] - 1.1) # Should be small unless a timeout occurred. 307 | 308 | Raises 309 | ------ 310 | TypeError 311 | If the trigger channel is set to a channel which cannot be sampled. 312 | """ 313 | self._trigger_channel = channel 314 | 315 | if channel == self.channel_one_map: 316 | channel = 0 317 | elif channel in self.CH234: 318 | channel = self.CH234.index(channel) + 1 319 | elif self.channel_one_map == "CH1": 320 | e = f"Cannot trigger on {channel} unless it is remapped to CH1." 321 | raise TypeError(e) 322 | else: 323 | e = f"Cannot trigger on CH1 when {self.channel_one_map} is mapped to CH1." 324 | raise TypeError(e) 325 | 326 | self._device.send_byte(CP.ADC) 327 | self._device.send_byte(CP.CONFIGURE_TRIGGER) 328 | # Trigger channel (4lsb) , trigger timeout prescaler (4msb) 329 | self._device.send_byte((prescaler << 4) | (1 << channel)) # TODO prescaler? 330 | level = self._channels[self._trigger_channel].unscale(voltage) 331 | self._device.send_int(level) 332 | self._device.get_ack() 333 | self._trigger_enabled = True 334 | 335 | @property 336 | def trigger_enabled(self) -> bool: 337 | """Activate or deactivate a trigger set by :meth:`configure_trigger`. 338 | 339 | Becomes True automatically when calling :meth:`configure_trigger`. 340 | 341 | If trigger_enabled is set to True without first calling 342 | :meth:`configure_trigger`, the trigger will be configured with a trigger 343 | voltage of 0 V on CH1. 344 | """ 345 | return self._trigger_enabled 346 | 347 | @trigger_enabled.setter 348 | def trigger_enabled(self, value: bool): 349 | self._trigger_enabled = value 350 | if self._trigger_enabled: 351 | self.configure_trigger(self._trigger_channel, self._trigger_voltage) 352 | 353 | def select_range(self, channel: str, voltage_range: Union[int, float]): 354 | """Set appropriate gain automatically. 355 | 356 | Setting the right voltage range will result in better resolution. 357 | 358 | Parameters 359 | ---------- 360 | channel : {'CH1', 'CH2'} 361 | Channel on which to apply gain. 362 | voltage_range : {16,8,4,3,2,1.5,1,.5} 363 | 364 | Examples 365 | -------- 366 | >>> from PSL.oscilloscope import Oscilloscope 367 | >>> scope = Oscilloscope() 368 | >>> scope.select_range('CH1', 8) 369 | # Gain set to 2x on CH1. Voltage range ±8 V. 370 | """ 371 | ranges = [16, 8, 4, 3, 2, 1.5, 1, 0.5] 372 | if voltage_range in ranges: 373 | idx = ranges.index(voltage_range) 374 | gain = achan.GAIN_VALUES[idx] 375 | self._channels[channel].gain = gain 376 | else: 377 | e = f"Invalid range: {voltage_range}. Valid ranges are {ranges}." 378 | raise ValueError(e) 379 | -------------------------------------------------------------------------------- /PSL/packet_handler.py: -------------------------------------------------------------------------------- 1 | """Low-level communication for PSLab. 2 | 3 | Example 4 | ------- 5 | >>> from PSL.packet_handler import Handler 6 | >>> device = Handler() 7 | >>> version = device.get_version() 8 | >>> device.disconnect() 9 | """ 10 | import logging 11 | import struct 12 | import time 13 | from functools import partial 14 | from typing import List, Union 15 | 16 | import serial 17 | from serial.tools import list_ports 18 | 19 | import PSL.commands_proto as CP 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | USB_VID = 0x04D8 24 | USB_PID = 0x00DF 25 | 26 | 27 | class Handler: 28 | """Provides methods for communicating with the PSLab hardware. 29 | 30 | When instantiated, Handler tries to connect to the PSLab. A port can optionally 31 | be specified; otherwise Handler will try to find the correct port automatically. 32 | 33 | Parameters 34 | ---------- 35 | See :meth:`connect. `. 36 | """ 37 | 38 | def __init__( 39 | self, 40 | port: str = None, 41 | baudrate: int = 1000000, 42 | timeout: float = 1.0, 43 | **kwargs, # Backward compatibility 44 | ): 45 | self.burst_buffer = b"" 46 | self.load_burst = False 47 | self.input_queue_size = 0 48 | self.version = "" 49 | self._log = b"" 50 | self._logging = False 51 | self.interface = serial.Serial() 52 | self.send_byte = partial(self._send, size=1) 53 | self.send_int = partial(self._send, size=2) 54 | self.get_byte = partial(self._receive, size=1) 55 | self.get_int = partial(self._receive, size=2) 56 | self.get_long = partial(self._receive, size=4) 57 | self.connect(port=port, baudrate=baudrate, timeout=timeout) 58 | 59 | # Backwards compatibility 60 | self.fd = self.interface 61 | self.occupiedPorts = set() 62 | self.connected = self.interface.is_open 63 | self.__sendByte__ = partial(self._send, size=1) 64 | self.__sendInt__ = partial(self._send, size=2) 65 | self.__get_ack__ = self.get_ack 66 | self.__getByte__ = partial(self._receive, size=1) 67 | self.__getInt__ = partial(self._receive, size=2) 68 | self.__getLong__ = partial(self._receive, size=4) 69 | self.waitForData = self.wait_for_data 70 | self.sendBurst = self.send_burst 71 | self.portname = self.interface.name 72 | self.listPorts = self._list_ports 73 | 74 | @staticmethod 75 | def _list_ports() -> List[str]: # Promote to public? 76 | """Return a list of serial port names.""" 77 | return [p.device for p in list_ports.comports()] 78 | 79 | def connect( 80 | self, port: str = None, baudrate: int = 1000000, timeout: float = 1.0, 81 | ): 82 | """Connect to PSLab. 83 | 84 | Parameters 85 | ---------- 86 | port : str, optional 87 | The name of the port to which the PSLab is connected as a string. On 88 | Posix this is a path, e.g. "/dev/ttyACM0". On Windows, it's a numbered 89 | COM port, e.g. "COM5". Will be autodetected if not specified. 90 | baudrate : int, optional 91 | Symbol rate in bit/s. The default value is 1000000. 92 | timeout : float, optional 93 | Time in seconds to wait before cancelling a read or write command. The 94 | default value is 1.0. 95 | 96 | Raises 97 | ------ 98 | SerialException 99 | If connection could not be established. 100 | """ 101 | # serial.Serial opens automatically if port is not None. 102 | self.interface = serial.Serial( 103 | port=port, baudrate=baudrate, timeout=timeout, write_timeout=timeout, 104 | ) 105 | 106 | if self.interface.is_open: 107 | # User specified a port. 108 | version = self.get_version() 109 | else: 110 | port_info_generator = list_ports.grep(f"{USB_VID:04x}:{USB_PID:04x}") 111 | 112 | for port_info in port_info_generator: 113 | self.interface.port = port_info.device 114 | self.interface.open() 115 | version = self.get_version() 116 | if any(expected in version for expected in ["PSLab", "CSpark"]): 117 | break 118 | else: 119 | version = "" 120 | 121 | if any(expected in version for expected in ["PSLab", "CSpark"]): 122 | self.version = version 123 | self.fd = self.interface # Backward compatibility 124 | logger.info(f"Connected to {self.version} on {self.interface.port}.") 125 | else: 126 | self.interface.close() 127 | self.version = "" 128 | raise serial.SerialException("Device not found.") 129 | 130 | def disconnect(self): 131 | """Disconnect from PSLab.""" 132 | self.interface.close() 133 | 134 | def reconnect( 135 | self, port: str = None, baudrate: int = None, timeout: float = None, 136 | ): 137 | """Reconnect to PSLab. 138 | 139 | Will reuse previous settings (port, baudrate, timeout) unless new ones are 140 | provided. 141 | 142 | Parameters 143 | ---------- 144 | See :meth:`connect. `. 145 | """ 146 | self.disconnect() 147 | 148 | # Reuse previous settings unless user provided new ones. 149 | baudrate = self.interface.baudrate if baudrate is None else baudrate 150 | port = self.interface.port if port is None else port 151 | timeout = self.interface.timeout if timeout is None else timeout 152 | 153 | self.interface = serial.Serial( 154 | port=port, baudrate=baudrate, timeout=timeout, write_timeout=timeout, 155 | ) 156 | self.connect() 157 | 158 | def __del__(self): # Is this necessary? 159 | """Disconnect before garbage collection.""" 160 | self.interface.close() 161 | 162 | def get_version(self, *args) -> str: # *args for backwards compatibility 163 | """Query PSLab for its version and return it as a decoded string. 164 | 165 | Returns 166 | ------- 167 | str 168 | Version string. 169 | """ 170 | self.send_byte(CP.COMMON) 171 | self.send_byte(CP.GET_VERSION) 172 | version = self.interface.readline() 173 | self._write_log(version, "RX") 174 | return version.decode("utf-8") 175 | 176 | def get_ack(self) -> int: # Make _internal? 177 | """Get response code from PSLab. 178 | 179 | Also functions as handshake. 180 | 181 | Returns 182 | ------- 183 | int 184 | Response code. Meanings: 185 | 1 SUCCESS 186 | 2 ARGUMENT_ERROR 187 | 3 FAILED 188 | """ 189 | if not self.load_burst: 190 | response = self.read(1) 191 | else: 192 | self.input_queue_size += 1 193 | return 1 194 | 195 | try: 196 | return CP.Byte.unpack(response)[0] 197 | except Exception as e: 198 | logger.error(e) 199 | return 3 # raise exception instead? 200 | 201 | @staticmethod 202 | def _get_integer_type(size: int) -> struct.Struct: 203 | if size == 1: 204 | return CP.Byte 205 | elif size == 2: 206 | return CP.ShortInt 207 | elif size == 4: 208 | return CP.Integer 209 | else: 210 | raise ValueError("size must be 1, 2, or 4.") 211 | 212 | def _send(self, value: Union[bytes, int], size: int = None): 213 | """Send a value to the PSLab. 214 | 215 | Optionally handles conversion from int to bytes. 216 | 217 | Parameters 218 | ---------- 219 | value : bytes, int 220 | Value to send to PSLab. Must fit in four bytes. 221 | size : int, optional 222 | Number of bytes to send. If not specified, the number of bytes sent 223 | depends on the size of :value:. 224 | """ 225 | if isinstance(value, bytes): 226 | packet = value 227 | else: 228 | # True + True == 2, see PEP 285. 229 | size = 2 ** ((value > 0xFF) + (value > 0xFFFF)) if size is None else size 230 | packer = self._get_integer_type(size) 231 | packet = packer.pack(value) 232 | 233 | if self.load_burst: 234 | self.burst_buffer += packet 235 | else: 236 | self.write(packet) 237 | 238 | def _receive(self, size: int) -> int: 239 | """Read and unpack the specified number of bytes from the serial port. 240 | 241 | Parameters 242 | ---------- 243 | size : int 244 | Number of bytes to read from the serial port. 245 | 246 | Returns 247 | ------- 248 | int 249 | Unpacked bytes, or -1 if too few bytes were read. 250 | """ 251 | received = self.read(size) 252 | 253 | if len(received) == size: 254 | if size in (1, 2, 4): 255 | unpacker = self._get_integer_type(size) 256 | retval = unpacker.unpack(received)[0] 257 | else: 258 | retval = int.from_bytes( 259 | bytes=received, byteorder="little", signed=False 260 | ) 261 | else: 262 | logger.error(f"Requested {size} bytes, got {len(received)}.") 263 | retval = -1 # raise an exception instead? 264 | 265 | return retval 266 | 267 | def read(self, number_of_bytes: int) -> bytes: 268 | data = self.interface.read(number_of_bytes) 269 | self._write_log(data, "RX") 270 | return data 271 | 272 | def write(self, data: bytes): 273 | self.interface.write(data) 274 | self._write_log(data, "TX") 275 | 276 | def _write_log(self, data: bytes, direction: str): 277 | if self._logging: 278 | self._log += direction.encode() + data + "STOP".encode() 279 | 280 | def wait_for_data(self, timeout: float = 0.2) -> bool: 281 | """Wait for :timeout: seconds or until there is data in the input buffer. 282 | 283 | Parameters 284 | ---------- 285 | timeout : float, optional 286 | Time in seconds to wait. The default is 0.2. 287 | 288 | Returns 289 | ------- 290 | bool 291 | True iff the input buffer is not empty. 292 | """ 293 | start_time = time.time() 294 | 295 | while time.time() - start_time < timeout: 296 | if self.interface.in_waiting: 297 | return True 298 | time.sleep(0.02) 299 | 300 | return False 301 | 302 | def send_burst(self) -> List[int]: 303 | """Transmit the commands stored in the burst_buffer. 304 | 305 | The burst_buffer and input buffer are both emptied. 306 | 307 | The following example initiates the capture routine and sets OD1 HIGH 308 | immediately. It is used by the Transient response experiment where the input 309 | needs to be toggled soon after the oscilloscope has been started. 310 | 311 | Example 312 | ------- 313 | >>> I.load_burst = True 314 | >>> I.capture_traces(4, 800, 2) 315 | >>> I.set_state(I.OD1, I.HIGH) 316 | >>> I.send_burst() 317 | 318 | Returns 319 | ------- 320 | list 321 | List of response codes 322 | (see :meth:`get_ack `). 323 | """ 324 | self.write(self.burst_buffer) 325 | self.burst_buffer = b"" 326 | self.load_burst = False 327 | acks = self.read(self.input_queue_size) 328 | self.input_queue_size = 0 329 | 330 | return list(acks) 331 | 332 | 333 | RECORDED_TRAFFIC = iter([]) 334 | """An iterator returning (request, response) pairs. 335 | 336 | The request is checked against data written to the dummy serial port, and if it matches 337 | the response can be read back. Both request and response should be bytes-like. 338 | 339 | Intended to be monkey-patched by the calling test module. 340 | """ 341 | 342 | 343 | class MockHandler(Handler): 344 | """Mock implementation of :class:`Handler` for testing. 345 | 346 | Parameters 347 | ---------- 348 | Same as :class:`Handler`. 349 | """ 350 | 351 | VERSION = "PSLab vMOCK" 352 | 353 | def __init__( 354 | self, port: str = None, baudrate: int = 1000000, timeout: float = 1.0, 355 | ): 356 | self._in_buffer = b"" 357 | super().__init__(port, baudrate, timeout) 358 | 359 | def connect( 360 | self, port: str = None, baudrate: int = 1000000, timeout: float = 1.0, 361 | ): 362 | self.version = self.get_version() 363 | 364 | def disconnect(self): 365 | pass 366 | 367 | def reconnect( 368 | self, port: str = None, baudrate: int = None, timeout: float = None, 369 | ): 370 | pass 371 | 372 | def get_version(self, *args) -> str: 373 | return self.VERSION 374 | 375 | def read(self, number_of_bytes: int) -> bytes: 376 | """Mimic the behavior of the serial bus by returning recorded RX traffic. 377 | 378 | The returned data depends on how :meth:`write` was called prior to calling 379 | :meth:`read`. 380 | """ 381 | read_bytes = self._in_buffer[:number_of_bytes] 382 | self._in_buffer = self._in_buffer[number_of_bytes:] 383 | return read_bytes 384 | 385 | def write(self, data: bytes): 386 | """Add recorded RX data to buffer if written data equals recorded TX data. 387 | """ 388 | tx, rx = next(RECORDED_TRAFFIC) 389 | if tx == data: 390 | self._in_buffer += rx 391 | 392 | def wait_for_data(self, timeout: float = 0.2) -> bool: 393 | """Return True if there is data in buffer, or return False after timeout. 394 | """ 395 | if self._in_buffer: 396 | return True 397 | else: 398 | time.sleep(timeout) 399 | return False 400 | -------------------------------------------------------------------------------- /PSL/sensorlist.py: -------------------------------------------------------------------------------- 1 | # I2C Address list from Adafruit : https://learn.adafruit.com/i2c-addresses/the-list 2 | 3 | sensors = { 4 | 0x00: ['Could be MLX90614. Try 0x5A'], 5 | 0x10: ['VEML6075', 'VEML7700'], 6 | 0x11: ['Si4713'], 7 | 0x13: ['VCNL40x0'], 8 | 0x18: ['MCP9808', 'LIS3DH'], 9 | 0x19: ['MCP9808', 'LIS3DH', 'LSM303 accelerometer & magnetometer'], 10 | 0x1A: ['MCP9808'], 11 | 0x1B: ['MCP9808'], 12 | 0x1C: ['MCP9808', 'MMA845x', 'FXOS8700'], 13 | 0x1D: ['MCP9808', 'MMA845x', 'FXOS8700','ADXL345', 'MMA7455L', 'LSM9DSO'], 14 | 0x1E: ['MCP9808', 'FXOS8700', 'LSM303', 'LSM9DSO', 'HMC5883'], 15 | 0x1F: ['MCP9808', 'FXOS8700'], 16 | 0x20: ['MCP23008', 'MCP23017', 'FXAS21002', 'Chirp! Water Sensor'], 17 | 0x21: ['MCP23008', 'MCP23017', 'FXAS21002'], 18 | 0x22: ['MCP23008', 'MCP23017'], 19 | 0x23: ['MCP23008', 'MCP23017'], 20 | 0x24: ['MCP23008', 'MCP23017'], 21 | 0x25: ['MCP23008', 'MCP23017'], 22 | 0x26: ['MCP23008', 'MCP23017', 'MSA301'], 23 | 0x27: ['MCP23008', 'MCP23017'], 24 | 0x28: ['BNO055', 'CAP1188', 'TSL2591'], 25 | 0x29: ['TSL2561', 'BNO055', 'TCS34725', 'TSL2591', 'VL53L0x', 'VL6180X', 'CAP1188'], 26 | 0x2A: ['CAP1188'], 27 | 0x2B: ['CAP1188'], 28 | 0x2C: ['CAP1188'], 29 | 0x2D: ['CAP1188'], 30 | 0x38: ['VEML6070','FT6206 touch controller'], 31 | 0x39: ['TSL2561', 'VEML6070', 'APDS-9960'], 32 | 0x3c: ['SSD1306 monochrome OLED', 'SSD1305 monochrome OLED'], 33 | 0x3d: ['SSD1306 monochrome OLED', 'SSD1305 monochrome OLED'], 34 | 0x40: ['Si7021', 'HTU21D-F', 'HDC1008','TMP007', 'TMP006', 'PCA9685', 'INA219', 'INA260'], 35 | 0x41: ['HDC1008','TMP007', 'TMP006', 'INA219', 'INA260', 'STMPE610/STMPE811'], 36 | 0x42: ['HDC1008','TMP007', 'TMP006', 'INA219'], 37 | 0x43: ['HDC1008','TMP007', 'TMP006', 'INA219', 'INA260'], 38 | 0x44: ['SHT31', 'TMP007', 'TMP006', 'ISL29125', 'INA219', 'INA260', 'STMPE610/STMPE811'], 39 | 0x45: ['SHT31', 'TMP007', 'TMP006', 'INA219', 'INA260'], 40 | 0x46: ['TMP007', 'TMP006', 'INA219', 'INA260'], 41 | 0x47: ['TMP007', 'TMP006', 'INA219', 'INA260'], 42 | 0x48: ['TMP102', 'PN532 NFS/RFID', 'ADS1115', 'INA219', 'INA260'], 43 | 0x49: ['TSL2561', 'TMP102', 'ADS1115', 'INA219', 'INA260'], 44 | 0x4A: ['TMP102', 'ADS1115', 'INA219', 'INA260'], 45 | 0x4B: ['TMP102', 'ADS1115', 'INA219', 'INA260'], 46 | 0x4C: ['INA219', 'INA260'], 47 | 0x4D: ['INA219', 'INA260'], 48 | 0x4E: ['INA219', 'INA260'], 49 | 0x4F: ['INA219', 'INA260'], 50 | 0x50: ['MB85RC'], 51 | 0x51: ['MB85RC'], 52 | 0x52: ['MB85RC', 'Nintendo Nunchuck controller'], 53 | 0x53: ['MB85RC', 'ADXL345'], 54 | 0x54: ['MB85RC'], 55 | 0x55: ['MB85RC'], 56 | 0x56: ['MB85RC'], 57 | 0x57: ['MB85RC', 'MAX3010x'], 58 | 0x58: ['TPA2016','SGP30'], 59 | 0x5A: ['MPR121', 'CCS811', 'MLX9061x', 'DRV2605'], 60 | 0x5B: ['MPR121', 'CCS811'], 61 | 0x5C: ['AM2315', 'MPR121'], 62 | 0x5D: ['MPR121'], 63 | 0x60: ['MPL115A2','MPL3115A2', 'Si5351A', 'Si1145', 'MCP4725A0', 'TEA5767', 'VCNL4040'], 64 | 0x61: ['Si5351A', 'MCP4725A0'], 65 | 0x62: ['MCP4725A0'], 66 | 0x63: ['Si4713', 'MCP4725A1'], 67 | 0x64: ['MCP4725A2'], 68 | 0x65: ['MCP4725A2'], 69 | 0x66: ['MCP4725A3'], 70 | 0x67: ['MCP4725A3'], 71 | 0x68: ['AMG8833', 'DS1307', 'PCF8523', 'DS3231', 'MPU-9250', 'MPU-60X0', 'ITG3200'], 72 | 0x69: ['AMG8833', 'MPU-9250', 'MPU-60X0', 'ITG3200'], 73 | 0x6A: ['L3GD20H', 'LSM9DS0'], 74 | 0x6B: ['L3GD20H', 'LSM9DS0'], 75 | 0x70: ['TCA9548', 'HT16K33'], 76 | 0x71: ['TCA9548', 'HT16K33'], 77 | 0x72: ['TCA9548', 'HT16K33'], 78 | 0x73: ['TCA9548', 'HT16K33'], 79 | 0x74: ['IS31FL3731', 'TCA9548', 'HT16K33'], 80 | 0x75: ['IS31FL3731', 'TCA9548', 'HT16K33'], 81 | 0x76: ['BME280 Temp/Barometric', 'IS31FL3731', 'TCA9548', 'HT16K33', 'MS5607/MS5611'], 82 | 0x77: ['BME280 Temp/Barometric/Humidity', 'BMP180 Temp/Barometric', 'BMP085 Temp/Barometric', 'BMA180 Accelerometer', 'IS31FL3731', 'TCA9548', 'HT16K33', 'MS5607/MS5611'], 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSLab Python Library 2 | 3 | The Python library for the [Pocket Science Lab](https://pslab.io) from FOSSASIA. 4 | 5 | [![Build Status](https://travis-ci.org/fossasia/pslab-python.svg?branch=development)](https://travis-ci.org/fossasia/pslab-python) 6 | [![Gitter](https://badges.gitter.im/fossasia/pslab.svg)](https://gitter.im/fossasia/pslab?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 7 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/ce4af216571846308f66da4b7f26efc7)](https://www.codacy.com/app/mb/pslab-python?utm_source=github.com&utm_medium=referral&utm_content=fossasia/pslab&utm_campaign=Badge_Grade) 8 | [![Mailing List](https://img.shields.io/badge/Mailing%20List-FOSSASIA-blue.svg)](https://groups.google.com/forum/#!forum/pslab-fossasia) 9 | [![Twitter Follow](https://img.shields.io/twitter/follow/pslabio.svg?style=social&label=Follow&maxAge=2592000?style=flat-square)](https://twitter.com/pslabio) 10 | 11 | This repository hosts the python library for communicating with the Pocket Science Lab open hardware platform (PSLab). This can be installed on a Linux or Windows system. Using this library you can communicate with the PSLab using simple Python code. The Python library is also used in the PSLab desktop application as a backend component. The goal of PSLab is to create an Open Source hardware device (open on all layers) and software applications that can be used for experiments by teachers, students and scientists. Our tiny pocket lab provides an array of instruments for doing science and engineering experiments. It provides functions of numerous measurement tools including an oscilloscope, a waveform generator, a frequency counter, a programmable voltage, current source and even a component to control robots with up to four servos. The website is at: https://pslab.io 12 | 13 | ## Buy 14 | 15 | * You can get a Pocket Science Lab device from the [FOSSASIA Shop](https://fossasia.com). 16 | * More resellers are listed on the [PSLab website](https://pslab.io/shop/). 17 | 18 | ## Communication 19 | 20 | * The PSLab [chat channel is on Gitter](https://gitter.im/fossasia/pslab). 21 | * Please also join us on the [PSLab Mailing List](https://groups.google.com/forum/#!forum/pslab-fossasia). 22 | 23 | ## Installation 24 | 25 | To install PSLab on Debian based GNU/Linux system, the following dependencies must be installed. 26 | 27 | ### Dependencies 28 | 29 | * Python 3.6 or higher [Link to Official download page](https://www.python.org/downloads/windows/) 30 | * Pip   **Support package installer** 31 | * NumPy   **For numerical calculations** 32 | * PySerial   **For device connection** 33 | * iPython-qtconsole   **_Optional_** 34 | 35 | **Note**: If you are only interested in using PSLab as an acquisition device without a display/GUI, only one repository [pslab-python](https://github.com/fossasia/pslab-python) needs to be installed. If you like a GUI, install the [pslab-desktop app](https://github.com/fossasia/pslab-desktop) and follow the instructions of the Readme in that repo. 36 | 37 | ### How To Install on Linux 38 | 39 | As root (or with sudo): 40 | 41 | # pip install git+https://github.com/fossasia/pslab-python@master 42 | 43 | Done! 44 | 45 | ### How to Install on Windows 46 | 47 | **Step 1**: Install the latest Python version on your computer and configure `PATH` variable to have both Python installation directory and the Scripts directory to access `pip` tools. In Windows, Python is installed in `C:` drive by default. We can set `$PATH` by opening the **Environment variables** dialog box by following the steps below: 48 | 49 | 1. [Right click on My Computer] 50 | 2. Select "Properties" 51 | 3. Open "System Properties" 52 | 4. Click "Advanced" tab 53 | 5. Click "Environment Variables" button 54 | 6. Look for "**_PATH_**" in "System Variables" section and click on it and press "Edit" button 55 | 7. To the end of "Variable value" text box, append "`C:\Python34\;C:\Python34\Scripts\;`" (without quotes and `34` may differ depending on the python version installed. It could be 35, 37 ...) 56 | 8. Click "OK" twice to save and move out from path windows 57 | 58 | **Step 2**: Open up command prompt and execute the following commands to install the required dependencies. 59 | 60 | $ pip install pyserial 61 | $ pip install numpy 62 | 63 | #### Validate 64 | 65 | 1. Download the PSLab-Python library from this repository and extract it to a directory. 66 | 2. Browse in to that directory and create a new file named `test-pslab-libs.py` 67 | 3. Paste the following code into that file and save it. 68 | ``` 69 | from PSL import sciencelab 70 | I = sciencelab.connect() 71 | capacitance = I.get_capacitance() 72 | print(capacitance) 73 | ``` 74 | 4. Plug in the PSLab device and check if both the LEDs are turned on. 75 | 5. Now run this file by typing `python test-pslab-libs.py` on a command prompt and observe a numerical value printed on the screen along with PSLab device version and the port it is connected to. 76 | 77 | ### How to Setup the Development Environment 78 | 79 | To set up the development environment, install the packages mentioned in dependencies. For building GUI's you can use Qt Designer or create a frontend using any other compatible technology. The PSLab desktop app is using Electron for example. 80 | 81 | ## How to Build the Documentation Website 82 | 83 | First install sphinx by running following command 84 | 85 | pip install -U Sphinx 86 | 87 | Then go to pslab/docs and run the following command 88 | 89 | $ make html 90 | 91 | ## License 92 | 93 | The library is free and open source software licensed under the [GPL v3](LICENSE). The copyright is owned by FOSSASIA. 94 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SEEL.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SEEL.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/SEEL" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SEEL" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/PSL.SENSORS.rst: -------------------------------------------------------------------------------- 1 | PSL.SENSORS package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | PSL.SENSORS.AD7718_class module 8 | ------------------------------- 9 | 10 | .. automodule:: PSL.SENSORS.AD7718_class 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | PSL.SENSORS.AD9833 module 16 | ------------------------- 17 | 18 | .. automodule:: PSL.SENSORS.AD9833 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | PSL.SENSORS.BH1750 module 24 | ------------------------- 25 | 26 | .. automodule:: PSL.SENSORS.BH1750 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | PSL.SENSORS.BMP180 module 32 | ------------------------- 33 | 34 | .. automodule:: PSL.SENSORS.BMP180 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | PSL.SENSORS.ComplementaryFilter module 40 | -------------------------------------- 41 | 42 | .. automodule:: PSL.SENSORS.ComplementaryFilter 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | PSL.SENSORS.HMC5883L module 48 | --------------------------- 49 | 50 | .. automodule:: PSL.SENSORS.HMC5883L 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | PSL.SENSORS.Kalman module 56 | ------------------------- 57 | 58 | .. automodule:: PSL.SENSORS.Kalman 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | PSL.SENSORS.MF522 module 64 | ------------------------ 65 | 66 | .. automodule:: PSL.SENSORS.MF522 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | PSL.SENSORS.MLX90614 module 72 | --------------------------- 73 | 74 | .. automodule:: PSL.SENSORS.MLX90614 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | PSL.SENSORS.MPU6050 module 80 | -------------------------- 81 | 82 | .. automodule:: PSL.SENSORS.MPU6050 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | PSL.SENSORS.SHT21 module 88 | ------------------------ 89 | 90 | .. automodule:: PSL.SENSORS.SHT21 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | PSL.SENSORS.SSD1306 module 96 | -------------------------- 97 | 98 | .. automodule:: PSL.SENSORS.SSD1306 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | PSL.SENSORS.TSL2561 module 104 | -------------------------- 105 | 106 | .. automodule:: PSL.SENSORS.TSL2561 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | PSL.SENSORS.supported module 112 | ---------------------------- 113 | 114 | .. automodule:: PSL.SENSORS.supported 115 | :members: 116 | :undoc-members: 117 | :show-inheritance: 118 | 119 | 120 | Module contents 121 | --------------- 122 | 123 | .. automodule:: PSL.SENSORS 124 | :members: 125 | :undoc-members: 126 | :show-inheritance: 127 | -------------------------------------------------------------------------------- /docs/PSL.rst: -------------------------------------------------------------------------------- 1 | PSLab@FOSSASIA package 2 | ------------ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | PSL.SENSORS 10 | 11 | Submodules 12 | ---------- 13 | 14 | PSL.Peripherals module 15 | ----------------------- 16 | 17 | .. automodule:: PSL.Peripherals 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | PSL.achan module 23 | ----------------- 24 | 25 | .. automodule:: PSL.achan 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | PSL.analyticsClass module 31 | -------------------------- 32 | 33 | .. automodule:: PSL.analyticsClass 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | PSL.commands_proto module 39 | -------------------------- 40 | 41 | .. automodule:: PSL.commands_proto 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | PSL.digital_channel module 47 | --------------------------- 48 | 49 | .. automodule:: PSL.digital_channel 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | PSL.sciencelab module 55 | --------------------- 56 | 57 | .. automodule:: PSL.sciencelab 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | PSL.packet_handler module 63 | -------------------------- 64 | 65 | .. automodule:: PSL.packet_handler 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | PSL.sensorlist module 71 | ---------------------- 72 | 73 | .. automodule:: PSL.sensorlist 74 | :members: 75 | :undoc-members: 76 | :show-inheritance: 77 | 78 | 79 | Module contents 80 | --------------- 81 | 82 | .. automodule:: PSL 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PSLab documentation build configuration file, created by 4 | # sphinx-quickstart 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import sys 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.viewcode', 37 | 'sphinx.ext.mathjax', 38 | ] 39 | 40 | mathjax_path = 'file:///usr/share/javascript/mathjax/MathJax.js' 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | # source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = u'FOSSASIA PSLab' 58 | copyright = u'2016, Praveen Patil, Jithin BP' 59 | author = u' Praveen Patil, Jithin BP' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = u'1.0' 67 | # The full version, including alpha/beta/rc tags. 68 | release = u'1.0.5' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = 'en' 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | # today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | # today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ['_build'] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | # default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | # add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | # add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | # show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = 'sphinx' 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | # modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | # keep_warnings = False 110 | 111 | # If true, `todo` and `todoList` produce output, else they produce nothing. 112 | todo_include_todos = True 113 | 114 | # -- Options for HTML output ---------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | # html_theme = 'alabaster' 119 | # html_theme_path = ["/usr/lib/python2.7/dist-packages/"] 120 | 121 | 122 | # Theme options are theme-specific and customize the look and feel of a theme 123 | # further. For a list of options available for each theme, see the 124 | # documentation. 125 | # html_theme_options = {} 126 | 127 | # Add any paths that contain custom themes here, relative to this directory. 128 | # html_theme_path = [] 129 | 130 | # The name for this set of Sphinx documents. If None, it defaults to 131 | # " v documentation". 132 | # html_title = None 133 | 134 | # A shorter title for the navigation bar. Default is the same as html_title. 135 | # html_short_title = None 136 | 137 | # The name of an image file (relative to this directory) to place at the top 138 | # of the sidebar. 139 | # html_logo = None 140 | 141 | # The name of an image file (relative to this directory) to use as a favicon of 142 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 143 | # pixels large. 144 | # html_favicon = None 145 | 146 | # Add any paths that contain custom static files (such as style sheets) here, 147 | # relative to this directory. They are copied after the builtin static files, 148 | # so a file named "default.css" will overwrite the builtin "default.css". 149 | html_static_path = ['_static'] 150 | 151 | # Add any extra paths that contain custom files (such as robots.txt or 152 | # .htaccess) here, relative to this directory. These files are copied 153 | # directly to the root of the documentation. 154 | # html_extra_path = [] 155 | 156 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 157 | # using the given strftime format. 158 | # html_last_updated_fmt = '%b %d, %Y' 159 | 160 | # If true, SmartyPants will be used to convert quotes and dashes to 161 | # typographically correct entities. 162 | # html_use_smartypants = True 163 | 164 | # Custom sidebar templates, maps document names to template names. 165 | # html_sidebars = {} 166 | 167 | # Additional templates that should be rendered to pages, maps page names to 168 | # template names. 169 | # html_additional_pages = {} 170 | 171 | # If false, no module index is generated. 172 | # html_domain_indices = True 173 | 174 | # If false, no index is generated. 175 | # html_use_index = True 176 | 177 | # If true, the index is split into individual pages for each letter. 178 | # html_split_index = False 179 | 180 | # If true, links to the reST sources are added to the pages. 181 | # html_show_sourcelink = True 182 | 183 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 184 | # html_show_sphinx = True 185 | 186 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 187 | # html_show_copyright = True 188 | 189 | # If true, an OpenSearch description file will be output, and all pages will 190 | # contain a tag referring to it. The value of this option must be the 191 | # base URL from which the finished HTML is served. 192 | # html_use_opensearch = '' 193 | 194 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 195 | # html_file_suffix = None 196 | 197 | # Language to be used for generating the HTML full-text search index. 198 | # Sphinx supports the following languages: 199 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 200 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 201 | # html_search_language = 'en' 202 | 203 | # A dictionary with options for the search language support, empty by default. 204 | # Now only 'ja' uses this config value 205 | # html_search_options = {'type': 'default'} 206 | 207 | # The name of a javascript file (relative to the configuration directory) that 208 | # implements a search results scorer. If empty, the default will be used. 209 | # html_search_scorer = 'scorer.js' 210 | 211 | # Output file base name for HTML help builder. 212 | htmlhelp_basename = 'SEELdoc' 213 | 214 | # -- Options for LaTeX output --------------------------------------------- 215 | 216 | latex_elements = { 217 | # The paper size ('letterpaper' or 'a4paper'). 218 | # 'papersize': 'letterpaper', 219 | 220 | # The font size ('10pt', '11pt' or '12pt'). 221 | # 'pointsize': '10pt', 222 | 223 | # Additional stuff for the LaTeX preamble. 224 | # 'preamble': '', 225 | 226 | # Latex figure (float) alignment 227 | # 'figure_align': 'htbp', 228 | } 229 | 230 | # Grouping the document tree into LaTeX files. List of tuples 231 | # (source start file, target name, title, 232 | # author, documentclass [howto, manual, or own class]). 233 | latex_documents = [ 234 | (master_doc, 'PSL.tex', u'PSL Documentation', 235 | u'Praveen Patil, Jithin BP', 'manual'), 236 | ] 237 | 238 | # The name of an image file (relative to this directory) to place at the top of 239 | # the title page. 240 | # latex_logo = None 241 | 242 | # For "manual" documents, if this is true, then toplevel headings are parts, 243 | # not chapters. 244 | # latex_use_parts = False 245 | 246 | # If true, show page references after internal links. 247 | # latex_show_pagerefs = False 248 | 249 | # If true, show URL addresses after external links. 250 | # latex_show_urls = False 251 | 252 | # Documents to append as an appendix to all manuals. 253 | # latex_appendices = [] 254 | 255 | # If false, no module index is generated. 256 | # latex_domain_indices = True 257 | 258 | 259 | # -- Options for manual page output --------------------------------------- 260 | 261 | # One entry per manual page. List of tuples 262 | # (source start file, name, description, authors, manual section). 263 | man_pages = [ 264 | (master_doc, 'psl', u'PSL Documentation', 265 | [author], 1) 266 | ] 267 | 268 | # If true, show URL addresses after external links. 269 | # man_show_urls = False 270 | 271 | 272 | # -- Options for Texinfo output ------------------------------------------- 273 | 274 | # Grouping the document tree into Texinfo files. List of tuples 275 | # (source start file, target name, title, author, 276 | # dir menu entry, description, category) 277 | texinfo_documents = [ 278 | (master_doc, 'PSL', u'PSL Documentation', 279 | author, 'PSL', 'One line description of project.', 280 | 'Miscellaneous'), 281 | ] 282 | 283 | # Documents to append as an appendix to all manuals. 284 | # texinfo_appendices = [] 285 | 286 | # If false, no module index is generated. 287 | # texinfo_domain_indices = True 288 | 289 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 290 | # texinfo_show_urls = 'footnote' 291 | 292 | # If true, do not generate a @detailmenu in the "Top" node's menu. 293 | # texinfo_no_detailmenu = False 294 | 295 | 296 | # -- Options for Epub output ---------------------------------------------- 297 | 298 | # Bibliographic Dublin Core info. 299 | epub_title = project 300 | epub_author = author 301 | epub_publisher = author 302 | epub_copyright = copyright 303 | 304 | # The basename for the epub file. It defaults to the project name. 305 | # epub_basename = project 306 | 307 | # The HTML theme for the epub output. Since the default themes are not 308 | # optimized for small screen space, using the same theme for HTML and epub 309 | # output is usually not wise. This defaults to 'epub', a theme designed to save 310 | # visual space. 311 | # epub_theme = 'epub' 312 | 313 | # The language of the text. It defaults to the language option 314 | # or 'en' if the language is not set. 315 | # epub_language = '' 316 | 317 | # The scheme of the identifier. Typical schemes are ISBN or URL. 318 | # epub_scheme = '' 319 | 320 | # The unique identifier of the text. This can be a ISBN number 321 | # or the project homepage. 322 | # epub_identifier = '' 323 | 324 | # A unique identification for the text. 325 | # epub_uid = '' 326 | 327 | # A tuple containing the cover image and cover page html template filenames. 328 | # epub_cover = () 329 | 330 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 331 | # epub_guide = () 332 | 333 | # HTML files that should be inserted before the pages created by sphinx. 334 | # The format is a list of tuples containing the path and title. 335 | # epub_pre_files = [] 336 | 337 | # HTML files that should be inserted after the pages created by sphinx. 338 | # The format is a list of tuples containing the path and title. 339 | # epub_post_files = [] 340 | 341 | # A list of files that should not be packed into the epub file. 342 | epub_exclude_files = ['search.html'] 343 | 344 | # The depth of the table of contents in toc.ncx. 345 | # epub_tocdepth = 3 346 | 347 | # Allow duplicate toc entries. 348 | # epub_tocdup = True 349 | 350 | # Choose between 'default' and 'includehidden'. 351 | # epub_tocscope = 'default' 352 | 353 | # Fix unsupported image types using the Pillow. 354 | # epub_fix_images = False 355 | 356 | # Scale large images. 357 | # epub_max_image_width = 0 358 | 359 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 360 | # epub_show_urls = 'inline' 361 | 362 | # If false, no index is generated. 363 | # epub_use_index = True 364 | -------------------------------------------------------------------------------- /docs/images/IMG_20160618_011156_HDR.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/IMG_20160618_011156_HDR.jpg -------------------------------------------------------------------------------- /docs/images/SplashNotConnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/SplashNotConnected.png -------------------------------------------------------------------------------- /docs/images/advanced controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/advanced controls.png -------------------------------------------------------------------------------- /docs/images/controlPanelNotConnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/controlPanelNotConnected.png -------------------------------------------------------------------------------- /docs/images/controlpanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/controlpanel.png -------------------------------------------------------------------------------- /docs/images/datastreaming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/datastreaming.png -------------------------------------------------------------------------------- /docs/images/lissajous1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/lissajous1.png -------------------------------------------------------------------------------- /docs/images/lissajous2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/lissajous2.png -------------------------------------------------------------------------------- /docs/images/logicanalyzer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/logicanalyzer.png -------------------------------------------------------------------------------- /docs/images/psl2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/psl2.jpg -------------------------------------------------------------------------------- /docs/images/pslab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/pslab.png -------------------------------------------------------------------------------- /docs/images/pslaboscilloscope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/pslaboscilloscope.png -------------------------------------------------------------------------------- /docs/images/pslpcb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/pslpcb.jpg -------------------------------------------------------------------------------- /docs/images/sensordataloger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/sensordataloger.png -------------------------------------------------------------------------------- /docs/images/sinewaveonoscilloscope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/sinewaveonoscilloscope.png -------------------------------------------------------------------------------- /docs/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/splash.png -------------------------------------------------------------------------------- /docs/images/squarewave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/squarewave.png -------------------------------------------------------------------------------- /docs/images/wiki_images/lubuntu_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/wiki_images/lubuntu_logo.png -------------------------------------------------------------------------------- /docs/images/wiki_images/ubuntu_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/wiki_images/ubuntu_logo.png -------------------------------------------------------------------------------- /docs/images/wiki_images/window_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/wiki_images/window_logo.png -------------------------------------------------------------------------------- /docs/images/wiki_images/xubuntu_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/wiki_images/xubuntu_logo.png -------------------------------------------------------------------------------- /docs/images/wirelesssensordataloger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/wirelesssensordataloger.png -------------------------------------------------------------------------------- /docs/images/with fossasia logo sticker .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/docs/images/with fossasia logo sticker .jpg -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. PSLab documentation master file, 2 | You can adapt this file completely to your liking, but it should at least 3 | contain the root `toctree` directive. 4 | 5 | Welcome to PSLab's documentation! 6 | ================================ 7 | 8 | Contents: 9 | 10 | .. toctree:: 11 | :maxdepth: 4 12 | 13 | PSL.interface 14 | PSL.Peripherals.I2C_class 15 | PSL.Peripherals.SPI_class 16 | PSL.Peripherals.MCP4728_class 17 | PSL.Peripherals.NRF24L01_class 18 | PSL.Peripherals.NRF_NODE 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SEEL.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SEEL.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools >= 35.0.2 2 | numpy >= 1.16.3 3 | pyserial >= 3.4 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.cmd import Command 2 | from distutils.util import execute 3 | import os 4 | import platform 5 | import shutil 6 | from subprocess import call 7 | import warnings 8 | 9 | from setuptools import setup, find_packages 10 | from setuptools.command.develop import develop 11 | from setuptools.command.install import install 12 | 13 | 14 | def udev_reload_rules(): 15 | call(["udevadm", "control", "--reload-rules"]) 16 | 17 | 18 | def udev_trigger(): 19 | call( # nosec 20 | [ 21 | "udevadm", 22 | "trigger", 23 | "--subsystem-match=usb", 24 | "--attr-match=idVendor=04d8", 25 | "--action=add", 26 | ] 27 | ) 28 | 29 | 30 | def install_udev_rules(): 31 | shutil.copy("99-pslab.rules", "/lib/udev/rules.d") 32 | execute(udev_reload_rules, [], "Reloading udev rules") 33 | execute(udev_trigger, [], "Triggering udev rules") 34 | 35 | 36 | def check_root(): 37 | return os.geteuid() == 0 38 | 39 | 40 | class CustomInstall(install): 41 | def run(self): 42 | install.run(self) 43 | self.run_command("udev") 44 | 45 | 46 | class CustomDevelop(develop): 47 | def run(self): 48 | develop.run(self) 49 | try: 50 | self.run_command("udev") 51 | except OSError as e: 52 | warnings.warn(e) 53 | 54 | 55 | class InstallUdevRules(Command): 56 | description = "install udev rules (requires root privileges)." 57 | user_options = [] 58 | 59 | def initialize_options(self): 60 | pass 61 | 62 | def finalize_options(self): 63 | pass 64 | 65 | def run(self): 66 | if platform.system() == "Linux": 67 | if check_root(): 68 | install_udev_rules() 69 | else: 70 | msg = "You must have root privileges to install udev rules." 71 | raise OSError(msg) 72 | 73 | 74 | setup( 75 | name="PSL", 76 | version="1.1.0", 77 | description="Pocket Science Lab by FOSSASIA", 78 | author="FOSSASIA PSLab Developers", 79 | author_email="pslab-fossasia@googlegroups.com", 80 | url="https://pslab.io/", 81 | install_requires=["numpy>=1.16.3."], 82 | packages=find_packages(exclude=("tests",)), 83 | package_data={ 84 | "": [ 85 | "*.css", 86 | "*.png", 87 | "*.gif", 88 | "*.html", 89 | "*.css", 90 | "*.js", 91 | "*.png", 92 | "*.jpg", 93 | "*.jpeg", 94 | "*.htm", 95 | "99-pslab.rules", 96 | ] 97 | }, 98 | cmdclass={ 99 | "develop": CustomDevelop, 100 | "install": CustomInstall, 101 | "udev": InstallUdevRules, 102 | }, 103 | ) 104 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpdang/pslab-python/e1b86e0e59eaf4c4fe86dac1d764e13ec6b77744/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption("--integration", action="store_true", default=False) 6 | parser.addoption("--record", action="store_true", default=False) 7 | 8 | 9 | @pytest.fixture 10 | def integration(request): 11 | return request.config.getoption("--integration") 12 | 13 | 14 | @pytest.fixture 15 | def record(request): 16 | return request.config.getoption("--record") 17 | -------------------------------------------------------------------------------- /tests/recordings/logic_analyzer/test_capture_four_too_low_frequency.json: -------------------------------------------------------------------------------- 1 | [[[10], [17], [11], [10], [0, 0], [16, 39]], [[], [1], [], [], [], [1]]] -------------------------------------------------------------------------------- /tests/recordings/logic_analyzer/test_capture_too_many_channels.json: -------------------------------------------------------------------------------- 1 | [[], []] -------------------------------------------------------------------------------- /tests/recordings/logic_analyzer/test_capture_too_many_events.json: -------------------------------------------------------------------------------- 1 | [[], []] -------------------------------------------------------------------------------- /tests/recordings/logic_analyzer/test_count_pulses.json: -------------------------------------------------------------------------------- 1 | [[[10], [6], [0, 0], [0, 0], [0], [0], [10], [17], [11], [25], [1], [11], [26]], [[], [], [], [], [], [1], [], [1], [], [], [1], [], [102, 78, 1]]] -------------------------------------------------------------------------------- /tests/recordings/logic_analyzer/test_get_states.json: -------------------------------------------------------------------------------- 1 | [[[9], [2]], [[], [15, 1]]] -------------------------------------------------------------------------------- /tests/recordings/logic_analyzer/test_measure_frequency_firmware.json: -------------------------------------------------------------------------------- 1 | [[[11], [3], [97, 0], [1], [11], [3], [97, 0], [1]], [[], [], [], [0, 141, 231, 228, 0, 141, 231, 228, 0, 1], [], [], [], [0, 207, 38, 0, 0, 207, 78, 0, 0, 1]]] -------------------------------------------------------------------------------- /tests/recordings/logic_analyzer/test_stop.json: -------------------------------------------------------------------------------- 1 | [[[10], [17], [11], [10], [0, 0], [16, 39], [10], [15], [196, 9], [5], [0], [10], [11], [10], [17], [10], [11]], [[], [1], [], [], [], [1], [], [], [], [], [1], [], [162, 16, 224, 17, 104, 37, 178, 55, 58, 75, 0, 0, 1], [], [1], [], [162, 16, 232, 17, 112, 37, 178, 55, 58, 75, 0, 0, 1]]] -------------------------------------------------------------------------------- /tests/test_achan.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | from PSL import achan 5 | 6 | 7 | class TestAnalogInput(unittest.TestCase): 8 | def setUp(self): 9 | self.Handler_patcher = patch("PSL.achan.packet_handler.Handler") 10 | self.mock_device = self.Handler_patcher.start() 11 | 12 | def tearDown(self): 13 | self.Handler_patcher.stop() 14 | 15 | def test_gain(self): 16 | ch1 = achan.AnalogInput("CH1", self.mock_device) 17 | example_gain = 8 18 | ch1.gain = example_gain 19 | self.assertEqual(ch1.gain, example_gain) 20 | gain_idx = achan.GAIN_VALUES.index(example_gain) 21 | self.mock_device.send_byte.assert_called_with(gain_idx) 22 | 23 | def test_gain_unsupported_value(self): 24 | ch2 = achan.AnalogInput("CH2", self.mock_device) 25 | bad_gain = 3 26 | with self.assertRaises(ValueError): 27 | ch2.gain = bad_gain 28 | 29 | def test_gain_set_unsupported_channel(self): 30 | ch3 = achan.AnalogInput("CH3", self.mock_device) 31 | example_gain = 10 32 | with self.assertRaises(TypeError): 33 | ch3.gain = example_gain 34 | 35 | def test_gain_get_unsupported_channel(self): 36 | ch3 = achan.AnalogInput("CH3", self.mock_device) 37 | self.assertIsNone(ch3.gain) 38 | self.assertEqual(ch3._gain, 1) 39 | 40 | def test_resolution(self): 41 | mic = achan.AnalogInput("MIC", self.mock_device) 42 | resolution_12bit = 12 43 | mic.resolution = resolution_12bit 44 | self.assertEqual(mic.resolution, resolution_12bit) 45 | self.assertEqual(mic._resolution, 2 ** resolution_12bit - 1) 46 | 47 | def test_resolution_unsupported_value(self): 48 | cap = achan.AnalogInput("CAP", self.mock_device) 49 | bad_resolution = 8 50 | with self.assertRaises(ValueError): 51 | cap.resolution = bad_resolution 52 | 53 | def test_scale(self): 54 | ch1 = achan.AnalogInput("CH1", self.mock_device) 55 | example_gain = 16 56 | ch1.gain = example_gain 57 | resolution_10bit = 10 58 | ch1.resolution = resolution_10bit 59 | raw_value = 588 60 | corresponding_voltage = -0.154233870967742 61 | self.assertAlmostEqual(ch1.scale(raw_value), corresponding_voltage) 62 | 63 | def test_unscale(self): 64 | ch1 = achan.AnalogInput("CH1", self.mock_device) 65 | example_gain = 16 66 | ch1.gain = example_gain 67 | resolution_10bit = 10 68 | ch1.resolution = resolution_10bit 69 | raw_value = 588 70 | corresponding_voltage = -0.154233870967742 71 | self.assertEqual(ch1.unscale(corresponding_voltage), raw_value) 72 | 73 | def test_unscale_clip(self): 74 | ch1 = achan.AnalogInput("CH1", self.mock_device) 75 | example_gain = 16 76 | ch1.gain = example_gain 77 | resolution_10bit = 10 78 | ch1.resolution = resolution_10bit 79 | raw_max = 2 ** resolution_10bit - 1 # 1023 80 | voltage_outside_range = -3.001008064516129 # raw = 2000 81 | self.assertEqual(ch1.unscale(voltage_outside_range), raw_max) 82 | -------------------------------------------------------------------------------- /tests/test_analytics.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PSL import analyticsClass 4 | 5 | 6 | class TestAnalytics(unittest.TestCase): 7 | def test_frexp10(self): 8 | input_value = 43.0982 9 | expected_result = (4.30982, 1) 10 | self.assertAlmostEqual(analyticsClass.frexp10(input_value), expected_result) 11 | 12 | def test_apply_si_prefix_rounding(self): 13 | input_value = { 14 | "value": 7545.230053, 15 | "unit": "J", 16 | } 17 | expected_result = "7.55 kJ" 18 | self.assertEqual(analyticsClass.apply_si_prefix(**input_value), expected_result) 19 | 20 | def test_apply_si_prefix_high_precision(self): 21 | input_value = { 22 | "value": -0.000002000008, 23 | "unit": "A", 24 | "precision": 6, 25 | } 26 | expected_result = "-2.000008 µA" 27 | self.assertEqual(analyticsClass.apply_si_prefix(**input_value), expected_result) 28 | 29 | def test_apply_si_prefix_low_precision(self): 30 | input_value = { 31 | "value": -1, 32 | "unit": "V", 33 | "precision": 0, 34 | } 35 | expected_result = "-1 V" 36 | self.assertEqual(analyticsClass.apply_si_prefix(**input_value), expected_result) 37 | 38 | def test_apply_si_prefix_too_big(self): 39 | input_value = { 40 | "value": 1e27, 41 | "unit": "V", 42 | } 43 | self.assertRaises(ValueError, analyticsClass.apply_si_prefix, **input_value) 44 | 45 | def test_apply_si_prefix_too_small(self): 46 | input_value = { 47 | "value": 1e-25, 48 | "unit": "V", 49 | } 50 | self.assertRaises(ValueError, analyticsClass.apply_si_prefix, **input_value) 51 | -------------------------------------------------------------------------------- /tests/test_logic_analyzer.py: -------------------------------------------------------------------------------- 1 | """These tests can be run as either unit tests or integration tests. 2 | 3 | By default, they are run as unit tests. When running as unit tests, recorded serial 4 | traffic from passing integration tests are played back during the test. 5 | 6 | By calling pytest with the --integration flag, the tests will instead be run as 7 | integration tests. In this test mode, the PSLab's PWM output is used to generate a 8 | signal which is analyzed by the logic analyzer. Before running the integration tests, 9 | connect SQ1->ID1->ID2->ID3->ID4. 10 | 11 | By calling pytest with the --record flag, the serial traffic generated by the 12 | integration tests will be recorded to JSON files, which are played back during unit 13 | testing. The --record flag implies --integration. 14 | """ 15 | 16 | import json 17 | import os.path 18 | import time 19 | 20 | import numpy as np 21 | import pytest 22 | 23 | import PSL.commands_proto as CP 24 | from PSL import logic_analyzer 25 | from PSL import packet_handler 26 | from PSL import sciencelab 27 | 28 | LOGDIR = os.path.join("tests", "recordings", "logic_analyzer") 29 | 30 | EVENTS = 2495 31 | FREQUENCY = 1e5 32 | DUTY_CYCLE = 0.5 33 | LOW_FREQUENCY = 100 34 | LOWER_FREQUENCY = 10 35 | MICROSECONDS = 1e6 36 | ONE_CLOCK_CYCLE = logic_analyzer.CLOCK_RATE ** -1 * MICROSECONDS 37 | 38 | 39 | def get_frequency(test_name): 40 | """Return the PWM frequency for integration tests. 41 | """ 42 | low_frequency_tests = ( 43 | "test_capture_four_low_frequency", 44 | "test_capture_four_lower_frequency", 45 | "test_capture_four_lowest_frequency", 46 | "test_capture_timeout", 47 | "test_get_states", 48 | ) 49 | if test_name in low_frequency_tests: 50 | return LOW_FREQUENCY 51 | elif test_name == "test_capture_four_too_low_frequency": 52 | return LOWER_FREQUENCY 53 | else: 54 | return FREQUENCY 55 | 56 | 57 | @pytest.fixture 58 | def scaffold(monkeypatch, request, integration, record): 59 | """Handle setup and teardown of tests. 60 | """ 61 | if record: 62 | integration = True 63 | 64 | test_name = request.node.name 65 | handler = get_handler(monkeypatch, test_name, integration) 66 | 67 | if record: 68 | handler._logging = True 69 | 70 | yield logic_analyzer.LogicAnalyzer(handler) 71 | 72 | if record: 73 | log = handler._log.split(b"STOP")[:-1] 74 | record_traffic(test_name, log) 75 | 76 | 77 | def get_handler(monkeypatch, test_name: str, integration: bool = True): 78 | """Return a Handler instance. 79 | 80 | When running unit tests, the Handler is a MockHandler. When running integration 81 | tests, this method also sets up the PWM signals before returning the Handler. 82 | """ 83 | if integration: 84 | psl = sciencelab.connect() 85 | psl.sqrPWM( 86 | freq=get_frequency(test_name), 87 | h0=DUTY_CYCLE, 88 | p1=0, 89 | h1=DUTY_CYCLE, 90 | p2=0, 91 | h2=DUTY_CYCLE, 92 | p3=0, 93 | h3=DUTY_CYCLE, 94 | ) 95 | return psl.H 96 | else: 97 | logfile = os.path.join(LOGDIR, test_name + ".json") 98 | tx, rx = json.load(open(logfile, "r")) 99 | traffic = ((bytes(t), bytes(r)) for t, r in zip(tx, rx)) 100 | monkeypatch.setattr(packet_handler, "RECORDED_TRAFFIC", traffic) 101 | return packet_handler.MockHandler() 102 | 103 | 104 | def record_traffic(test_name: str, log: list): 105 | """Record serial traffic to a JSON file. 106 | 107 | The file name is the test name + .json. 108 | """ 109 | tx = [] 110 | rx = [] 111 | 112 | for b in log: 113 | direction = b[:2] 114 | data = b[2:] 115 | if direction == b"TX": 116 | tx.append(list(data)) 117 | rx.append([]) 118 | elif direction == b"RX": 119 | rx[-1] += list(data) 120 | else: 121 | raise ValueError("Unknown direction: {direction}") 122 | 123 | logfile = os.path.join(LOGDIR, test_name + ".json") 124 | print([tx, rx]) 125 | json.dump([tx, rx], open(logfile, "w")) 126 | 127 | 128 | def test_capture_one_channel(scaffold): 129 | t = scaffold.capture(1, EVENTS) 130 | assert len(t[0]) == EVENTS 131 | 132 | 133 | def test_capture_two_channels(scaffold): 134 | t1, t2 = scaffold.capture(2, EVENTS) 135 | assert len(t1) == len(t2) == EVENTS 136 | 137 | 138 | def test_capture_four_channels(scaffold): 139 | t1, t2, t3, t4 = scaffold.capture(4, EVENTS) 140 | assert len(t1) == len(t2) == len(t3) == len(t4) == EVENTS 141 | 142 | 143 | def test_capture_four_low_frequency(scaffold): 144 | e2e_time = (LOW_FREQUENCY ** -1) / 2 145 | t1 = scaffold.capture(4, 10, e2e_time=e2e_time)[0] 146 | # When capturing every edge, the accuracy seems to depend on 147 | # the PWM prescaler as well as the logic analyzer prescaler. 148 | pwm_abstol = ONE_CLOCK_CYCLE * logic_analyzer.PRESCALERS[2] 149 | assert np.array(9 * [e2e_time * MICROSECONDS]) == pytest.approx( 150 | np.diff(t1), abs=ONE_CLOCK_CYCLE * logic_analyzer.PRESCALERS[1] + pwm_abstol 151 | ) 152 | 153 | 154 | def test_capture_four_lower_frequency(scaffold): 155 | e2e_time = LOW_FREQUENCY ** -1 156 | t1 = scaffold.capture(4, 10, modes=4 * ["rising"], e2e_time=e2e_time)[0] 157 | assert np.array(9 * [e2e_time * MICROSECONDS]) == pytest.approx( 158 | np.diff(t1), abs=ONE_CLOCK_CYCLE * logic_analyzer.PRESCALERS[2] 159 | ) 160 | 161 | 162 | def test_capture_four_lowest_frequency(scaffold): 163 | e2e_time = (LOW_FREQUENCY ** -1) * 16 164 | t1 = scaffold.capture( 165 | 4, 10, modes=4 * ["sixteen rising"], e2e_time=e2e_time, timeout=2 166 | )[0] 167 | assert np.array(9 * [e2e_time * MICROSECONDS]) == pytest.approx( 168 | np.diff(t1), abs=ONE_CLOCK_CYCLE * logic_analyzer.PRESCALERS[3] 169 | ) 170 | 171 | 172 | def test_capture_four_too_low_frequency(scaffold): 173 | e2e_time = (LOWER_FREQUENCY ** -1) * 4 174 | with pytest.raises(ValueError): 175 | scaffold.capture(4, 10, modes=4 * ["four rising"], e2e_time=e2e_time, timeout=5) 176 | 177 | 178 | def test_capture_nonblocking(scaffold): 179 | scaffold.capture(1, EVENTS, block=False) 180 | time.sleep(EVENTS * FREQUENCY ** -1) 181 | t = scaffold.fetch_data() 182 | assert len(t[0]) >= EVENTS 183 | 184 | 185 | def test_capture_rising_edges(scaffold): 186 | events = 100 187 | t1, t2 = scaffold.capture(2, events, modes=["any", "rising"]) 188 | expected = FREQUENCY ** -1 * MICROSECONDS / 2 189 | result = t2 - t1 - (t2 - t1)[0] 190 | assert np.arange(0, expected * events, expected) == pytest.approx( 191 | result, abs=ONE_CLOCK_CYCLE 192 | ) 193 | 194 | 195 | def test_capture_four_rising_edges(scaffold): 196 | events = 100 197 | t1, t2 = scaffold.capture(2, events, modes=["rising", "four rising"]) 198 | expected = FREQUENCY ** -1 * MICROSECONDS * 3 199 | result = t2 - t1 - (t2 - t1)[0] 200 | assert np.arange(0, expected * events, expected) == pytest.approx( 201 | result, abs=ONE_CLOCK_CYCLE 202 | ) 203 | 204 | 205 | def test_capture_sixteen_rising_edges(scaffold): 206 | events = 100 207 | t1, t2 = scaffold.capture(2, events, modes=["four rising", "sixteen rising"]) 208 | expected = FREQUENCY ** -1 * MICROSECONDS * 12 209 | result = t2 - t1 - (t2 - t1)[0] 210 | assert np.arange(0, expected * events, expected) == pytest.approx( 211 | result, abs=ONE_CLOCK_CYCLE 212 | ) 213 | 214 | 215 | def test_capture_too_many_events(scaffold): 216 | with pytest.raises(ValueError): 217 | scaffold.capture(1, CP.MAX_SAMPLES // 4 + 1) 218 | 219 | 220 | def test_capture_too_many_channels(scaffold): 221 | with pytest.raises(ValueError): 222 | scaffold.capture(5) 223 | 224 | 225 | def test_measure_frequency(scaffold): 226 | frequency = scaffold.measure_frequency("ID1", timeout=0.1) 227 | assert FREQUENCY == pytest.approx(frequency) 228 | 229 | 230 | def test_measure_frequency_firmware(scaffold): 231 | frequency = scaffold.measure_frequency( 232 | "ID2", timeout=0.1, simultaneous_oscilloscope=True 233 | ) 234 | assert FREQUENCY == pytest.approx(frequency) 235 | 236 | 237 | def test_measure_interval(scaffold): 238 | scaffold.configure_trigger("ID1", "falling") 239 | interval = scaffold.measure_interval( 240 | channels=["ID1", "ID2"], modes=["rising", "falling"], timeout=0.1 241 | ) 242 | expected_interval = FREQUENCY ** -1 * MICROSECONDS * 0.5 243 | assert expected_interval == pytest.approx(interval, abs=ONE_CLOCK_CYCLE) 244 | 245 | 246 | def test_measure_interval_same_channel(scaffold): 247 | scaffold.configure_trigger("ID1", "falling") 248 | interval = scaffold.measure_interval( 249 | channels=["ID1", "ID1"], modes=["rising", "falling"], timeout=0.1 250 | ) 251 | expected_interval = FREQUENCY ** -1 * DUTY_CYCLE * MICROSECONDS 252 | assert expected_interval == pytest.approx(interval, abs=ONE_CLOCK_CYCLE) 253 | 254 | 255 | def test_measure_interval_same_channel_any(scaffold): 256 | scaffold.configure_trigger("ID1", "falling") 257 | interval = scaffold.measure_interval( 258 | channels=["ID1", "ID1"], modes=["any", "any"], timeout=0.1 259 | ) 260 | expected_interval = FREQUENCY ** -1 * DUTY_CYCLE * MICROSECONDS 261 | assert expected_interval == pytest.approx(interval, abs=ONE_CLOCK_CYCLE) 262 | 263 | 264 | def test_measure_interval_same_channel_four_rising(scaffold): 265 | scaffold.configure_trigger("ID1", "falling") 266 | interval = scaffold.measure_interval( 267 | channels=["ID1", "ID1"], modes=["rising", "four rising"], timeout=0.1 268 | ) 269 | expected_interval = FREQUENCY ** -1 * 3 * MICROSECONDS 270 | assert expected_interval == pytest.approx(interval, abs=ONE_CLOCK_CYCLE) 271 | 272 | 273 | def test_measure_interval_same_channel_sixteen_rising(scaffold): 274 | scaffold.configure_trigger("ID1", "falling") 275 | interval = scaffold.measure_interval( 276 | channels=["ID1", "ID1"], modes=["rising", "sixteen rising"], timeout=0.1 277 | ) 278 | expected_interval = FREQUENCY ** -1 * 15 * MICROSECONDS 279 | assert expected_interval == pytest.approx(interval, abs=ONE_CLOCK_CYCLE) 280 | 281 | 282 | def test_measure_interval_same_channel_same_event(scaffold): 283 | scaffold.configure_trigger("ID1", "falling") 284 | interval = scaffold.measure_interval( 285 | channels=["ID3", "ID3"], modes=["rising", "rising"], timeout=0.1 286 | ) 287 | expected_interval = FREQUENCY ** -1 * MICROSECONDS 288 | assert expected_interval == pytest.approx(interval, abs=ONE_CLOCK_CYCLE) 289 | 290 | 291 | def test_measure_duty_cycle(scaffold): 292 | period, duty_cycle = scaffold.measure_duty_cycle("ID4", timeout=0.1) 293 | expected_period = FREQUENCY ** -1 * MICROSECONDS 294 | assert (expected_period, DUTY_CYCLE) == pytest.approx( 295 | (period, duty_cycle), abs=ONE_CLOCK_CYCLE 296 | ) 297 | 298 | 299 | def test_get_xy_rising_trigger(scaffold): 300 | scaffold.configure_trigger("ID1", "rising") 301 | t = scaffold.capture(1, 100) 302 | _, y = scaffold.get_xy(t) 303 | assert y[0] 304 | 305 | 306 | def test_get_xy_falling_trigger(scaffold): 307 | scaffold.configure_trigger("ID1", "falling") 308 | t = scaffold.capture(1, 100) 309 | _, y = scaffold.get_xy(t) 310 | assert not y[0] 311 | 312 | 313 | def test_get_xy_rising_capture(scaffold): 314 | t = scaffold.capture(1, 100, modes=["rising"]) 315 | _, y = scaffold.get_xy(t) 316 | assert sum(y) == 100 317 | 318 | 319 | def test_get_xy_falling_capture(scaffold): 320 | t = scaffold.capture(1, 100, modes=["falling"]) 321 | _, y = scaffold.get_xy(t) 322 | assert sum(~y) == 100 323 | 324 | 325 | def test_stop(scaffold): 326 | scaffold.capture(1, EVENTS, modes=["sixteen rising"], block=False) 327 | time.sleep(EVENTS * FREQUENCY ** -1) 328 | progress_time = time.time() 329 | progress = scaffold.get_progress() 330 | scaffold.stop() 331 | stop_time = time.time() 332 | time.sleep(EVENTS * FREQUENCY ** -1) 333 | assert progress < CP.MAX_SAMPLES // 4 334 | abstol = FREQUENCY * (stop_time - progress_time) 335 | assert progress == pytest.approx(scaffold.get_progress(), abs=abstol) 336 | 337 | 338 | def test_get_states(scaffold): 339 | time.sleep(LOW_FREQUENCY ** -1) 340 | states = scaffold.get_states() 341 | expected_states = {"ID1": True, "ID2": True, "ID3": True, "ID4": True} 342 | assert states == expected_states 343 | 344 | 345 | def test_count_pulses(scaffold): 346 | interval = 0.2 347 | pulses = scaffold.count_pulses("ID2", interval) 348 | expected_pulses = FREQUENCY * interval 349 | assert expected_pulses == pytest.approx(pulses, rel=0.1) # Pretty bad accuracy. 350 | -------------------------------------------------------------------------------- /tests/test_oscilloscope.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | import numpy as np 5 | 6 | import PSL.commands_proto as CP 7 | from PSL import oscilloscope 8 | 9 | 10 | class TestOscilloscope(unittest.TestCase): 11 | def setUp(self): 12 | self.Handler_patcher = patch("PSL.oscilloscope.packet_handler.Handler") 13 | self.mock_device = self.Handler_patcher.start() 14 | example_signal = np.sin(np.arange(200) / 20) 15 | self.scope = oscilloscope.Oscilloscope(device=self.mock_device) 16 | unscale = self.scope._channels["CH1"].unscale 17 | example_raw_signal = [unscale(s) for s in example_signal] 18 | example_incoming_bytes = b"" 19 | for r in example_raw_signal: 20 | example_incoming_bytes += CP.ShortInt.pack(r) 21 | self.mock_device.interface.read.return_value = example_incoming_bytes 22 | self.mock_device.get_byte.return_value = 1 23 | self.mock_device.get_int.return_value = 200 24 | 25 | def tearDown(self): 26 | self.Handler_patcher.stop() 27 | 28 | def test_capture_one_12bit(self): 29 | xy = self.scope.capture(channels=1, samples=200, timegap=2) 30 | self.assertEqual(np.shape(xy), (2, 200)) 31 | self.assertEqual(self.scope._channels["CH1"].resolution, 12) 32 | 33 | def test_capture_one_high_speed(self): 34 | xy = self.scope.capture(channels=1, samples=200, timegap=0.5) 35 | self.assertEqual(np.shape(xy), (2, 200)) 36 | self.assertEqual(self.scope._channels["CH1"].resolution, 10) 37 | 38 | def test_capture_one_trigger(self): 39 | self.scope.trigger_enabled = True 40 | xy = self.scope.capture(channels=1, samples=200, timegap=1) 41 | self.assertEqual(np.shape(xy), (2, 200)) 42 | self.assertEqual(self.scope._channels["CH1"].resolution, 10) 43 | 44 | def test_capture_two(self): 45 | xy = self.scope.capture(channels=2, samples=200, timegap=2) 46 | self.assertEqual(np.shape(xy), (3, 200)) 47 | 48 | def test_capture_four(self): 49 | xy = self.scope.capture(channels=4, samples=200, timegap=2) 50 | self.assertEqual(np.shape(xy), (5, 200)) 51 | 52 | def test_capture_invalid_channel_one(self): 53 | self.scope.channel_one_map = "BAD" 54 | with self.assertRaises(ValueError): 55 | self.scope.capture(channels=1, samples=200, timegap=2) 56 | 57 | def test_capture_timegap_too_small(self): 58 | with self.assertRaises(ValueError): 59 | self.scope.capture(channels=1, samples=200, timegap=0.2) 60 | 61 | def test_capture_too_many_channels(self): 62 | with self.assertRaises(ValueError): 63 | self.scope.capture(channels=5, samples=200, timegap=2) 64 | 65 | def test_capture_too_many_samples(self): 66 | with self.assertRaises(ValueError): 67 | self.scope.capture(channels=4, samples=3000, timegap=2) 68 | 69 | def test_invalidate_buffer(self): 70 | self.scope._channels["CH1"].samples_in_buffer = 100 71 | self.scope._channels["CH1"].buffer_idx = 0 72 | self.scope.channel_one_map = "CH2" 73 | self.scope.capture(channels=1, samples=200, timegap=2) 74 | self.assertEqual(self.scope._channels["CH1"].samples_in_buffer, 0) 75 | self.assertIsNone(self.scope._channels["CH1"].buffer_idx) 76 | 77 | def test_configure_trigger(self): 78 | self.scope.configure_trigger(channel="CH3", voltage=1.5) 79 | self.assertTrue(self.scope.trigger_enabled) 80 | self.mock_device.send_byte.assert_any_call(CP.CONFIGURE_TRIGGER) 81 | 82 | def test_configure_trigger_on_unmapped(self): 83 | with self.assertRaises(TypeError): 84 | self.scope.configure_trigger(channel="AN8", voltage=1.5) 85 | 86 | def test_configure_trigger_on_remapped_ch1(self): 87 | self.scope.channel_one_map = "CAP" 88 | with self.assertRaises(TypeError): 89 | self.scope.configure_trigger(channel="CH1", voltage=1.5) 90 | 91 | def test_trigger_enabled(self): 92 | self.scope.trigger_enabled = True 93 | self.mock_device.send_byte.assert_any_call(CP.CONFIGURE_TRIGGER) 94 | 95 | def test_select_range(self): 96 | self.scope.select_range("CH1", 1.5) 97 | self.mock_device.send_byte.assert_called() 98 | self.assertEqual(self.scope._channels["CH1"].gain, 10) 99 | 100 | def test_select_range_invalid(self): 101 | with self.assertRaises(ValueError): 102 | self.scope.select_range("CH1", 15) 103 | -------------------------------------------------------------------------------- /tests/test_packet_handler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | import serial 5 | from serial.tools.list_ports_common import ListPortInfo 6 | 7 | import PSL.commands_proto as CP 8 | from PSL.packet_handler import Handler 9 | 10 | VERSION = "PSLab vMOCK\n" 11 | PORT = "mockport" 12 | 13 | 14 | class TestHandler(unittest.TestCase): 15 | @staticmethod 16 | def mock_ListPortInfo(found=True): 17 | if found: 18 | yield ListPortInfo(device=PORT) 19 | else: 20 | return 21 | 22 | def setUp(self): 23 | self.Serial_patcher = patch("PSL.packet_handler.serial.Serial") 24 | self.list_ports_patcher = patch("PSL.packet_handler.list_ports") 25 | self.mock_Serial = self.Serial_patcher.start() 26 | self.mock_list_ports = self.list_ports_patcher.start() 27 | self.mock_Serial().readline.return_value = VERSION.encode("utf-8") 28 | 29 | def tearDown(self): 30 | self.Serial_patcher.stop() 31 | self.list_ports_patcher.stop() 32 | 33 | def test_connect_port_provided(self): 34 | Handler(port=PORT) 35 | self.assertTrue(self.mock_Serial().is_open) 36 | 37 | def test_connect_scan_port(self): 38 | self.mock_Serial().is_open = False 39 | self.mock_list_ports.grep.return_value = self.mock_ListPortInfo() 40 | Handler() 41 | self.mock_Serial().open.assert_called() 42 | 43 | def test_connect_scan_failure(self): 44 | self.mock_Serial().is_open = False 45 | self.mock_list_ports.grep.return_value = self.mock_ListPortInfo(found=False) 46 | self.assertRaises(serial.SerialException, Handler) 47 | 48 | def test_disconnect(self): 49 | H = Handler() 50 | H.disconnect() 51 | self.mock_Serial().close.assert_called() 52 | 53 | def test_reconnect(self): 54 | H = Handler(port=PORT) 55 | H.reconnect(port=PORT[::-1]) 56 | self.assertIn({"port": PORT}, self.mock_Serial.call_args_list) 57 | self.assertIn({"port": PORT[::-1]}, self.mock_Serial.call_args_list) 58 | self.mock_Serial().close.assert_called() 59 | 60 | def test_del(self): 61 | H = Handler() 62 | H.__del__() 63 | self.mock_Serial().close.assert_called() 64 | 65 | def test_get_version(self): 66 | H = Handler() 67 | 68 | self.mock_Serial().write.assert_called_with(CP.GET_VERSION) 69 | self.assertEqual(H.version, VERSION) 70 | 71 | def test_get_ack_success(self): 72 | H = Handler() 73 | success = 1 74 | self.mock_Serial().read.return_value = CP.Byte.pack(success) 75 | self.assertEqual(H.get_ack(), success) 76 | 77 | def test_get_ack_burst_mode(self): 78 | H = Handler() 79 | success = 1 80 | H.load_burst = True 81 | queue_size = H.input_queue_size 82 | self.mock_Serial().read.return_value = b"" 83 | 84 | self.assertEqual(H.get_ack(), success) 85 | self.assertEqual(H.input_queue_size, queue_size + 1) 86 | 87 | def test_get_ack_failure(self): 88 | H = Handler() 89 | error = 3 90 | self.mock_Serial().read.return_value = b"" 91 | self.assertEqual(H.get_ack(), error) 92 | 93 | def test_send_bytes(self): 94 | H = Handler() 95 | H._send(CP.Byte.pack(0xFF)) 96 | self.mock_Serial().write.assert_called_with(CP.Byte.pack(0xFF)) 97 | 98 | def test_send_byte(self): 99 | H = Handler() 100 | H._send(0xFF) 101 | self.mock_Serial().write.assert_called_with(CP.Byte.pack(0xFF)) 102 | 103 | def test_send_byte_burst_mode(self): 104 | H = Handler() 105 | H.load_burst = True 106 | H._send(0xFF) 107 | self.assertEqual(H.burst_buffer, CP.Byte.pack(0xFF)) 108 | 109 | def test_send_int(self): 110 | H = Handler() 111 | H._send(0xFFFF) 112 | self.mock_Serial().write.assert_called_with(CP.ShortInt.pack(0xFFFF)) 113 | 114 | def test_send_int_burst_mode(self): 115 | H = Handler() 116 | H.load_burst = True 117 | H._send(0xFFFF) 118 | self.assertEqual(H.burst_buffer, CP.ShortInt.pack(0xFFFF)) 119 | 120 | def test_send_long(self): 121 | H = Handler() 122 | H._send(0xFFFFFFFF) 123 | self.mock_Serial().write.assert_called_with(CP.Integer.pack(0xFFFFFFFF)) 124 | 125 | def test_send_long_burst_mode(self): 126 | H = Handler() 127 | H.load_burst = True 128 | H._send(0xFFFFFFFF) 129 | self.assertEqual(H.burst_buffer, CP.Integer.pack(0xFFFFFFFF)) 130 | 131 | def test_receive(self): 132 | H = Handler() 133 | self.mock_Serial().read.return_value = CP.Byte.pack(0xFF) 134 | r = H._receive(1) 135 | self.mock_Serial().read.assert_called_with(1) 136 | self.assertEqual(r, 0xFF) 137 | 138 | def test_receive_uneven_bytes(self): 139 | H = Handler() 140 | self.mock_Serial().read.return_value = int.to_bytes( 141 | 0xFFFFFF, length=3, byteorder="little", signed=False 142 | ) 143 | r = H._receive(3) 144 | self.mock_Serial().read.assert_called_with(3) 145 | self.assertEqual(r, 0xFFFFFF) 146 | 147 | def test_receive_failure(self): 148 | H = Handler() 149 | self.mock_Serial().read.return_value = b"" 150 | r = H._receive(1) 151 | self.mock_Serial().read.assert_called_with(1) 152 | self.assertEqual(r, -1) 153 | 154 | def test_wait_for_data(self): 155 | H = Handler() 156 | self.assertTrue(H.wait_for_data()) 157 | 158 | def test_wait_for_data_timeout(self): 159 | H = Handler() 160 | self.mock_Serial().in_waiting = False 161 | self.assertFalse(H.wait_for_data()) 162 | 163 | def test_send_burst(self): 164 | H = Handler() 165 | H.load_burst = True 166 | 167 | for b in b"abc": 168 | H._send(b) 169 | H.get_ack() 170 | 171 | self.mock_Serial().read.return_value = b"\x01\x01\x01" 172 | acks = H.send_burst() 173 | 174 | self.mock_Serial().write.assert_called_with(b"abc") 175 | self.mock_Serial().read.assert_called_with(3) 176 | self.assertFalse(H.load_burst) 177 | self.assertEqual(H.burst_buffer, b"") 178 | self.assertEqual(H.input_queue_size, 0) 179 | self.assertEqual(acks, [1, 1, 1]) 180 | 181 | def test_get_integer_unsupported_size(self): 182 | H = Handler() 183 | self.assertRaises(ValueError, H._get_integer_type, size=3) 184 | 185 | def test_list_ports(self): 186 | self.list_ports_patcher.stop() 187 | H = Handler() 188 | self.assertTrue(isinstance(H._list_ports(), list)) 189 | self.list_ports_patcher.start() 190 | 191 | 192 | if __name__ == "__main__": 193 | unittest.main() 194 | --------------------------------------------------------------------------------