Dependency Injection - locally testing a Python IoT component
Dependency Injection is a method of reducing coupling by removing the knowledge of where how that component's dependencies are created. The dependencies required in a component are provided to it rather than created by it. Removing this coupling lets us change the nature of the things a component relies on. In the case of this example: a hardware-specific dependency can be swapped with a virtual hardware version for testing or for alternative uses.
References
Sample Code: https://github.com/freemansoft/ESP8266-MicroPython
Video Discussion: https://youtu.be/cHM4FydObmw
Summary View
Our code contains two different entry points.
- On the IoT device, main.py is the actual IoT based entry point. main.py can configure I/O pins based on some configuration information. It can then do the same for Servo pins. Finally, the main.py initializes a web server passing in the configured I/O and Servo pins.
- On our development machine or any computer, webserver_test.py is the entry point. The test program can create Fake I/O and Servo pins that implement the same API as the hardware I/O and Servo pins on the IoT device. The test program initializes the fake pins and then initializes the web server passing in the fake I/O and Servo objects.
From this point on the web server operates the same. It updates the pins, hardware on the IoT device, and fake software pins on the development machine.
Dependency Injectable
I have a Python-based web server that runs on IoT devices that accepts commands GET requests. It updates hardware devices based on the GET parameters. I want to run the same web server on my development machine while working on the HTTP and HTML pieces without having to redeploy to my IoT device.
The IoT device has a variety of pins that can be configured differently. We could have the web server initialize the pins based on some configuration files. The web server would read the file and then program the pins. This creates a hard dependency against the pins in the web server code.
We instead want to inject the pins into the web server at initialization time. Those I/O and Servo pins can be created in any fashion and then injected into the initializer. The only requirement is that the passed in pins implement some API that the web server needs to change digital I/O state or servo positions.
class WebServer(object):
"""webserver that can change output pins and display current pin state"""
def __init__(
self,
control_pins,
control_pin_labels,
control_pin_on_high,
servo_pins,
servo_pin_labels,
monitor_pins,
message,
):
...
def run_server(self):
...
https://github.com/freemansoft/ESP8266-MicroPython/blob/main/webserver.py
The Web Page
This image shows the Output Pins and Servo Pins sections. Those sections control their respective pin times, either hardware or simulated.
Click the image to Enlarge
IoT Usage
This shows how the WebServer is initialized on the IoT device. It uses hardware machine pins and the servo machine support class that wraps a hardware pin. All of the hardware device specific setup work happens prior to the web server initialization
server = WebServer(
[machine.Pin(2, machine.Pin.OUT), machine.Pin(16, machine.Pin.OUT)],
["LED (Pin 2)", "RELAY (Pin 16)"],
[False, True],
[Servo(machine.Pin(14))],
["Servo (P 14)"],
[machine.Pin(i) for i in [0, 2, 4, 5, 12, 13, 15, 16]],
"Station:" + str(ipinfo_sta[0]) + "<br/>AP:" + str(ipinfo_ap[0]),
)
server.run_server()
Dev Machine Usage
This shows how the WebServer is initialized on the developer device. It uses fake machine pins and a fake servo support class. All of the fake device setup work happens prior to the web server initialization
pin1 = FakePin(2) pin2 = FakePin(5) pin3 = FakePin(16) out_pins = [pin1, pin3] out_labels = ["LED (Pin 2)", "RELAY (Pin 16)"] out_inversion = [False, True] servo_pins = [FakeServo(FakePin(14))] servo_labels = ["Servo 14"] out_pins_all = [pin1, pin2, pin3] server = WebServer( out_pins, out_labels, out_inversion, servo_pins, servo_labels, out_pins_all, "Hello this is the message area", ) server.run_server()
pin1 = FakePin(2)
pin2 = FakePin(5)
pin3 = FakePin(16)
out_pins = [pin1, pin3]
out_labels = ["LED (Pin 2)", "RELAY (Pin 16)"]
out_inversion = [False, True]
servo_pins = [FakeServo(FakePin(14))]
servo_labels = ["Servo 14"]
out_pins_all = [pin1, pin2, pin3]
server = WebServer(
out_pins,
out_labels,
out_inversion,
servo_pins,
servo_labels,
out_pins_all,
"Hello this is the message area",
)
server.run_server()
FakePin - Test Pins
The web server knows about three functions of a Pin. Our test/developer Pins need to only implement the API called by the web server. This should be an interface but I didn't want to take up any more space in my IoT device. It can get and set the I/O pin value and generate a string representation of a Pin. Our FakePin implements the API and thus can be injected in place of physical hardware pins objects
class FakePin(object):
"""This is a test class. Do NOT install this class on the MicroPython board."""
def __init__(
self,
pin_no,
pin_mode=-1
):
def __str__(self) -> str:
def value(self, value=None):
Fake Servo - Test Servos
The web server knows about three functions on a Servo. Our test/developer Servos need to only implement the API called by the web server. This should be an interface but I didn't want to take up any more space in my IoT device. It knows how to set a servo position as an angle or as a function of time. It also knows how to generate a string representation of itself in its __str__() method. Our FakeServo implements the API and thus can be injected in place of physical servo objects
class FakeServo:
"""
Exact copy of servo.py without the PWM controls
"""
def __init__(self, pin, freq=50, min_us=600, max_us=2400, angle=180):
self.min_us = min_us
self.max_us = max_us
self.us = 0
self.freq = freq
self.angle = angle
# self.pwm = PWM(pin, freq=freq, duty=0)
# hacked in test
self._pin = pin
def __str__(self) -> str:
def write_us(self, us):
def write_angle(self, degrees=None, radians=None):
Conclusion
Dependency Injection and Inversion of Control reduce the coupling between what a component does and how it is configured. This is most often used for switching between injecting production or mock/test dependencies. It can also be used to inject completely different implementations like if you had different Pin implementations for different types of IoT devices.
Video
Revision history
2022 12 created
Comments
Post a Comment