OBCP engine based on Micropython, developed as a part of "Model-Based Execution Platform for Space Applications" project (contract 4000146882/24/NL/KK) financed by the European Space Agency.
The On-Board Control Procedure Engine is based on Micropython (https://micropython.org/), and derived from the "embed" port to maximize compatibility with target platforms, such as x86-64 (for testing/demonstration) and ARM Cortex-M7 (e.g., Microchip SAMV71/SAMRH71). It is intended to be integrated within larger C-based Application Software. Interface requirements, for emitting events, as well as interacting with DataPool, Requests and Reports are driven by MBEP-N7S-EP-SRS document, while its high-level design is described in MBEP-N7S-EP-SDD.
The engine exposes a simple C interface (see src/obcp_engine/obcpengine.h) used to integrate the runtime with host application code (datapool, events, IO, time and packet functions are provided by the embedding application via the engine context).
The engine can work in a concurrent, and non-concurrent mode:
- in the non-concurrent mode, only a single OBCP can be executed at a time,
- in the concurrent mode, multiple OBCPs can be executed in parallel, each within its own task.
The concurrent mode modifies the Micropython internals by replacing mp_state_ctx global variable with a thread-local instance. This approach is ported from ESA's Micropython evolution for Leon processors, executed within the scope of ESA Contract No. 4000137198/22/NL/MGu/kk. A custom, non-OS dependent implementation of mp_state_ptr is provided instead of the one used in the original ESA contract. Thread-local handling is done by user-provided functions to maximize compatibility with various environments.
The changes are provided as stand-alone files that are patched into the mainline repository during the build process. This approach was taken to avoid forking the Micropython repository, and facilitating backporting into new Micropython versions (via generating patches and reapplying them to a new version). Fork is avoided to reduce future maintenance effort. Submission of the patches to the main repository is deemed infeasible, as:
- by replacing a single global variable with thread-local values accessed via function calls, performance may be impacted, which is undesired for many Micropython uses.
- the approach is derived from a previous ESA project in which the original Micropython maintainer was involved, and the modifications were not backported back then.
Execute 'make embed' to generate the custom embed port.
Execute 'make test' to execute automated tests.
Execute 'make patches' to generate the diffs between the mainline Micropython and the patched code, for ease of review of the changes and possible backporting.
The primary entry points are:
obcpengine_init(context)— initialize the engine with callback implementations provided inobcp_engine_context_t.obcpengine_execute_py(script, heap, heap_size)— execute a null-terminated Python source string using the supplied heap.obcpengine_execute_mpy(script, script_length, heap, heap_size)— execute precompiled MicroPython bytecode (.mpy) from a buffer.obcpengine_provide_buffer(buffer, length)— provide an I/O buffer that is required if OBCPs use obcppacket module.
Short usage examples (C):
Initialize and run a Python string:
#include "obcp_engine/obcpengine.h"
/* Provide implementations for required callbacks... */
static obcp_engine_context_t ctx = { /* fill callbacks */ };
int main(void) {
char heap[8 * 1024]; /* example heap */
static const char *script =
"import obcpio\n"
"obcpio.write('Hello, World!')\n";
if (!obcpengine_init(&ctx)) return -1;
if (!obcpengine_execute_py(script, heap, sizeof(heap))) return -1;
return 0;
}Run a precompiled .mpy buffer:
const uint8_t mpy_blob[] = { /* compiled .mpy bytes */ };
char heap[8 * 1024];
if (!obcpengine_execute_mpy(mpy_blob, sizeof(mpy_blob), heap, sizeof(heap))) {
/* handle error */
}Provide a shared buffer for I/O:
uint8_t shared_buf[1024];
obcpengine_provide_buffer(shared_buf, sizeof(shared_buf));For full callback types and required behaviour see src/obcp_engine/obcpengine.h.
OBCP_ENABLE_CONCURRENT_OBCPS define shall be provided is the engine is to be used in the concurrent mode.
For authoritative example please refer to test/arm-freertos.
The below modules and functions are available to Micropython OBCPs. If any is used, the relevant user-side handling function shall be provided as a callback registered in context submited to 'obcpengine_init'. Please refer to 'src/obcp_interface/' for signatures, and 'test/mocks/.c' for examples.
obcpio
- write(text): Write a text string to the runtime output. Raises
RuntimeError if the embedding runtime did not provide
obcp_writeor if the write fails.
obcpdatapool
- readintparameter(id) -> int: Read an integer parameter identified by
id. Intended to be used also for enumerations. Raises RuntimeError if datapool callbacks are not provided or the access fails. - writeintparameter(id, value): Write an integer parameter.
- readfloatparameter(id) -> float: Read a floating-point parameter.
- writefloatparameter(id, value): Write a floating-point parameter.
- readboolparameter(id) -> bool: Read a boolean parameter.
- writeboolparameter(id, value): Write a boolean parameter.
obcpevents
- sendevent(event_id): Emit an event with the given integer ID.
obcpcontrol
- beginstep(id): Begin a control step with the given ID. Returns normally or raises RuntimeError on failure.
- endstep(id, success): End a control step and indicate
successas a boolean.
obcptime
- wait(milliseconds): Block (via the embedding runtime) for the specified number of milliseconds.
- waituntil(seconds, milliseconds): Block until an absolute time given
by
secondsandmilliseconds. - gettime() -> (seconds, milliseconds): Return current on-board time as a two-element tuple of unsigned integers.
obcppackets
- ispacketavailable(channel) -> bool: Check if a packet is available on
the given
channel. - getchannelwithpacketavailable() -> int|None: Return the first channel
with an available packet, or
Noneif none is available. - cansendpacket(channel) -> bool: Check whether a packet can be sent on
the given
channel. - sendpacket(channel, data): Send a packet on
channel.datais a bytes-like object; raises RuntimeError if sending fails or callback is not provided. - receivepacket(channel, timeout_ms) -> bytes|None: Receive a packet
into the engine-provided buffer and return it as
bytes, orNoneif no packet was received beforetimeout_mselapsed. A buffer must be provided to the engine usingobcpengine_provide_bufferprior to calling this function when using packet I/O.
Notes
- All functions raise
RuntimeErrorinside MicroPython if the corresponding runtime callback was not supplied via theobcp_engine_context_tor if the runtime reports failure. - See
src/obcpmods/*/mod*.cfor implementation details andsrc/obcp_engine/obcpengine.hfor the engine context and callback signatures.