A NASA Core Flight System (cFS) based mission software stack integrating a Lua-driven sequence engine and a telemetry/attitude management application.
This project builds on NASA's open-source cFS framework and adds two custom flight software applications:
| App | Repo | Description |
|---|---|---|
lseq |
paulmccumber/lseq |
Lua-based sequence engine — scheduled by sch_lab |
tam |
paulmccumber/tam |
Simulated Magnetometer — runs autonomously at 1 Hz, generates HK and sensor traffic |
- Linux build host (native simulation target)
- Git with SSH access to GitHub
- Standard cFS build dependencies:
cmake,gcc/g++,make
git clone git@github.com:nasa/cFS.git
cd cFSOptional: Pin to a known-good commit before proceeding:
git checkout 83c735e469e96537b289a50574781abe113c1230 # This README was written building from this hash git checkout 9c786d2536821aae608560e0d75835e3637b499d # This app was developed on this hash
git submodule init
git submodule updateOnly required once per workspace.
cp cfe/cmake/Makefile.sample Makefile
cp -r cfe/cmake/sample_defs/ sample_defsmake SIMULATION=native prep
makecd apps/
git clone git@github.com:paulmccumber/lseq.git
git clone git@github.com:paulmccumber/tam.git
cd ..Edit sample_defs/targets.cmake and add lseq and tam to the global app list:
list(APPEND MISSION_GLOBAL_APPLIST lseq tam)Edit sample_defs/cpu1_cfe_es_startup.scr to load both apps at boot. Add the following entries (and remove sample_app / sample_lib if not needed):
CFE_APP, lseq, LSEQ_Main, LSEQ_PP, 51, 16384, 0x0, 0;
CFE_APP, tam_app, TAM_AppMain, TAM_APP, 55, 16384, 0x0, 0;
Edit apps/sch_lab/fsw/tables/sch_lab_table.c to add a scheduler slot for lseq.
#include "default_lseq_app_msgids.h"
/* Schedule our lseq hk output */
{CFE_SB_MSGID_WRAP_VALUE(LSEQ_SEND_HK_MID), 100, 0},
Note:
tamdoes not require a scheduler entry — it runs its own 1000 ms sleep loop internally.
Edit:apps/sch_lab/CMakeLists.txt
include_directories(${lseq_MISSION_DIR}/config)
Edit:apps/to_lab/fsw/tables/to_lab_sub.c
{CFE_SB_MSGID_WRAP_VALUE(0x0892), {0, 0}, 4},
{CFE_SB_MSGID_WRAP_VALUE(0x0902), {0, 0}, 4},
{CFE_SB_MSGID_WRAP_VALUE(0x0903), {0,0},4},
make SIMULATION=native prep
make
make installAfter each build, copy the lseq Lua sequences and JSON configuration files into the CPU1 load directory:
cd apps/lseq/fsw/tables/
cp *.lua ../../../../build/exe/cpu1/cf/
cp *.json ../../../../build/exe/cpu1/cf/
cd ../../../..This step is required after every rebuild — the
cf/folder is not populated automatically.
cd build/exe/cpu1
sudo ./core-cpu1The repo is configured for "Debug builds" so that the emmy debugger can function. Read and understand the following. Support is in place such that "Flight builds" will be sandboxed.
lseq embeds a Lua interpreter in each child task. By default the full Lua
standard library (luaL_openlibs) is not loaded. Instead, OpenSafeLibs
opens only the libraries a sequence script actually needs:
| Library | Reason included |
|---|---|
base |
Core language (print, pcall, tostring, etc.) |
math |
Numeric utilities |
string |
String formatting |
table |
Table manipulation |
coroutine |
Co-operative multitasking within a script |
The following libraries are excluded in flight builds:
| Library | Reason excluded |
|---|---|
package |
require() and loadlib() can load arbitrary native .so files at runtime |
io |
Direct file I/O bypasses cFS abstractions |
os |
os.execute() can spawn shell commands; os.exit() can kill the process |
debug |
Exposes Lua VM internals; not needed in sequence scripts |
A sequence script whose job is to send and receive cFS Software Bus messages
has no legitimate need for any of these. Excluding them means a buggy or
malicious script cannot load native code, execute shell commands, or
manipulate the VM state. This follows the same principle as Lua's _ENV
sandboxing mechanism — the language was designed with restricted environments
as a first-class feature.
The EmmyLua VS Code debugger attaches via require('emmy_core'), which
requires the package library. A LSEQ_DEBUG_BUILD CMake option re-enables
package and io for development use.
To build with the debugger enabled (development only):
In CMakeLists.txt, set the option default to ON:
option(LSEQ_DEBUG_BUILD "Enable debug Lua libs (package, io) for EmmyLua" ON)Then rebuild:
rm build/native/default_cpu1/CMakeCache.txt build/CMakeCache.txt
make SIMULATION=native prep
makeThe lseq_init.lua script guards the EmmyLua block so it is silently skipped
when package is not available:
if package then
package.cpath = package.cpath .. ";cf/?.so"
local ok, dbg = pcall(require, 'emmy_core')
if ok then
dbg.tcpListen('127.0.0.1', 9966)
dbg.waitIDE()
else
cfs.event(100, 4, "emmy_core load failed: " .. tostring(dbg))
end
endNo script changes are required when switching between build modes.
To build for flight, set the option default to OFF:
option(LSEQ_DEBUG_BUILD "Enable debug Lua libs (package, io) for EmmyLua" OFF)Then do a clean rebuild. Since you are deploying to a target rather than
running locally, distclean is safe here:
make distclean
make SIMULATION=native prep
makeIn the flight build:
packageandioare absent from every Lua state- The
if package thenguard inlseq_init.luaskips the EmmyLua block with no error - Scripts have access only to the safe library set listed above
- Scheduled via
sch_lab - Sequences are authored in Lua and executed by the embedded interpreter
- Runs autonomously; no scheduler slot required
- Sleeps 1000 ms per cycle
- Publishes housekeeping (HK) and simulated sensor telemetry
lseq hosts up to 4 independent Lua threads (slots), each running its own Lua state. Every slot is independently started, stopped, and monitored. Sequences interact with the cFS Software Bus through a dedicated cfs.* Lua extension library.
The LuaConfig cFS table configures all slots. Each slot (LSEQ_ThreadCfg_t) has the following fields:
| Field | Type | Description |
|---|---|---|
msg_json |
char[128] |
Path to the CCDD JSON message registry for this slot |
init_script |
char[128] |
Path to the Lua script run once at thread startup |
tick_script |
char[128] |
Path to the Lua script that defines the tick() function |
tick_period_ms |
uint32 |
Heartbeat interval in milliseconds (0 = compiled default) |
enabled |
uint32 |
Non-zero to auto-start this slot at app initialization |
The table is loaded from lseq_lua_cfg.c and can be updated at runtime via the standard cFS table services mechanism.
Each slot expects two scripts:
init_script — Evaluated once when the thread starts. Use this to load messages, register subscriptions, set up on-receive callbacks, and define the on_error() handler.
tick_script — Evaluated once at startup to define the global tick() function. LSEQ calls tick() on every heartbeat at the configured tick_period_ms rate.
-- init_script: subscribe and register callbacks
cfs.loadmsgs("/cf/table.json")
cfs.subscribe("MY_Telemetry")
cfs.on("MY_Telemetry", function(fields)
print(fields.some_field)
end)
function on_error(msg)
cfs.event(199, 4, "lseq error: " .. tostring(msg))
end
-- tick_script: define the periodic function
function tick()
local ok, val = pcall(cfs.field, "MY_Telemetry", "some_field")
if ok then
-- act on val
end
endAll commands are sent to LSEQ_CMD_MID. Housekeeping is requested via LSEQ_SEND_HK_MID and published on LSEQ_HK_TLM_MID.
| Command | CC | Payload | Description |
|---|---|---|---|
| Noop | 0 |
— | No-op; increments command counter |
| Reset Counters | 1 |
— | Clears CommandCounter and CommandErrorCounter |
| Start Sequence | 2 |
slot (u8), config_index (u8) |
Snapshots config from the given table row and spawns a Lua child task in slot |
| Stop Sequence | 3 |
slot (u8) |
Signals the running child task in slot to exit |
slot is 0-based and must be less than LSEQ_MAX_THREADS (4). Starting a slot that is already running is an error. Stopping a slot that is not running is a no-op.
The HK packet payload (LSEQ_HkTlm_Payload_t) contains:
| Field | Type | Description |
|---|---|---|
CommandCounter |
uint8 |
Count of successfully accepted commands |
CommandErrorCounter |
uint8 |
Count of rejected or errored commands |
seq[N].running |
uint8 |
Non-zero if slot N is active |
seq[N].config_index |
uint8 |
Table row used to start slot N |
seq[N].tick_count |
uint32 |
Number of times tick() has been called in slot N |
seq[N].err_count |
uint32 |
Number of Lua errors caught in slot N |
N ranges from 0 to LSEQ_MAX_THREADS - 1 (3).
Lua errors in both init and tick execution are caught with lua_pcall. On any error:
err_countfor the affected slot is incremented (visible in HK telemetry).- If the script defines a global function named
on_error(msg), it is called with the error message string. - A cFS EVS error event is emitted if
on_error()itself fails. - The thread continues running — a single Lua error does not kill the slot.
The tick_count and err_count fields in HK provide a continuous health signal: a slot that has stopped incrementing tick_count while running remains set indicates a stall.
-- Recommended error handler in init_script
function on_error(msg)
cfs.event(199, 4, "lseq error: " .. tostring(msg))
endAll extensions are available in every slot under the cfs table.
| Function | Signature | Returns | Description |
|---|---|---|---|
loadmsgs |
cfs.loadmsgs(path) |
count (int) |
Parses a CCDD JSON file and registers all messages in the global registry |
subscribe |
cfs.subscribe(mnemonic) |
mid_value (int) |
Subscribes the slot's SB pipe to the named message's MID |
on |
cfs.on(mnemonic, fn) |
— | Registers fn(fields) as the callback invoked whenever mnemonic is received; fields is a table of decoded payload values keyed by field name |
field |
cfs.field(mnemonic, field_name) |
value | Returns the most recently cached value of a named field from the last received packet; raises a Lua error if no sample has arrived |
send |
cfs.send(mnemonic, fc [, field, val, ...]) |
fc (int) |
Builds and transmits a CCSDS packet; optional field/value pairs (must be even count) are written into the payload before transmission |
time |
cfs.time(mnemonic) |
seconds, subseconds |
Returns the cFS timestamp of the last received packet for mnemonic; useful for staleness detection |
now |
cfs.now() |
seconds, subseconds |
Returns the current cFS mission elapsed time from CFE_TIME_GetTime() |
event |
cfs.event(event_id, event_type, message) |
— | Sends a cFS EVS event; prefer this over print() for flight-visible notifications |
EVS event type constants (for use with cfs.event):
| Constant | Value |
|---|---|
EVS_DEBUG |
1 |
EVS_INFO |
2 |
EVS_WARN |
3 |
EVS_ERROR |
4 |
lseq supports interactive Lua debugging via the EmmyLuaDebugger shared library and the EmmyLua VS Code extension.
Clone and build emmy_core.so against Lua 5.4:
rm -rf EmmyLuaDebugger/
git clone --recursive https://github.com/EmmyLua/EmmyLuaDebugger.git
cd EmmyLuaDebugger
mkdir build && cd build
cmake .. -DEMMY_LUA_VERSION=54 -DCMAKE_BUILD_TYPE=Release
cmake --build . --config ReleaseKnown build fix: If the build fails with a missing
CHAR_BITor similar<climits>error, add the following include toemmy_debugger/src/api/lua_api_loader.cppbefore rebuilding:#include <climits>
Once built, copy emmy_core.so into the CPU1 load directory alongside your Lua scripts:
cp emmy_core.so /path/to/cFS/build/exe/cpu1/cf/Place a .vscode/ folder inside build/exe/cpu1/ with the following launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "emmylua_new",
"request": "launch",
"name": "Attach to lseq",
"host": "localhost",
"port": 9966,
"ext": [".lua"],
"sourcePaths": ["${workspaceFolder}"],
"ideConnectDebugger": true
}
]
}Add the following block to the top of the slot's init_script to make the sequencer wait for VS Code to attach before executing:
if package then
package.cpath = package.cpath .. ";cf/?.so"
local ok, result = pcall(require, 'emmy_core')
if ok then
result.tcpListen('127.0.0.1', 9966)
result.waitIDE()
else
cfs.event(100, 4, "emmy_core load failed: " .. tostring(result))
end
endImportant: With this block in place, the slot will block at
waitIDE()until the VS Code debugger connects. Make sure to launch the "Attach to lseq" debug configuration in VS Code before or immediately after starting the sequence, otherwise the slot will hang waiting for the connection.
Remove or comment out the block for normal (non-debug) operation.
Two bash scripts are provided in tools/cFS-GroundSystem/Subsystems/cmdUtil/ to start and stop sequence slots from the command line.
start_seq.sh — Start a slot using a given config table index:
#!/bin/bash
SLOT=${1}
CONFIG_INDEX=${2}
HALF=$(printf "0x%02X%02X" "$SLOT" "$CONFIG_INDEX")
./cmdUtil --host 127.0.0.1 --port 1234 \
--pktid 0x1884 --cmdcode 2 \
--half "$HALF" \
--half 0x0000Usage: ./start_seq.sh <slot> <config_index> — e.g. ./start_seq.sh 0 0
stop_seq.sh — Stop a running slot:
#!/bin/bash
SLOT=${1}
./cmdUtil --host 127.0.0.1 --port 1234 \
--pktid 0x1884 --cmdcode 3 \
--half $(printf "0x%02X00" "$SLOT") \
--half 0x0000Usage: ./stop_seq.sh <slot> — e.g. ./stop_seq.sh 0
Both scripts target 127.0.0.1:1234 (the default cFS-GroundSystem cmdUtil port) and use MID 0x1884 (LSEQ_CMD_MID).
To monitor lseq housekeeping telemetry in the cFS Ground System, add the following two files.
tools/cFS-GroundSystem/Subsystems/tlmGUI/lseq-hk-tlm.txt:
CommandErrorCounter, 12, 1, B, Dec, NULL, NULL, NULL, NULL
CommandCounter, 13, 1, B, Dec, NULL, NULL, NULL, NULL
seq0_running, 16, 1, B, Dec, NULL, NULL, NULL, NULL
seq0_config_index, 17, 1, B, Dec, NULL, NULL, NULL, NULL
seq0_tick_count, 20, 4, I, Dec, NULL, NULL, NULL, NULL
seq0_err_count, 24, 4, I, Dec, NULL, NULL, NULL, NULL
seq1_running, 28, 1, B, Dec, NULL, NULL, NULL, NULL
seq1_config_index, 29, 1, B, Dec, NULL, NULL, NULL, NULL
seq1_tick_count, 32, 4, I, Dec, NULL, NULL, NULL, NULL
seq1_err_count, 36, 4, I, Dec, NULL, NULL, NULL, NULL
seq2_running, 40, 1, B, Dec, NULL, NULL, NULL, NULL
seq2_config_index, 41, 1, B, Dec, NULL, NULL, NULL, NULL
seq2_tick_count, 44, 4, I, Dec, NULL, NULL, NULL, NULL
seq2_err_count, 48, 4, I, Dec, NULL, NULL, NULL, NULL
seq3_running, 52, 1, B, Dec, NULL, NULL, NULL, NULL
seq3_config_index, 53, 1, B, Dec, NULL, NULL, NULL, NULL
seq3_tick_count, 56, 4, I, Dec, NULL, NULL, NULL, NULL
seq3_err_count, 60, 4, I, Dec, NULL, NULL, NULL, NULL
Add to tools/cFS-GroundSystem/Subsystems/tlmGUI/telemetry-pages.txt:
LSEQ HK, GenericTelemetry.py, 0x892, lseq-hk-tlm.txt
This registers the LSEQ HK telemetry page (MID 0x892) in the Ground System telemetry browser, displaying all four slot status fields — running, config_index, tick_count, and err_count — in real time.
cFS/
├── apps/
│ ├── lseq/ # Lua sequence engine
│ ├── tam/ # Telemetry & attitude manager
│ └── sch_lab/
│ └── fsw/tables/
│ └── sch_lab_table.c # <-- Add lseq schedule slot here
├── sample_defs/
│ ├── targets.cmake # <-- Add lseq, tam to MISSION_GLOBAL_APPLIST
│ └── cpu1_cfe_es_startup.scr # <-- Add app startup entries here
├── Makefile
└── ...
The cFS framework is released under NASA's Open Source Agreement (NOSA) 1.3.
lseq and tam are proprietary — see each repository for license terms.