Introduction
In firmware development, effective testing is crucial. Developers often need reliable ways to interact with their hardware. This tutorial will guide you through creating a Python-based command-line interface (CLI) tool that leverage the CANable USB-to-CAN adapter for testing devices on a CAN bus. By building this tool, you’ll streamline your testing process and provide a valuable resource for your entire team.
(Check out this link for more information about the CANable.)
What is Python Click?
We’ll be using Python Click, a powerful package for creating command-line interfaces. Click, short for “Command Line Interface Creation Kit,” offers several advantages:
- Arbitrary nesting of commands
- Automatic help page generation
- Lazy loading of commands at runtime
These features make Click an excellent choice for building maintainable and user-friendly CLI tools. For more details, visit the Python Click official website.
Setting Up the Project
It’s easy enough to run your script locally, but we want to allow the easy sharing of these tools across our team. To make distribution and use of the tool easier, we can use Python setuptools.
Let’s start by creating a setup.py file in your project directory. First, import the setup() function from the setuptools package and pass a name and a version to the setup() function. The name will be the name of the tool you install and execute to run the helper script. Since we are using the Click package, add 'click == 8.1.7' to the install_requires entry. This will automatically install the v8.1.7 Click package when we install the tool.
# setup.py
from setuptools import setup
setup(
name='canable',
version='0.1.0',
install_requires=[
'click == 8.1.7',
],
)
Creating the CLI Structure
Our Click-based CLI tool will run in a similar way to any other CLI tool. For most cases you will specify an executable name, then a command, and then one or more flags.
$ <executable> <command> <--flag1> <--flag2>
Our executable will be named canable as specified in our setup.py. To interact with the CAN bus, we want to be able to send and receive CAN messages. Therefore we want a send command and a receive command. Click uses a Python construct called a Decorator to mark each function as a command.
In Click, each command is part of a group. To simplify this example, we create a single group called cli that ultimately works just as a pass-thru, but your tool could include layers of hierarchy as it evolves.
Let’s dive into the actual script. Create a file named main.py.
# main.py
import click
@click.group()
def cli():
pass
@cli.command()
def send():
"""Sends a CAN message using a CANable."""
click.echo('Sending CAN messages...')
@cli.command()
def receive():
"""Receives CAN messages using a CANable."""
click.echo('Receiving CAN messages...')
if __name__ == '__main__':
cli()
Preparing and Installing the Package
Now let’s prepare our script to be installed and run by our users. We can make use of the entry_points entry in setup.py to achieve this. The canable = main:cli maps the name of our executable canable to the main:cli function in our main.py script.
The following is the usage of the entry_points argument:
# setup.py
setup(
# ...
entry_points={
'console_scripts': [
'canable = main:cli',
]
}
)
When we install the package, a canable executable will be created. The canable executable will invoke the cli() function in main.py, which will in turn execute the appropriate command in the cli() group.
Now that we have everything ready, it’s time to install the package with the following command:
pip install --editable <PATH/TO/PROJECT>
The —-editable or -e option allows us to continue editing the script even after installing it.
Running the Tool
After the package is installed, we can now run the tool. Although the tool doesn’t do much yet, Python Click has automatically created a help page for us!
Run the following command to show the help page:
$ canable --help
Usage: canable [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
receive Receives CAN messages using a CANable.
send Sends a CAN message using a CANable.
In order to send or receive CAN messages, we need to first import the can module into main.py. To install the can module automatically, add the can module to setup.py.
# setup.py
setup(
...
install_requires=[
'click == 8.1.7',
'python-can[serial] == 4.3.1',
],
...
)
# main.py
import click
import can
...
Implementing CAN Message Sending and Receiving
For those who haven’t worked with CAN bus devices, it is a differential pair bus used often on machinery, heavy equipment, automotive, industrial automation, and a variety of other cases. Devices on the bus typically need to know the bit rate to participate on the bus.
Here’s the code to initialize the CAN bus device and send and receive some dummy data.
# main.py
...
@cli.command()
def send():
"""Sends a CAN message using a CANable."""
with can.interface.Bus(bustype='slcan', channel='/dev/cu.usbmodem11301', bitrate=500000) as bus:
try:
msg = can.Message(arbitration_id=0xc0ffee,
data=[0, 25, 0, 1, 3, 1, 4, 1],
is_extended_id=True)
try:
bus.send(msg)
click.echo("Message {} sent successfully".format(message))
except can.CanError:
click.echo("Failed to send message {}.".format(message))
except KeyboardInterrupt:
bus.shutdown()
pass
@cli.command()
def receive(bustype, channel, bitrate):
"""Receives CAN messages using a CANable."""
click.echo('Receiving CAN messages...')
with can.interface.Bus(bustype='slcan', channel='/dev/cu.usbmodem11301', bitrate=500000) as bus:
try:
while True:
msg = bus.recv(1)
if not msg:
continue
click.echo(msg)
except KeyboardInterrupt:
bus.shutdown()
pass
...
In this version, you may have noticed that we had to hard-code the bustype, channel, and bitrate. These values, especially the channel, are almost certainly different for your application. Let’s learn about options to find out how we can make this tool more flexible and avoid hard-coding.
Adding Options (Flags) to Your CLI Tool
Click also supports options – which are often referred to as flags. Let’s enhance our tool by adding some flags to our commands.
For both send and receive commands, a few useful options may be to set the protocol of the USB-to-CAN adapter, the USB-to-CAN adapter device port, and CAN bit rate. We can set common default values and show them when we display our help.
slcan is a common communication protocol for serial-based USB-to-CAN adapter devices and is well supported on many platforms.
For the device port, we can set a default value automatically by using serial.tools.list_ports. Our version simply grabs the first available CANable. This way most users will be able to use the tool without providing the port, but if an advanced user has multiple CANable devices, they can use the tool as well by specifying the ports.
500 kbps is a common rate for CAN bus devices, so we set the default of 500000.
# main.py
...
import serial.tools.list_ports
channel = ""
# The CANable will appear as a USB CDC device: /dev/ttyACMX or /dev/ttyUSBX on Linux or /dev/cu.usbmodemXXXX on on Mac.
ports = serial.tools.list_ports.grep("^/dev/cu.usbmodem")
for p in ports:
if "CANable" in p.description:
channel = p.device
break
...
@cli.command()
@click.option('--bustype', default='slcan', show_default=True, help='The CAN bus firmware.')
@click.option('--channel', default=channel, show_default=True, help='The USB CDC device port.')
@click.option('--bitrate', default=500000, show_default=True, help='The CAN bit rate in Hz.')
def send(bustype, channel, bitrate):
"""Sends a CAN message using a CANable."""
with can.interface.Bus(bustype=bustype, channel=channel, bitrate=bitrate) as bus:
try:
msg = can.Message(arbitration_id=0xc0ffee,
data=[0, 25, 0, 1, 3, 1, 4, 1],
is_extended_id=True)
try:
bus.send(msg)
click.echo("Message {} sent successfully".format(message))
except can.CanError:
click.echo("Failed to send message {}.".format(message))
except KeyboardInterrupt:
bus.shutdown()
pass
@cli.command()
@click.option('--bustype', default='slcan', show_default=True, help='The CAN bus firmware.')
@click.option('--channel', default=channel, show_default=True, help='The USB CDC device port.')
@click.option('--bitrate', default=500000, show_default=True, help='The CAN bit rate in Hz.')
def receive(bustype, channel, bitrate):
"""Receives CAN messages using a CANable."""
click.echo('Receiving CAN messages...')
with can.interface.Bus(bustype=bustype, channel=channel, bitrate=bitrate) as bus:
try:
while True:
msg = bus.recv(1)
if not msg:
continue
click.echo(msg)
except KeyboardInterrupt:
bus.shutdown()
pass
...
Adding an Option for Message Content
Currently, we are only able to send a fixed message. To pass the message as an argument to the command, we can make use of the callback argument with a helper function. This keeps the send() command code clean and allows the message to be passed directly to the can.Message() constructor without additional manipulation.
# main.py
...
def split_commas(ctx, param, value: str) -> list[int]:
"""Splits commas. Used as callback in click Options."""
if not value:
return []
return [int(x) for x in value.split(",")]
...
@cli.command()
@click.option('--bustype', default='slcan', show_default=True, help='The CAN bus firmware.')
@click.option('--channel', default=channel, show_default=True, help='The USB CDC device port.')
@click.option('--bitrate', default=500000, show_default=True, help='The CAN bit rate in Hz.')
@click.option('--message', type=str, callback=split_commas, help='The message to be sent.')
def send(bustype, channel, bitrate, message):
"""Sends a CAN message using a CANable."""
with can.interface.Bus(bustype=bustype, channel=channel, bitrate=bitrate) as bus:
try:
msg = can.Message(arbitration_id=0xc0ffee,
data=message,
is_extended_id=True)
try:
bus.send(msg)
click.echo("Message {} sent successfully".format(message))
except can.CanError:
click.echo("Failed to send message {}.".format(message))
except KeyboardInterrupt:
bus.shutdown()
pass
...
Conclusion
You’ve now created a functional CLI tool that can send and receive CAN messages. By creating this tool, you’ve significantly streamlined the process of interacting with CAN bus devices using a CANable device. This tool can be easily distributed to your team, ensuring consistent testing procedures and decreasing the testing burden across your team!
Sending a message:
$ canable send --message 1,2,3,4,5,6,7,8,9
Message [1, 2, 3, 4, 5, 6, 7, 8, 9] sent successfully
Receiving a message:
$ canable receive
Receiving CAN messages...
Dojo Five can help you create test scripts to interact with or validate your board. We bring modern tools, techniques, and best practices from the web and mobile development environments, paired with leading-edge innovations in firmware, to our customers to help them build successful products and successful clients. Our talented engineers are on hand, ready to help you with all aspects of your EmbedOps journey. Bring your interesting problems that need solving – we are always happy to help out. You can reach out at any time on LinkedIn or through email!
Sign up to get our content updates!
Unlock the full potential of your embedded projects with our expert insights! Dive into our comprehensive resources to stay ahead in firmware development. Subscribe now to get the latest best practices and guides delivered straight to your inbox.
Sign Up for Updates


