-
Notifications
You must be signed in to change notification settings - Fork 3
Tutorial
This step-by-step tutorial shows how to call a function in Python but make it execute on an MCU.
PC with Linux, macOS or Windows.
To follow this tutorial you need some sort of MCU board with serial connection to the PC.
You will need Python >= 3.10. And because we will use serial port for communication, you also need to install pyserial.
pip install pyserialPython package management is out of the scope of this tutorial. Compost supports usage as an installed package, but also without installation. We will use the most simple approach, which is to use the source archive without installation.
Extract the source archive and enter the compost_rpc directory in your favorite
terminal.
cd compost_rpc-0.6.0/compost_rpcYou can check your setup by running the main module:
$ python compost_rpc.py
Compost 0.6.0
Pyserial found.
This module does nothing when called as a script, it's only usable as a library.Compost uses the Python language itself to define the protocol. You may be more familiar with the name "interface" instead of "protocol". We are talking about a group of function signatures.
We will create a new file called protocol_def.py.
First step is to define prototypes of remote functions you would like to call. We
will define one function with one argument of type compost_rpc.U8 and a
return value of type compost_rpc.U16. You always have to type annotate
all the arguments and return value with one of the supported types.
We put all the functions as methods in one class inherited from the
compost_rpc.Protocol class.
from compost_rpc import Protocol, rpc, U16, U8
# All RPC functions must be in one class inherited from compost_rpc.Protocol
class ExampleProtocol(Protocol):
# You must use the decorator to assign unique RPC_ID to each function
@rpc(0x00B)
def adc_read(self, channel: U8) -> U16:
"""Reads specified ADC channel"""
# Function body should be emptyTip
For a larger example check out the example protocol definition.
We call the Python file with function prototypes the protocol definition file.
From the protocol definition Compost can generate code for other languages.
We use the same protocol definition file as a script that will generate the code.
from compost_rpc import Protocol, Generator, rpc, U16, U8
class ExampleProtocol(Protocol):
@rpc(0x00B)
def adc_read(self, channel: U8) -> U16:
"""Reads specified ADC channel"""
if __name__ == "__main__":
with Generator(ExampleProtocol) as gen:
gen.c.generate() # Generate the C code files!Run the script to see what it will generate:
python protocol_def.pyThe script creates compost.c and compost.h
files with C code in your current directory.
You need to add all of these files to your C project.
For more information about code generation check out Generating code.
The main entry point to Compost in C language is the function
compost_msg_process(). You have to call this function for every received
Compost message and it will give you the response you have to send back.
Example of integration with blocking uart_read() and uart_write()
functions. In real application with real serial port, it's recommended to use
timeout for reading the rest of the frame after the first byte is received.
#include "mcu.h"
#include "uart.h"
#include "compost.h"
uint8_t tx_buf[1024];
uint8_t rx_buf[1024];
int main(void)
{
mcu_init();
for (;;) {
/* Receive the first byte of the header which is the msg_data length in 32b words */
uart_read(rx_buf, 1);
/* Receive the rest of the header and the msg_data */
if (uart_read(rx_buf + 1, 3 + 4 * rx_buf[0])) {
continue;
}
/* You pass the rx_buf with received Compost frame
and Compost will call the function and put the
Compost response with the return value to tx_buf. */
int16_t msg_size = compost_msg_process(tx_buf, sizeof(tx_buf), rx_buf, 4 + 4 * rx_buf[0]);
/* Send the response from tx_buf */
if (msg_size > 0) {
uart_write(tx_buf, msg_size);
} else if (msg_size == 0) {
// No response to send
} else {
// Error handling
}
}
}This example does not compile yet:
$ make
LD build/obj/compost_impl.o
build/obj/compost_rpc.o: In function `invoke_adc_read':
compost_rpc.c:245: undefined reference to `adc_read_handler'
collect2: error: ld returned 1 exit status
make: *** [Makefile:83: SPC582B60_Main] Error 1This is a feature of Compost - you have to implement all of the remote functions.
In C you define body of the function you would like to call remotely.
Compost generates the C function prototype for you from the protocol definition
file. Just include compost.h.
#include "compost.h"
uint16_t adc_read_handler(uint8_t channel)
{
// In real application we would read the adc value here.
// For this example we will just return a dummy value.
if (channel == 0)
return 1;
else
return 0;
}One way to think about this is that we have just written the body of the function that we defined in the Python protocol definition.
Your project should now compile successfully.
For calling our new RPC function we will create a new script:
from compost_rpc import SerialTransport
from protocol_def import ExampleProtocol
rpc = ExampleProtocol(SerialTransport(serial_port="COM16", baudrate=921600))
adc_value = rpc.adc_read(0)
print(adc_value)First you instantiate a transport - we use serial port as it is one of the simplest ones. Next you create a connection by instantiating the class in which you defined the remote function prototypes. Then call the Python function which you specified in the protocol definition file:
$ python client_script.py
1Thats it! We called a function in Python but it was executed on an MCU.
See other Python interface usage examples in example script.
Roughly, this is what happens when you call rpc.adc_read(0):
- Python takes the argument
0, serializes it into a Compost request message and sends it over serial port - Your firmware receives the message and calls
compost_msg_process(...)with pointers to RX buffer containing the received message and TX buffer - Compost parses the message and calls your function with the passed argument
adc_read_handler(0); - Your
adc_read_handlerfunction returns the value1 - Compost prepares the response message in the TX buffer with this return value
and
compost_msg_process()returns - Your firmware sends the response message prepared in the TX buffer by
compost_msg_process() - Python receives the response message, parses it and the function
rpc.adc_read(0)returns1