Raspberry Pi Hardware Mocking

October 14, 2017
23 min. read

This post is part of the Burn-In Cart series.

Before I get to deeply into specific functionality and operation of our charging carts, I thought it would be good to take a detour into drivers for custom hardware. In addition I’ll cover mocking hardware devices to allow testing of your drivers and operating your system in a simulation with no hardware present. This allows you to flush out your hardware design with software before fabricating PCBs. (A PCB takes a long time to “compile”.)

When the system is dependent on external devices, such as the 144 Android tablets that will be connected to it, a little Python code is much easier to write and refactor to simulate tablets than an separate Android app. With the Requests package, I can easily act like a tablet calling a web service. I can also prove my system before I interface with our programmers to know the problem shouldn’t be on my side. We will talk more about this in later articles.

I cover hardware at first, because I want you to understand a little about it before I get to software, but this will be mostly software related article.

Hardware Interfaces

All Raspberry Pi logic is at the 3.3V level, so care must be taken to use 3.3V logic chips or use level converters. Those used to 5V capable chips, such as the Arduino, need to be careful. In the case of driving higher voltages, MOSFETs are common and must be specified with Vgs appropriate for logic levels. These are unsurprisingly called “Logic Level” MOSFETs. I use these to drive 12V logic from the 3.3V of the RPi.

There are a few hardware interfaces available on the Raspberry Pi, the most commonly used being GPIO and I2C bus.

SMBus

On the RPi, the I2C bus is called SMbus. I2C and SMBus are essentially compatible at 100 KHz. I2C allows 400 KHz and 3.4 MHz interfacing that SMBus does not support. I named my packages to follow the smbus Python package. This type of bus has a clock line and a data line. Both are pulled up to 3.3V via a resistor and devices using the bus must pull them down to ground. It is a master/slave setup with multiple devices allowed on a given bus. All slaves have a 7-bit address number and must be unique on the bus. The Raspberry Pi is the master, which always starts communication.

The master will send an 8-bit value, with the first 7-bit the address and the last bit controlling if this is a read or write. The slave will respond that it received the info and the master will continue with more commands until the exchange is complete.

I’m using the smbus interface for getting a silicon serial number, reading voltage and current across a shunt to determine power being used, and reading temperature of the board. These are three devices on the same bus, all with different addresses.

These chips can get fairly complex in functionality, but all use the same interfaces. So mocking the hardware will get harder with complexity. However, there may only get a few of the available smbus commands that you use and therefore must implement in your fake devices.

GPIO

The GPIO interface is for General Purpose Input Output. These are single pins that you can use to send data to devices in an output mode or receive information from devices with an input mode. This is what I use to drive a chain of shift registers. Almost all pins are available for GPIO, unless you use them for other buses. GPIO 2 and 3 are used for SMBus. I could not use SMBus if these were allocated to GPIO. The same is true with reserved pins for SPI and UART buses. And important part of designing a hardware project is allocation of IO.

For a shift register, we need 4 outputs: clock, data, enable, and strobe. We clock data into the buffer and then strobe it from buffer directly to output. This keeps the bits from shifting down the actual outputs and only toggles them when we have shifted everything through.

This is a little different to mock in software, as you need to look at pin transitions. For example, the data line value is used when the clock line has a rising transition (goes from low to high). So my mock devices will need a hook to these transitions that are important and emulate functionality. Although writing tests for the objects helped me find a subtle issue with these callbacks that became important for simulation, as you will see below.

Software Interfaces

When RPi.GPIO is imported, you get a global object that you can only setup once. If you try to start a new one before using .cleanup() on the existing one, you will get a warning. And if you try to share pins with multiple instances of GPIO, you are in uncharted (read: dangerous) territory. As I was mocking up the GPIO interface, I started trying to emulate this error condition. However, a properly structured Python project with only over one reference to GPIO, so this was overkill.

All of my RPi hardware drivers have an interface reference as the first argument, either for GPIO or SMBus. This allows me to easily pass in a reference to the single object that holds that interface. There is no worry about accidentally creating a second one. This also allows me to pass in a fake version of GPIO or SMBus for testing and simulation.

My imports for these interfaces looks like this:

try:
    import RPi.GPIO as GPIO
    import smbus
    SIMULATION = False
except ImportError:
    from rpi_hardware.mocked import GPIO
    from rpi_hardware.mocked import smbus
    SIMULATION = True

With the boolean SIMULATION variable, I can control if my system is running normally or in simulation. This is usually the difference between my running on the configured RPi or my local laptop. With PyCharm I can just change the interpreter reference and debug exactly the same either locally or over SSH on the RPi. That is worth the price of admission to the Professional version of PyCharm.

FakeGPIO

In order to only allow a single GPIO, I used a singleton pattern by overriding __new__. This stores the instance in a class level attribute of __self__. The constructor needs to be defined as def _init() instead of def __init__(), as we are not creating new object each time. For break down between tests or to reset the object, the destroy() method is provided. This is what I call when I’m emulating the GPIO.cleanup().

class Singleton(object):
    def __new__(cls, *args, **kwds):
        self = "__self__"
        if not hasattr(cls, self):
            instance = object.__new__(cls)
            instance._init(*args, **kwds)
            setattr(cls, self, instance)
        return getattr(cls, self)

    def _init(self, *args, **kwargs):
        raise NotImplementedError('must implement init method')

    @classmethod
    def destroy(cls):
        self = '__self__'
        if hasattr(cls, self):
            instance = getattr(cls, self)
            del instance
            delattr(cls, self)

The full FakeGPIO source can be viewed here, as it is too large to include inline. There is quite a bit of bookkeeping with constants and handling both BCM and BOARD based pin numbering schemes. All of the normal function are replacements for what your hardware driver would normally call. You can look through the object and see how things are working. I’m liberal with ValueError to help shake out issues with the bad hardware driver calls. It is easier to find these problems in software than when we are trying to interface with actual hardware.

The two major data structures in FakeGPIO are _pins and _edge_callback.

The _pins attribute of the FakeGPIO stores all GPIO pins direction and value as a 2 item list. The key is the BCM value for the pin. All internal values are BCM numbering and BOARD pin numbers are translated to BCM for use. For direction, we are using the defined constants of GPIO.IN and GPIO.OUT. All constants have been defined using the underlying C code values, in case a user is bad and calls with a number rather than referencing the constant.

The _edge_callback attribute is a defaultdict(list) that will store callback methods for each pin (again BCM naming). The value is a tuple with (edge_type, callback). This could possibly be done more efficiently with a tuple as key, such as (pin, edge_type). Then I could use the dictionary hash to find exact callbacks I need, instead of looking if I have the proper transition for a given pin. Although if you are pin heavy and callback light, the overhead of tuple building for key search might actually be slower than the loop when you have a 50-50 chance of a callback.

There are a few helper under functions, but the two that are important in use of FakeGPIO are _simulate_set_pin and _simulate_read_out_pin. When we have an input, we are expecting hardware to provide that value. The _simulate_set_pin method is how your virtual hardware provides an input. When we have an output, we expect hardware to read it and do something with it. The _simulate_read_out_pin method is where your virtual hardware can read those values. In the implementation of my fake HCF4904 shift register hardware, I’m using a callback function to tell me that the device has been clocked. But I use this _simulate_read_out_pin to get the value of the data pin at that point.

HCF4094 Driver

This driver is for a shift register. This is a device that allows you to use a clock and data to serially shift out bits to be available parallel as 8-bits for each chip.

The logic on this is confusing sometimes, because making the RPi pin high (3.3V) turns on a MOSFET that pulls the high voltage output low. When the RPi pin is low, the MOSFET turns off, allowing the line to pull high (12V). This also makes mocking the hardware backwards.

To make things a little less confusing, I’ve defined _OUTPUT_HIGH and _OUTPUT_LOW that are already flipped.

__init__ is fairly simple, just storing the 4 pins used by the device and setting up the 4 pins as GPIO.OUTPUT.

class HCF4094(object):
    """
    Drives HCF4094 shift register with external pull up resistors
    to high voltage and N-MOSFET pulling down to ground.

    HCF4094 is capable of higher voltages than RPi interface (I'm using 12V.)
    This allows a more robust signal line, but requires isolation with RPi.

    I'm driving base of DMN3404 N-MOSFET through 100R resistor.
    Base is pulled to Ground via 10K resistor.
    Drain is connected directly to output for each of the 4 pins and via 4.7K resistor to 12V.
    Source is connected to ground.

    I am chaining 6 boards with each having 6 HCF4094 devices.  This allows 288 outputs.
    Chaining HCF4094 requires nothing different in software, with exception of larger data for shift_data.
    """

    # Due to N-MOSFET Pulling down, logic is reversed

    _OUTPUT_HIGH = 0
    _OUTPUT_LOW = 1

    def __init__(self, gpio_ref, data_gpio, clock_gpio, strobe_gpio, out_enable_gpio,
                 enable_output_immediate=False):
        """
        Initialization

        :param gpio_ref:  reference to RPi.GPIO object
        :param data_gpio: data pin number
        :param clock_gpio: clock pin number
        :param strobe_gpio: strobe pin number
        :param out_enable_gpio: output enable pin number
        :return:
        """
        self._gpio = gpio_ref
        self._data_pin = data_gpio
        self._clock_pin = clock_gpio
        self._strobe_pin = strobe_gpio
        self._out_enable_pin = out_enable_gpio

        self._gpio.setup(self._data_pin, self._gpio.OUT, initial=self._OUTPUT_LOW)
        self._gpio.setup(self._clock_pin, self._gpio.OUT, initial=self._OUTPUT_LOW)
        self._gpio.setup(self._out_enable_pin, self._gpio.OUT, initial=self._OUTPUT_LOW)
        self._gpio.setup(self._strobe_pin, self._gpio.OUT, initial=self._OUTPUT_LOW)

        if enable_output_immediate:
            self.set_output_enable(True)

    def set_output_enable(self, enable):
        """
        Set output enable pin

        :param enable: State of pin
        :return: None
        """
        output_val = self._OUTPUT_LOW
        if enable:
            output_val = self._OUTPUT_HIGH
        self._gpio.output(self._out_enable_pin, output_val)

To send data out, we send an iterable of 0 or 1 values that are clocked out and then the whole data set is strobed to move from buffer to actual output.

I’m using an OrderedDict as my internal storage so I can easily set bit values by dictionary key, but shift them out in the correct order to the proper pin on the shift registers. (Keeping track of bits is a big deal when your shift length is 288.)

    def shift_data(self, data):
        """
        Shifts data out, in order of the list.

        :param data: Data to be shifted as list or tuple
        :return: Bits shifted count
        """
        shift_count = 0
        self._gpio.output(self._strobe_pin, self._OUTPUT_LOW)
        for bit_value in data:
            # Inverting due to N-MOSFET inversion, also error if not 0/1.

            value = (HCF4094._OUTPUT_LOW, HCF4094._OUTPUT_HIGH)[bit_value]
            self._gpio.output(self._data_pin, value)
            self._gpio.output(self._clock_pin, self._OUTPUT_HIGH)
            self._gpio.output(self._clock_pin, self._OUTPUT_LOW)
            shift_count += 1
        self._gpio.output(self._strobe_pin, self._OUTPUT_HIGH)
        return shift_count

HCF4094 Fake Hardware

I call my fake hardware HCF4094Capture as I’m capturing the data that is sent out. In addition to the normal fields in HCF4094.__init__, I also have bit_list and callback. The bit_length is determined by len(bit_list). The callback will be sent a list of tuples with (pin_number, current_state) if any of the pins have changed since last strobe.

I attach a callback to the FakeGPIO for a transition on the clock and strobe pin. This is a FALLING transition, even though I’m actually wanting a RISING transition at the HCF4094 chip. Again, this is due to the reversing on the MOSFETs. Data is buffered during the clock callback and toggled to output (and callback processed) with a strobe callback. I initially had this backwards and could not get my tests to pass. That is why we take the time to write tests.

class HCF4094Capture(object):
    """
    This class is used to emulate the data that is shifted to the HCF4094.

    A callback method is registered and will be called with a list of tuples (index, bit_state) for each
    output that has changed since last strobe.  This allows you to simulate hardware that occurs when the
    bit state changes.

    An example of this would be if the bit is controlling power to a device.  With a `1` bit state, you
    could simulate what actions occur when power is applied to the device.

    """

    def __init__(self, gpio_ref, data_gpio, clock_gpio, strobe_gpio, out_enable_gpio,
                 bits_list, callback):
        """
        Initialization

        Callback method details:

        One argument is given to callback method, a list of tuples (index, 0 or 1).
        index: index of bits_list which has changed since last strobe event.
        0 or 1: new state of bit.

        It is expected that you maintain old state to see if you need to trigger simulation events for what
        electrical event corresponds with that bit changing.

        :param gpio_ref:  reference to Mock.GPIO object
        :param data_gpio: data pin number
        :param clock_gpio: clock pin number
        :param strobe_gpio: strobe pin number
        :param out_enable_gpio: output enable pin number
        :param bits_list: list of bit states for current output, order is furthest to nearest bit.  As shifting
                          occurs at index 0 and finished at index ``n``.
                          This sets initial state to trigger callbacks and bit length
                          Initial state usually all 0, so `[0] * bit_depth` might be easy initialization
        :param callback: method to call when bits change
        """
        self._gpio = gpio_ref
        self._data_pin = data_gpio
        self._clock_pin = clock_gpio
        self._strobe_pin = strobe_gpio
        self._out_enable_pin = out_enable_gpio

        test_list = [bit for bit in bits_list if bit not in (0, 1)]
        if len(test_list) != 0:
            raise ValueError('bits_list may only contain 0 or 1.  Found {}'.format(test_list))

        self._bit_count = len(bits_list)
        self.current_data = tuple(bits_list)
        self._buffered_data = []
        self._callback = callback

        # Register with Mock GPIO to call methods when clock or strobe occurs

        # We are using FALLING instead of RISING, because logic is backwards due to

        # MOSFET for output.

        self._gpio.add_event_callback(self._clock_pin, self._gpio.FALLING, self._clocked)
        self._gpio.add_event_callback(self._strobe_pin, self._gpio.FALLING, self._  strobed)

These two methods are callbacks from GPIO. When a clock transition occurs, we push data (swapped due to negative logic) in the buffer. When the strobe occurs, we send data to attached callbacks.

If strobe is not brought low before data is transferred, it will strobe data down the outputs as you clock the shift. This can be useful in some situations for moving displays. However, I do not allow this in my driver, so I did not emulate in the virtual hardware. We could add this by testing for strobe state and calling _strobed if needed in _clocked.

    def _clocked(self):
        # Have to reverse data, as MOSFET output is backwards

        bit_value = (1, 0)[self._gpio._simulate_read_out_pin(self._data_pin)]
        self._buffered_data.append(bit_value)

    def _strobed(self):
        self._send_data()

My goal for the _send_data method is to build a tuple of index and value for pins that have changed.

First I generate the new_data by adding buffered data to the end and taking the last bits that satisfy the _bit_count of our shift register system. Then I’m using a list comprehension with enumerate to generate my index and zip to give me an (old, new) tuple. The list comprehension only outputs if old != new.

Before sending to the callback, I update my current_data in case the callback decides to process the entire output data.

    def _send_data(self):
        # Make list with last `self._bit_count` number of bits shifted.

        # May be called with only a few bits shifted, so need to include old data.

        new_data = (list(self.current_data) + self._buffered_data)[-self._bit_count:]
        changes = [(index, new)
                   for (index, (old, new)) in enumerate(zip(self.current_data, new_data))
                   if old != new]
        self.current_data = tuple(new_data)
        self._callback(changes)

Testing FakeGPIO

First we look at parts of the tests I wrote when building the FakeGPIO object. With my fixtures, I’m not returning any data. RPi.GPIO uses the global GPIO reference and I do the same. However, I’m using fixtures to reset the GPIO to three states: Initialized (raw), Using BCM numbering (bcm), and using Board numbering (board).

@pytest.fixture
def raw():
    GPIO.cleanup()


@pytest.fixture
def bcm():
    GPIO.cleanup()
    GPIO.setmode(GPIO.BCM)


@pytest.fixture
def board():
    GPIO.cleanup()
    GPIO.setmode(GPIO.BOARD)

I’ve previously talked about using the with pytest.raises(ExceptionType) as a method of proving error conditions are caught. I’m skipping some of these tests and simple functionality checks. You can see those at the full file link above.

GPIO is capable of calling a method when a pin has a rising or falling transition. To catch this callback, I’m using the mock package. func = mock.Mock() provides a callback function that allows you to test what was sent to it or if it was even called.

In both of these methods, I’m creating the callback, configuring the pin so a transition will fire the callback, adding the callback, and triggering it with an output call. To test that it was called, I use func.assert_called_with(). I’m using no arguments, because GPIO callbacks have none. When we write tests for our HCF4094Capture fake object, you will see callbacks with an argument.

Note: If you need many callbacks and only want to use one function, look at functools partial for freezing arguments in a function.

def test_rising_callback(board):
    func = mock.Mock()
    GPIO.setup(38, GPIO.OUT, initial=GPIO.LOW)
    GPIO.add_event_callback(38, GPIO.RISING, func)
    GPIO.output(38, GPIO.HIGH)
    func.assert_called_with()


def test_falling_callback(bcm):
    func = mock.Mock()
    GPIO.setup(6, GPIO.OUT, initial=GPIO.HIGH)
    GPIO.add_event_callback(6, GPIO.FALLING, func)
    GPIO.output(6, GPIO.LOW)
    func.assert_called_with()

Testing HCF4094

Testing HCF4094 functionality is not too complex, because the main functionality we have with a shift register is to just shift out data. Like the GPIO testing above, we need to handle callbacks and will be using mock again.

I define the 4 pins so I can share them between the driver and fake hardware, otherwise they won’t link up. I have one fixture that creates the FakeGPIO from mocked. Then I create a callback with mock.Mock(). We pass this into HCF4094Capture to get changed data back. Then I create the hardware driver HCF4094 and return everything needed in tests as a tuple.

import mock
import pytest
from rpi_hardware.mocked import GPIO
from rpi_hardware.mocked import HCF4904Capture
from rpi_hardware import HCF4094

OUT_EN = 20
STROBE = 19
CLOCK = 26
DATA = 21


@pytest.fixture
def capture():
    GPIO.cleanup()
    GPIO.setmode(GPIO.BCM)
    callback = mock.Mock()
    hcf_capture = HCF4904Capture(GPIO, DATA, CLOCK, STROBE, OUT_EN, [0]*16, callback)
    hcf = HCF4094(GPIO, DATA, CLOCK, STROBE, OUT_EN, True)
    return hcf_capture, hcf, callback

In the first test, you can see how I’m using tuple unpacking to get at the objects sent in with the fixture. I want to test the callback functionality for the fake hardware, without bringing the hardware driver into the mix. So I reached inside the object and modified the internal attributes that would be created with clock callbacks and strobe callbacks. This allowed me to test functionality expected if those callbacks worked.

Notice that we are again testing the fact that a call back occurred with the mock function by using callback.assert_called_with(expected arguments). In this scenario it isn’t just testing that a callback occurred, but doing essentially an assert [expected arguments] == [callback arguments received].

def test_hcf_capture_send_data_internal(capture):
    hcf_capture, hcf, callback = capture
    hcf_capture._buffered_data = [1]
    hcf_capture._send_data()
    callback.assert_called_with([(15, 1)])

    hcf_capture._buffered_data = [1]*16
    hcf_capture._send_data()
    callback.assert_called_with([(index, 1) for index in range(15)])

The value of writing the above function and testing just one piece became apparent when I was developing the callback structure for getting data from HCF4094. This test was to be a full up test of the HCF4094 hardware driver, sending pin changes through the FakeGPIO and triggering callbacks in HCF4094Capture which will trigger a callback for data changed. It is a complex chain. And it didn’t work. I received a failed test, because the callback was never called.

This is where I discovered that the callback I needed to register with FakeGPIO was FALLING instead of RISING, due to the reversed logic. I am a firm believer that tests do not take any more time than writing throw away code to test a method. And instances like this really help you find issues with your code.

def test_hcf4904_shift_data(capture):
    hcf_capture, hcf, callback = capture
    # Shift full set of 1's should get all changes

    hcf.shift_data([1]*16)
    callback.assert_called_with([(index, 1) for index in range(16)])
    # Partial shift

    hcf.shift_data([0, 1, 0, 1])
    callback.assert_called_with([(12, 0), (14, 0)])

So these callbacks in HCF4094Capture are useful for testing operation of our hardware. But this is the hook to simulate the system. If we see a certain pin go to 1 then I know a tablet has been given charging power. I can start making the battery voltage go up and send a web service call indicating PowerApplied. I’ll cover simulation using these interfaces in another article.

Fake SMBus

Implementing fake hardware for SMBus is both easier and harder. The interface to the hardware is defined, so we can use actual method calls and don’t have to track pin transitions in a custom way for each device type. However, the device can be very complex and this would need to be implemented virtually for full simulation. Often you only need to use a certain subset of functionality to cover what you actually use for a device.

However, if you have a full capabilities driver, then you need full capabilities simulator to integrate everything into tests. For the complex chips, I stub out the functionality I need for simulation and am doing much of the testing with the actual hardware. Everything is a tradeoff of effort vs return.

I’m not going to go into SMBus with much detail, as the GPIO implementation was more complex in the topics I wanted to cover in this article. I have two objects in my mocked/smbus.py file: SMBus and FakeSMBDevice. To create a virtual SMBus device, you would inherit from FakeSMBDevice, which provides function stubs for functionality available in SMBus and registers the device with SMBus. You only need to implement what you use. If you get a NotImplementedError, then you use it and didn’t implement it.

DS28CM00 Driver

The DS28CM00 chips is about as simple as an SMBus device gets. This is a silicon serial number. Its sole purpose is to give you a unique 6-byte serial number.

Why would you use something like this? It allows me to keep all software the same on each RPi and use this to load unique configurations from the DB at startup. So pushing updates to all carts is simply a multiple target SCP transfer of files, with no unique configuration data. This is very helpful.

This is a ROM with only 1-bit configurable (toggle between SMBus and I2C modes). Since I drive this at 100 KHz, I never need to touch this. I only need to read the serial number value and check the CRC on the data received. Then I provide this in a hex string format. Since calling hardware is slow and this will not change after being read, I cache the value and only call once.

from .util.crc import crc8_check


class DS28CM00(object):
    """
    I2C driver for DS28CM00 silicon serial number

    This is a simple chip that only gives you a unique serial number.
    It is useful if you want to load the same software on multiple systems
    and still have them able to determine who they are.

    I use this serial as the primary key in the DB.  This allows the device to
    load config data from the DB at start up.
    """

    _ADDRESS = 0b1010000

    def __init__(self, smbus_ref):
        """
        Initalize object and give proper smbus to use.

        :param smbus_ref: smbus object, as create with smbus.Smbus(bus_number) or mock smbus object.
        """
        self._smbus = smbus_ref
        self._serial_number = None
        self._serial_hex = None

    @property
    def serial_number(self):
        """
        Read silicon serial number.

        :return: 48-bit serial number as hex value
        :raises: ValueError if CRC check fails
        """
        if not self._serial_number:
            # Load it once and cache it

            self._smbus.write_byte(self._ADDRESS, 0x00)
            data = [self._smbus.read_byte(self._ADDRESS) for _ in range(8)]
            if not crc8_check(data[:-1], data[-1]):
                raise ValueError('CRC validation failed for reading serial number.')
            self._serial_number = 0
            for byte_value in data[1:-1]:
                self._serial_number = (self._serial_number << 8) + byte_value
            self._serial_hex = hex(self._serial_number)
        return self._serial_hex

Fake DS28CM00

As you see in the code above, I’m only using two methods on the SMBus object: write_byte and read_byte. So these are the only methods I need to implement for my FakeSMBusDevice.

At __init__, we pass in the serial number we want the device to have. I do some validation that we have the correct number of bits and values are within range. I used a list of integers, instead of bytes or bytearray as it eliminated conversions to do the CRC-8 calculation. This is transparent to the user of the DS28CM00 real device.

I setup the memory into _data and also build a _write_map that only allows writing to the 8th byte (configuration register), but have not implemented this write. When reading an SMBus memory, it is common to write the address location then repeatedly call read_byte. So we have an internal _index that points to the next memory. Each call to read_byte increments this and wraps around if we get to the edge of memory.

from .smbus import FakeSMBusDevice
from rpi_hardware import DS28CM00
from rpi_hardware.util.crc import crc8_value


class FakeDS28CM00(FakeSMBusDevice):
    """
    Fake Hardware for DS28CM00, to be talked to using DS28CM00 object for testing and simulation

    DS28CM00 is a silicon serial number.  So we just have a simple memory device that
    is read with multiple byte calls.

    Note: Only implemented write for address selection.  Not for writing of the one
    configuration bit, which would occur with a write of 0x08 followed by 0x00 or 0x01.
    """

    _ADDRESS = DS28CM00._ADDRESS

    def __init__(self, smbus, serial_number_byte_list):
        """
        Initalize object and give proper smbus to use.

        :param smbus: mock smbus object.
        :param serial_number_byte_list: 6 member list with bytes for serial number
        """
        if not len(serial_number_byte_list) == 6:
            raise ValueError('serial_number_byte_list must be a list of 6 byte integers.')
        if max(serial_number_byte_list) > 255 or min(serial_number_byte_list) < 0:
            raise ValueError('serial_number_byte_list contains values not within range(256)')

        # Memory is Family Code (0x70), 6 bytes of serial number, crc8, Control Register byte (0x01)

        data = [0x70] + serial_number_byte_list[:]
        self._data = data + [crc8_value(data)] + [0x01]
        self._write_map = [False] * 8 + [True]
        self._index = 0
        super().__init__(smbus, self._ADDRESS)

    def _inc_index(self):
        self._index += 1
        if self._index > 8:
            self._index = 0

    def write_byte(self, byte):
        if -1 < byte < 9:
            self._index = byte
        else:
            raise ValueError('Valid memory addresses for write at 0x00 to 0x08.')

    def read_byte(self):
        value = self._data[self._index]
        self._inc_index()
        return value

Testing DS28CM00

Testing is straight forward, with a fixture to initialize the mocked.SMBus. I first test that bad values for serial number initialization are not allowed in FakeDS28CM00. Then test_straight_read is just a simple serial number read from FakeDS28CM00 through mocked.SMBus into DS28CM00 and validation that data in is the same a data out.

import pytest
from rpi_hardware.mocked import smbus
from rpi_hardware.mocked import FakeDS28CM00
from rpi_hardware import DS28CM00


@pytest.fixture
def smb():
    bus = smbus.SMBus(1)
    return bus


def test_bad_byte_values(smb):
    bad_number_values = [1, 2, 3, 4, 5]
    with pytest.raises(ValueError):
        ds = FakeDS28CM00(smb, bad_number_values)
    bad_values = [0, -1, 256, 3, 19, 43]
    with pytest.raises(ValueError):
        ds = FakeDS28CM00(smb, bad_values)


def test_straight_read(smb):
    serial_number = [15, 45, 120, 255, 0, 192]

    fds = FakeDS28CM00(smb, serial_number)
    rds = DS28CM00(smb)
    serial = 0
    for byte in serial_number:
        serial = (serial << 8) + byte
    assert hex(serial) == rds.serial_number

Because a CRC is included to validate data and detect if transmission errors occurred, I could add in bad data functionality to my FakeDS28CM00 object and test my CRC errors in the main object. This would allow me to help validate all outcomes.

Summary

This is one of my longer articles, but I hope it was of value for you. Let’s hit some bullet points of what we covered:

  • Raspberry Pi common hardware interfaces
  • How to implement a Singleton model in Python
  • How to create a mocked replacement for both GPIO and SMBus
  • How to structure imports to switch between real and fake interfaces
  • How to implement drivers in a way that allows interface substitutions
  • Testing with pytest and mock to verify callback functions
  • Testing drivers with mocked hardware

When I look at all of those, I can understand why this article is so long. In future articles for this series, I will discuss implementation of the cart workflow and web software and simulation, using these virtual devices.


Part 2 of 3 in the Burn-In Cart series.

Burn-In Carts | RPi Startup IP and Web Display

comments powered by Disqus