"""Convenience module that creates instances of every instrument for you.
Every PSLab instrument can be imported and instantiated individually. However,
if you need to use several at once the ScienceLab class provides a convenient
collection.
"""
from typing import Iterable, List
import pslab.protocol as CP
from pslab.bus.i2c import I2CMaster
from pslab.instrument.logic_analyzer import LogicAnalyzer
from pslab.instrument.multimeter import Multimeter
from pslab.instrument.oscilloscope import Oscilloscope
from pslab.instrument.power_supply import PowerSupply
from pslab.instrument.waveform_generator import PWMGenerator, WaveformGenerator
from pslab.peripherals import NRF24L01
from pslab.serial_handler import SerialHandler
[docs]class ScienceLab(SerialHandler):
"""Aggregate interface for the PSLab's instruments.
Attributes
----------
logic_analyzer : pslab.LogicAnalyzer
oscilloscope : pslab.Oscilloscope
waveform_generator : pslab.WaveformGenerator
pwm_generator : pslab.PWMGenerator
multimeter : pslab.Multimeter
power_supply : pslab.PowerSupply
i2c : pslab.I2CMaster
nrf : pslab.peripherals.NRF24L01
"""
def __init__(
self,
port: str = None,
baudrate: int = 1000000,
timeout: float = 1.0,
):
super().__init__(port, baudrate, timeout)
self.logic_analyzer = LogicAnalyzer(device=self)
self.oscilloscope = Oscilloscope(device=self)
self.waveform_generator = WaveformGenerator(device=self)
self.pwm_generator = PWMGenerator(device=self)
self.multimeter = Multimeter(device=self)
self.power_supply = PowerSupply(device=self)
self.i2c = I2CMaster(device=self)
self.nrf = NRF24L01(device=self)
if "V6" in self.version: # Set the built-in WS2812B to green :)
self.rgb_led([0, 20, 0])
@property
def temperature(self):
"""float: Temperature of the MCU in degrees Celsius."""
# TODO: Get rid of magic numbers.
cs = 3
V = self._get_ctmu_voltage(0b11110, cs, 0)
if cs == 1:
return (646 - V * 1000) / 1.92 # current source = 1
elif cs == 2:
return (701.5 - V * 1000) / 1.74 # current source = 2
elif cs == 3:
return (760 - V * 1000) / 1.56 # current source = 3
def _get_ctmu_voltage(self, channel: int, current_range: int, tgen: bool = True):
"""Control the Charge Time Measurement Unit (CTMU).
ctmu_voltage(5, 2) will activate a constant current source of 5.5 µA on
CAP and then measure the voltage at the output.
If a diode is used to connect CAP to ground, the forward voltage drop
of the diode will be returned, e.g. 0.6 V for a 4148 diode.
If a resistor is connected, Ohm's law will be followed within
reasonable limits.
Parameters
----------
channel : int
Pin number on which to generate a current and measure output
voltage. Refer to the PIC24EP64GP204 datasheet for channel
numbering.
current_range : {0, 1, 2, 3}
0 -> 550 µA
1 -> 550 nA
2 -> 5.5 µA
3 -> 55 µA
tgen : bool, optional
Use Time Delay mode instead of Measurement mode. The default value
is True.
Returns
-------
voltage : float
"""
self.send_byte(CP.COMMON)
self.send_byte(CP.GET_CTMU_VOLTAGE)
self.send_byte((channel) | (current_range << 5) | (tgen << 7))
raw_voltage = self.get_int() / 16 # 16*voltage across the current source
self.get_ack()
vmax = 3.3
resolution = 12
voltage = vmax * raw_voltage / (2 ** resolution - 1)
return voltage
def _start_ctmu(self, current_range: int, trim: int, tgen: int = 1):
self.send_byte(CP.COMMON)
self.send_byte(CP.START_CTMU)
self.send_byte((current_range) | (tgen << 7))
self.send_byte(trim)
self.get_ack()
def _stop_ctmu(self):
self.send_byte(CP.COMMON)
self.send_byte(CP.STOP_CTMU)
self.get_ack()
[docs] def reset(self):
"""Reset the device.
Standalone mode will be enabled if an OLED is connected to the I2C
port.
"""
self.send_byte(CP.COMMON)
self.send_byte(CP.RESTORE_STANDALONE)
[docs] def rgb_led(self, colors: List, output: str = "RGB", order: str = "GRB"):
"""Set shade of a WS2812B RGB LED.
Parameters
----------
colors : list
List of three values between 0-255, where each value is the
intensity of red, green, and blue, respectively. When daisy
chaining several LEDs, colors should be a list of three-value
lists.
output : {"RGB", "PGC", "SQ1"}, optional
Pin on which to output the pulse train setting the LED color. The
default value, "RGB", sets the color of the built-in WS2812B
(PSLab v6 only).
order : str, optional
Color order of the connected LED as a three-letter string. The
built-in LED has order "GRB", which is the default.
Examples
--------
Set the built-in WS2812B to yellow.
>>> import pslab
>>> psl = pslab.ScienceLab()
>>> psl.rgb_led([10, 10, 0])
Set a chain of three RGB WS2812B connected to SQ1 to red, cyan, and
magenta.
>>> psl.rgb_led([[10,0,0],[0,10,10],[10,0,10]], output="SQ1", order="RGB")
"""
if output == "RGB":
pin = CP.SET_RGB1
elif output == "PGC":
pin = CP.SET_RGB2
elif output == "SQ1":
pin = CP.SET_RGB3
else:
raise ValueError(
f"Invalid output: {output}. output must be 'RGB', 'PCG', or 'SQ1'."
)
if not isinstance(colors[0], Iterable):
colors = [colors]
if not all([len(color) == 3 for color in colors]):
raise ValueError("Invalid color; each color list must have three values.")
order = order.upper()
if not sorted(order) == ["B", "G", "R"]:
raise ValueError(
f"Invalid order: {order}. order must contain 'R', 'G', and 'B'."
)
self.send_byte(CP.COMMON)
self.send_byte(pin)
self.send_byte(len(colors) * 3)
for color in colors:
self.send_byte(color[order.index("R")])
self.send_byte(color[order.index("G")])
self.send_byte(color[order.index("B")])
self.get_ack()
def _read_program_address(self, address: int):
"""Return the value stored at the specified address in program memory.
Parameters
----------
address : int
Address to read from. Refer to PIC24EP64GP204 programming manual.
Returns
-------
data : int
16-bit wide value read from program memory.
"""
self.send_byte(CP.COMMON)
self.send_byte(CP.READ_PROGRAM_ADDRESS)
self.send_int(address & 0xFFFF)
self.send_int((address >> 16) & 0xFFFF)
data = self.get_int()
self.get_ack()
return data
def _device_id(self):
a = self._read_program_address(0x800FF8)
b = self._read_program_address(0x800FFA)
c = self._read_program_address(0x800FFC)
d = self._read_program_address(0x800FFE)
val = d | (c << 16) | (b << 32) | (a << 48)
return val
def _read_data_address(self, address: int):
"""Return the value stored at the specified address in RAM.
Parameters
----------
address : int
Address to read from. Refer to PIC24EP64GP204 programming manual.
Returns
-------
data : int
16-bit wide value read from RAM.
"""
self.send_byte(CP.COMMON)
self.send_byte(CP.READ_DATA_ADDRESS)
self.send_int(address & 0xFFFF)
data = self.get_int()
self.get_ack()
return data
def _write_data_address(self, address: int, value: int):
"""Write a value to the specified address in RAM.
Parameters
----------
address : int
Address to write to. Refer to PIC24EP64GP204 programming manual.
value : int
Value to write to RAM.
"""
self.send_byte(CP.COMMON)
self.send_byte(CP.WRITE_DATA_ADDRESS)
self.send_int(address & 0xFFFF)
self.send_int(value)
self.get_ack()
[docs] def enable_uart_passthrough(self, baudrate: int, persist=False):
"""Relay all data received by the device to TXD/RXD.
If a period > 0.5 seconds elapses between two transmit/receive events,
the device resets and resumes normal mode. This timeout feature has
been implemented in lieu of a hard reset option.
Can be used to load programs into secondary microcontrollers with
bootloaders such ATMEGA or ESP8266
Parameters
----------
baudrate : int
Baudrate of the UART bus.
persist : bool, optional
If set to True, the device will stay in passthrough mode until the
next power cycle. Otherwise(default scenario), the device will
return to normal operation if no data is sent/received for a period
greater than one second at a time.
"""
self.send_byte(CP.PASSTHROUGHS)
self.send_byte(CP.PASS_UART)
self.send_byte(1 if persist else 0)
self.send_int(int(round(((64e6 / baudrate) / 4) - 1)))
[docs] def set_uart_baud(self, baudrate: int):
"""Set the baudrate of the UART bus.
Parameters
----------
baudrate : int
Baudrate to set on the UART bus.
"""
self.send_byte(CP.UART_2)
self.send_byte(CP.SET_BAUD)
self.send_int(int(round(((64e6 / baudrate) / 4) - 1)))
self.get_ack()
[docs] def write_uart(self, byte: int):
"""Write a single byte to the UART bus.
Parameters
----------
byte : int
Byte value to write to the UART bus.
"""
self.send_byte(CP.UART_2)
self.send_byte(CP.SEND_BYTE)
self.send_byte(byte)
self.get_ack()
[docs] def read_uart(self):
"""Read a single byte from the UART bus.
Returns
-------
byte : int
Byte value read from the UART bus."""
self.send_byte(CP.UART_2)
self.send_byte(CP.READ_BYTE)
return self.get_byte()
[docs] def read_uart_status(self):
"""Return available bytes in UART buffer.
Returns
-------
status : int
"""
self.send_byte(CP.UART_2)
self.send_byte(CP.READ_UART2_STATUS)
return self.get_byte()
[docs] def read_log(self):
"""Read hardware debug log.
Returns
-------
log : bytes
Bytes read from the hardware debug log.
"""
self.send_byte(CP.COMMON)
self.send_byte(CP.READ_LOG)
log = self.interface.readline().strip()
self.get_ack()
return log