Case Study: Serial DC Motor Controller¶
This article will walk you through connecting Parlay to a simple serial motor controller from Sparkfun. Details and specifications of the motor controller can be found here.
Motor Controller Serial Interface¶
The interface with the motor controller is very simple. The motor controller drives two DC motors. Over a serial connection, we can command the motors to spin with an ASCII string formatted like “1f5\r”. There are three characters in the command, and then it is terminated with a carriage return (not a newline character!).
- 1st character: “1” means motor 1, “2” means motor 2
- 2nd character: “f” means spin forward, “r” means spin in reverse
- 3rd character: “5” means spin at speed 5, in range 0-9. 0 is stopped, 9 is 100% PWM.
The parameters for the serial interface are 115200 baud, 8-N-1.
Desired Commands¶
Since we are writing a custom Protocol and custom Item to interact with this motor controller, we can decide how we want our users to interact with our Item. The users don’t have to know the serial interface described above – they can interact with the motors using commands that we create. So, let’s decide on the following requirements:
- We want Motor 1 and Motor 2 to be separate Items
- We want to have understandable commands for our motors: “Spin” and “Stop”
- When we give our motor a “Spin” command, we want to be able to specify the speed as a positive or negative number
- These commands should be translated into the correct serial string to be sent to the motor controller
The code¶
Here is the code to achieve this. We will examine it a bit at a time.
from parlay.protocols.serial_line import ASCIILineProtocol, LineItem
from parlay import parlay_command, start
class SerialMotorControllerProtocol(ASCIILineProtocol):
def __init__(self, port):
super(SerialMotorControllerProtocol, self).__init__(port=port)
self.items = [SerialMotorControllerItem(1, "Motor1", "Motor1", self),
SerialMotorControllerItem(2, "Motor2", "Motor2", self)]
@classmethod
def open(cls, broker, port="/dev/ttyUSB0"):
return super(SerialMotorControllerProtocol, cls).open(broker, port=port, baudrate=115200, delimiter="\r")
class SerialMotorControllerItem(LineItem):
def __init__(self, motor_index, item_id, name, protocol):
LineItem.__init__(self, item_id, name, protocol)
self._motor_index = motor_index
@parlay_command()
def spin(self, speed):
"""
Move the motor at a constant speed, between -9 and 9. Negative speed causes motor to spin in reverse.
:param speed: speed to move
:type speed int
:return: serial response from motor controller
"""
speed = int(speed)
if speed > 9 or speed < -9:
raise ValueError("Speed outside range") # this exception causes an error message to be sent back to whoever sent the command
direction = "f" if speed >= 0 else "r"
self.send_raw_data("{}{}{}".format(self._motor_index, direction, abs(speed)))
# this waits for a response string from the motor controller, which will be shown in the UI as "result"
return self.wait_for_data()
@parlay_command()
def stop(self):
"""
Stop the motor
:return: serial response from motor controller
"""
# returns what spin() returns, which is the motor controller response string
return self.spin(0)
if __name__ == "__main__":
start()
The Protocol¶
The first step is to create our own Protocol class, which we will call
SerialMotorControllerProtocol
.
class SerialMotorControllerProtocol(ASCIILineProtocol):
Parlay has pre-built Protocol classes for many common interfaces,
including delimited ASCII serial communication. The class that does this
is ASCIILineProtocol
, which will communicate over a serial line
using our specified COM port, baudrate, and delimiter character.
Override the __init__
method¶
To fulfill our requirements that we specified above, we must override
the __init__
function and populate the self.items
list with our
SerialMotorControllerItem
objects (described below). The items in
self.items
will be visible after the user has performed a discovery.
def __init__(self, port):
super(SerialMotorControllerProtocol, self).__init__(self, port=port)
self.items = [SerialMotorControllerItem(1, "Motor1", "Motor1", self),
SerialMotorControllerItem(2, "Motor2", "Motor2", self)]
Override the open
class method¶
SerialMotorControllerProtocol
inherits from ASCIILineProtocol
,
which inherits from BaseProtocol
. BaseProtocol
has an open
method that any child class must override. ASCIILineProtocol
already does this, which handles setting up the serial port with the
desired settings.
For our motor controller, the baudrate and delimiter character are
specified by the hardware, so there’s no need to make the user specify
that. So, in SerialMotorControllerProtocol
, we also override the
open
class method and specify the baudrate to be 115200 baud, and
the delimiter character to be “\r”, or carriage return.
The broker
argument of the __open__
function is required.
@classmethod
def open(cls, broker, port="/dev/ttyUSB0"):
return super(SerialMotorControllerProtocol, cls).open(broker, port=port, baudrate=115200, delimiter="\r")
When calling our parent’s open
method, we must use python’s
super
function like so:
super(SerialMotorControllerProtocol, cls).open(...)
If we were to override open
like below, our protocol would be shown
in the Parlay User Interface as a ASCIILineProtocol
, rather than a
SerialMotorControllerProtocol
like we want:
@classmethod
def open(cls, broker, port="/dev/ttyUSB0"):
# WRONG! DON'T DO THIS!
return ASCIILineProtocol.open(broker, port=port, baudrate=115200, delimiter="\r")
Using the base class get_discovery
method¶
SerialMotorControllerProtocol
inherits from ASCIILineProtocol
,
which inherits from BaseProtocol
. BaseProtocol
has a get_discovery
method defined as follows:
def get_discovery(self):
return {'TEMPLATE': 'Protocol',
'NAME': str(self),
'protocol_type': getattr(self, "_protocol_type_name", "UNKNOWN"),
'CHILDREN': [x.get_discovery() for x in self.items]}
For the base get_discovery
method to work, the protocol must create a list
of items in self.items
that each support their own get_discovery
method.
We already took care of this in our __init__
method as described above. As
described below, our SerialMotorControllerItem
class inherits from
ParlayCommandItem
, which means that it supports the get_discovery
method.
The Item¶
The second step is to create our own Item class, which we will call
SerialMotorControllerItem
. It inherits from LineItem
, which is a
pre-built class designed to work with a serial protocol. It provides the
helper function send_raw_data
, which we will use later to send our
commands out the serial port.
The __init__
method¶
We must call our parent’s init function (not necessary to use
super
here). We also store the provided motor_index
in a member
variable so we can correctly format the command strings to be sent over
the serial port to the motor controller.
class SerialMotorControllerItem(LineItem):
def __init__(self, motor_index, item_id, name, protocol):
LineItem.__init__(self, item_id, name, protocol)
self._motor_index = motor_index
The spin
command¶
SerialMotorControllerItem
inherits from LineItem
, which inherits
from ParlayCommandItem
. This base class takes care of a lot of grunt
work for you to make your command functions be discoverable and visible
in the Parlay User Interface.
To make a command that is visible in the UI, just create a function and
decorate it with @parlay_command
.
@parlay_command()
def spin(self, speed):
"""
Move the motor at a constant speed, between -9 and 9. Negative speed causes motor to spin in reverse.
:param speed: speed to move
:type speed int # the Parlay UI can use type hinting to force the user to enter an integer
:return: serial response from motor controller
"""
speed = int(speed)
if speed > 9 or speed < -9:
raise ValueError("Speed outside range") # this exception causes an error message to be sent back to whoever sent the command
direction = "f" if speed >= 0 else "r"
self.send_raw_data("{}{}{}".format(self._motor_index, direction, abs(speed)))
# this waits for a response string from the motor controller, which will be shown in the UI as "result"
return self.wait_for_data()
The stop
command¶
Stopping the motor is just sending it a spin
command with speed = 0.
We can do that! Once again, we add the @parlay_command
decorator to
the function.
@parlay_command()
def stop(self):
"""
Stop the motor
:return: serial response from motor controller
"""
# returns what spin() returns, which is the motor controller response string
return self.spin(0)
Starting Parlay¶
If this file is called as a python script, such as
$ python motor_controller.py
, we can start parlay automatically.
Otherwise, we can import this file in any other python file to use the
SerialMotorControllerProtocol
and SerialMotorControllerItem
that
we have just defined.
if __name__ == "__main__":
start()