Add a New Command¶
Adding a new command involves three phases:
- Declare Protobuf Messages (Protobuf stands for Protocol Buffers)
- Extend USB DevKit Firmware
- Extend TROPIC01 Util
All steps below are relative to the libtropic/examples/applications/tropic01_util/ directory.
Before you start, complete Install and Use and then follow these steps to install the protobuf-compiler, which is needed when adding new Protobuf messages, as the Protobuf files have to be re-generated:
Install Protobuf compiler
- Install:
- Ubuntu/Debian:
sudo apt update && sudo apt install protobuf-compiler - Fedora:
sudo dnf install protobuf-compiler
- Ubuntu/Debian:
- Ensure the version is 3 or newer:
protoc --version
- Try running it:
- Build the USB DevKit firmware with additional CMake option — follow these instructions and add the
-DGEN_PROTO_FILES=1switch tocmake. - CMake automatically runs
protobuf-compiler, generates Protobuf files in C (for the firmware) and Python (for the CLI application), and places them at the expected location — Check that it builds without any errors. - Because the Protobuf messages have not been changed yet,
gitshould not report any new Protobuf files.
- Build the USB DevKit firmware with additional CMake option — follow these instructions and add the
- Install:
- Homebrew:
brew install protobuf
- Homebrew:
- Ensure the version is 3+:
protoc --version
- Try to run it. TBA
- Install:
- Winget:
winget install protobuf
- Winget:
- Ensure the version is 3+:
protoc --version
- Try to run it. TBA
Declare Protobuf Messages¶
We need to define two new Protobuf messages: one for the command (raw or application), and one for the response. This is done in the protobuf/usb_devkit_messages.proto file:
- Decide whether the new command is raw (RawCmd) or application (AppCmd). Because the approach is same for both types, we will show how to add a new application command and application response.
- Come up with a new name in format
<cmd_name_in_camel_case>Cmd, for exampleAddNumsAndPingCmd. It will add two numbers and send the result to TROPIC01 using the Ping L3 command to showcase how to work with Libtropic in the TROPIC01 Util ecosystem. - Look for the
AppCmdmessage declaration and add the new command:// =============================== AppCmd ================================ message AppCmd { oneof type { PinSetCmd pin_set = 1; // <other commands> AddNumsAndPingCmd add_nums_and_ping = 2; } }- The number assigned to
add_nums_and_pingis a field number, not a value. This number must be unique. - Always assign a field number of the previous command (in our case
pin_set) plus one.
- The number assigned to
- Declare
AddNumsAndPingCmdbelowAppCmdand all other application commands:// ------------------------------- AddNumsAndPingCmd message AddNumsAndPingCmd { int32 a_in = 1; int32 b_in = 2; }AddNumsAndPingCmdmay have some fields (e.g. thea_in) — see available types in the Protobuf documentation (it is also valid to have no fields, like inGetGpoCmd). Again, use appropriate field number.
- Look for the
AppRespmessage declaration and add the new responseAddNumsAndPingResp:// =============================== AppResp =============================== message AppResp { optional uint32 libtropic_res_code = 1; oneof type { PinSetResp pin_set = 2; // <other responses> AddNumsAndPingResp add_nums_and_ping = 3; } }- The number assigned to
add_nums_and_pingis a field number, not a value. This number must be unique. - Always assign a field number of the previous response (in our case
pin_set) plus one.
- The number assigned to
- Declare
AddNumsAndPingRespbelowAppRespand all other application responses, along withAddNumsAndPingRespCodeenum (for result codes):// ------------------------------- AddNumsAndPingResp message AddNumsAndPingResp { AddNumsAndPingRespCode res_code = 1; int32 c_out = 2; } enum AddNumsAndPingRespCode { ADD_NUMS_AND_PING_RESP_CODE_UNSPECIFIED = 0; ADD_NUMS_AND_PING_RESP_CODE_OK = 1; ADD_NUMS_AND_PING_RESP_CODE_ERROR = 2; ADD_NUMS_AND_PING_RESP_CODE_MSG_MISMATCH = 3; // other optional result codes }- Always declare the
res_codefield — that way, error reporting is unified across all responses. AddNumsAndPingRespCode: always declare the*_UNSPECIFIEDvalue (required by Protobuf) and the*_OKvalue to indicate success. Other values for the errors are not mandatory, but recommended (see other result code enums for inspiration).AddNumsAndPingRespmay have some fields (e.g. thec_out) — see available types in the Protobuf documentation (it is also valid to have no fields, like inGetGpoCmd). Again, use appropriate field number.
- Always declare the
- Depending on the types you used for
AddNumsAndPingCmdorAddNumsAndPingRespfields, you may need to add some constraints in theprotobuf/usb_devkit_messages.optionsfile:- This file is specific to Nanopb, a plain-C implementation of Protobuf, targeted at embedded systems. We are using Nanopb in the USB DevKit Firmware because Protobuf does not support the C language.
- See the available options in the Nanopb documentation. Typically, some size constraints should be used — feel free to get inspired by other command/responses.
Extend USB DevKit Firmware¶
Before adding any new changes to the USB DevKit firmware, run the protobuf-compiler to validate the changes from above. As already mentioned, the firmware's CMake runs protobuf-compiler automatically when the -DGEN_PROTO_FILES=1 switch is passed to it; follow these instructions and pass the switch to cmake. After this, the files inside protobuf/generated/ should be updated with your changes from above.
Now, modify the firmware:
- Create a new header file
add_nums_and_ping.hfor the command callback function in:firmware/Inc/app_cmd/, if it's an application command (our case),firmware/Inc/raw_cmd/, if it's a raw command.
- The contents of
add_nums_and_ping.hshould look like this:#ifndef ADD_NUMS_AND_PING_H #define ADD_NUMS_AND_PING_H // This is where the Protobuf messages are declared. #include "usb_devkit_messages.pb.h" void add_nums_and_ping(const AddNumsAndPingCmd *cmd, AppResp *resp); #endif // ADD_NUMS_AND_PING_H -
Create a new source file
add_nums_and_ping.cfor the command callback function in:firmware/Src/app_cmd/, if it's an application command (our case),firmware/Src/raw_cmd/, if it's a raw command.
After that, add the file to
firmware/CMakeLists.txt:4. Implement the command callback function — the contents of# Add sources to executable target_sources(${CMAKE_PROJECT_NAME} PRIVATE # Project sources # <other sources in app_cmd/> ${CMAKE_CURRENT_SOURCE_DIR}/Src/app_cmd/add_nums_and_ping.c )add_nums_and_ping.cshould look like this:#include "app_cmd/add_nums_and_ping.h" #include "libtropic.h" #include "libtropic_common.h" #include "main.h" #include "usb_devkit_messages.pb.h" void add_nums_and_ping(const AddNumsAndPingCmd *cmd, AppResp *resp) { // Add the numbers. resp->type.add_nums_and_ping.c_out = cmd->a_in + cmd->b_in; // Ping TROPIC01. uint8_t msg_out[sizeof(resp->type.add_nums_and_ping.c_out)]; uint8_t msg_in[sizeof(resp->type.add_nums_and_ping.c_out)]; memcpy(msg_out, &resp->type.add_nums_and_ping.c_out, sizeof(msg_out)); lt_ret_t lt_ret = lt_ping(<_handle, msg_out, msg_in, sizeof(msg_out)); // Check libtropic result code. if (lt_ret != LT_OK) { resp->type.add_nums_and_ping.res_code = ADD_NUMS_AND_PING_RESP_CODE_ERROR; resp->has_libtropic_res_code = true; resp->libtropic_res_code = lt_ret; return; } // Check if the incoming message is the same as the outgoing. if (0 != memcmp(msg_out, msg_in, sizeof(msg_out))) { resp->type.add_nums_and_ping.res_code = ADD_NUMS_AND_PING_RESP_CODE_MSG_MISMATCH; return; } resp->type.add_nums_and_ping.res_code = ADD_NUMS_AND_PING_RESP_CODE_OK; }resp->has_libtropic_res_codeis initialized tofalsebeforeadd_nums_and_ping()is called. Other fields inrespare also initialized to their default values.
-
Call
add_nums_and_ping()in:process_app_cmd()function insidefirmware/Src/app_cmd/app_cmd_common.c, if it's an application command (our case),process_raw_cmd()function insidefirmware/Src/raw_cmd/raw_cmd_common.c, if it's a raw command.
To do that, add a new
caseto theswitch()(before thedefaultcase):#include "app_cmd/add_nums_and_ping.h" // Put it to the top of the file. void process_app_cmd(const UsbDevkitCmd *cmd, UsbDevkitResp *resp) { switch (cmd->type.app.which_type) { // <other cases> case AppCmd_add_nums_and_ping_tag: resp->type.app.which_type = AppResp_add_nums_and_ping_tag; add_nums_and_ping(&cmd->type.app.type.add_nums_and_ping, &resp->type.app); break; default: resp->which_type = UsbDevkitResp_error_tag; resp->type.error.res_code = ERROR_RESP_CODE_UNKNOWN_CMD; break; } } -
Re-build the firmware in
firmware/build/and check there are no errors (no need to runcmakeagain, just run the generator, e.g.make). - Flash the modified firmware into the USB DevKit — follow the instructions in Flash USB DevKit Firmware.
Extend TROPIC01 Util¶
At this moment, TROPIC01 Util supports only application commands, so this step applies only to them.
Add support for the new application command:
- Create a new file
add_nums_and_ping.pyintropic01_util_app/commands/. In it, we will implement the CLI command that will issue theAddNumsAndPingCmdUSB DevKit application command. - At the top of
add_nums_and_ping.py, handle imports:from __future__ import annotations """add-nums-and-ping command.""" import argparse from ..command_core import AppCommandSender, CliCommandSpec, print_libtropic_res_code from protobuf.generated import usb_devkit_messages_pb2 as pb - In the end of
add_nums_and_ping.py, create a new instance ofCliCommandSpec— this defines some basic properties of the CLI command:ADD_NUMS_AND_PING_SPEC = CliCommandSpec( name="add-nums-and-ping", help_text="Adds two integers and pings TROPIC01.", # Short help text. description="Adds two 32-bit integers and sends them to TROPIC01 via the Ping L3 command.", # Detailed description. add_arguments=add_arguments, # Callback to add arguments to argparse. execute=execute # Callback to execute the command. )- For more details about the
CliCommandSpecclass, look intotropic01_util_app/command_core.py.
- For more details about the
- In the middle of
add_nums_and_ping.py, implementadd_arguments()callback:def add_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--a", type=int, required=True, help="First addend A.", ) parser.add_argument( "--b", type=int, required=True, help="Second addend B.", ) - In the middle of
add_nums_and_ping.py, implementexecute()callback:def execute(args: argparse.Namespace, app_cmd_sender: AppCommandSender) -> int: # Construct the AddNumsAndPingCmd to send. app_cmd = pb.AppCmd() app_cmd.add_nums_and_ping.a_in = args.a app_cmd.add_nums_and_ping.b_in = args.b # Send the command. app_resp = app_cmd_sender.send( app_cmd=app_cmd, expected_resp_type="add_nums_and_ping" ) # Check the result code and print result. res_code = app_resp.add_nums_and_ping.res_code print(f"{ADD_NUMS_AND_PING_SPEC.name} result: {pb.AddNumsAndPingRespCode.Name(res_code)}") print_libtropic_res_code(app_resp, ADD_NUMS_AND_PING_SPEC.name) # Prints only if there is some if res_code == pb.ADD_NUMS_AND_PING_RESP_CODE_OK: print(f"{ADD_NUMS_AND_PING_SPEC.name} addition: {app_resp.add_nums_and_ping.c_out}") return 0 return 1 - In
tropic01_util_app/commands/__init__.py, register the new CLI command:"""Registry of available CLI commands.""" # Import new commands here: # <other command imports> from .add_nums_and_ping import ADD_NUMS_AND_PING_SPEC # Add new commands here: COMMAND_SPECS = { # <other commands> ADD_NUMS_AND_PING_SPEC.name: ADD_NUMS_AND_PING_SPEC }
After these steps, you should be able to execute the new command using tropic01_util.py.