main.c
The purpose of this tutorial is to show new users how to use LibJuno as a embedded software micro-framework. LibJuno is intended to provide developers and software architects the freedom to implement their software system according to their use case, instead of users conforming to our library.
This means users will need to define their own entry point. On systems with an OS, this would be a main function.
Additionally, users will need to implement project specific functions like logging and time management since LibJuno doesn't assume your OS or time use-case.
Logging
In this case, we are going to implement the logging functions using printf. A fixed size buffer is utilized with vsnprintf to ensure the logger doesn't see any memory overruns.
{
(void) ptJunoLog;
va_list tVarg;
va_start(tVarg, pcMsg);
char pcPrintStr[512] = {0};
vsnprintf(pcPrintStr, sizeof(pcPrintStr), pcMsg, tVarg);
printf("DEBUG: %s\n", pcPrintStr);
va_end(tVarg);
}
{
(void) ptJunoLog;
va_list tVarg;
va_start(tVarg, pcMsg);
char pcPrintStr[512] = {0};
vsnprintf(pcPrintStr, sizeof(pcPrintStr), pcMsg, tVarg);
printf("INFO: %s\n", pcPrintStr);
va_end(tVarg);
}
{
(void) ptJunoLog;
va_list tVarg;
va_start(tVarg, pcMsg);
char pcPrintStr[512] = {0};
vsnprintf(pcPrintStr, sizeof(pcPrintStr), pcMsg, tVarg);
printf("WARN: %s\n", pcPrintStr);
va_end(tVarg);
}
{
(void) ptJunoLog;
va_list tVarg;
va_start(tVarg, pcMsg);
char pcPrintStr[512] = {0};
vsnprintf(pcPrintStr, sizeof(pcPrintStr), pcMsg, tVarg);
printf("ERROR: %s\n", pcPrintStr);
va_end(tVarg);
}
#define JUNO_STATUS_SUCCESS
Operation completed successfully.
Definition status.h:56
int32_t JUNO_STATUS_T
Canonical status type for LibJuno functions.
Definition status.h:49
struct JUNO_LOG_ROOT_TAG JUNO_LOG_ROOT_T
Definition log_api.h:49
static JUNO_STATUS_T LogWarning(const JUNO_LOG_ROOT_T *ptJunoLog, const char *pcMsg,...)
Definition main.c:71
static JUNO_STATUS_T LogError(const JUNO_LOG_ROOT_T *ptJunoLog, const char *pcMsg,...)
Definition main.c:82
static JUNO_STATUS_T LogDebug(const JUNO_LOG_ROOT_T *ptJunoLog, const char *pcMsg,...)
Definition main.c:49
static JUNO_STATUS_T LogInfo(const JUNO_LOG_ROOT_T *ptJunoLog, const char *pcMsg,...)
Definition main.c:60
Time
In this example project we will need a timestamp, so we'll implement the Now function. We don't need Sleep or SleepTo so we'll provide mocks to those functions. LibJuno provides time math functions that we can utilize so we only need to provide implementations to these three time functions.
{
JUNO_TIMESTAMP_RESULT_T tResult = {0};
if(!ptTime)
{
return tResult;
}
struct timespec tTime = {0};
clock_gettime(CLOCK_MONOTONIC, &tTime);
tResult = ptTime->ptApi->NanosToTimestamp(ptTime, tTime.tv_nsec);
tResult.tOk.iSeconds = tTime.tv_sec;
return tResult;
}
{
(void) ptTime;
(void) tTimeToWakeup;
}
{
(void) ptTime;
(void) tDuration;
}
#define JUNO_ASSERT_SUCCESS(tStatus,...)
Execute the provided failure operation(s) if status is not success.
Definition macros.h:87
#define JUNO_STATUS_NULLPTR_ERROR
A required pointer argument was NULL or invalid.
Definition status.h:60
struct JUNO_TIME_ROOT_TAG JUNO_TIME_ROOT_T
Definition time_api.h:69
static JUNO_STATUS_T SleepTo(const JUNO_TIME_ROOT_T *ptTime, JUNO_TIMESTAMP_T tTimeToWakeup)
Sleep this thread until a specific time.
Definition main.c:118
static JUNO_STATUS_T Sleep(const JUNO_TIME_ROOT_T *ptTime, JUNO_TIMESTAMP_T tDuration)
Sleep this thread for a duration.
Definition main.c:125
static JUNO_TIMESTAMP_RESULT_T Now(const JUNO_TIME_ROOT_T *ptTime)
Get the current time as specified by the implementation.
Definition main.c:102
Timestamp consisting of whole seconds and fixed-point subseconds.
Definition time_api.h:105
Instantiating APIs
LibJuno utilizes a vtable, or a table of function pointers to the specific implementation, that is passed to the LibJuno module. This vtable is called an "API" in LibJuno nomenclature and it provides a standard interface to the capabilities. LibJuno is implemented as a set of capabilities, or software components that perform a set functionality. These capabilities are called "Modules" and can be extened through derivation. Additionally APIs can be extended through derivation. When a module or API is extended its called "derived".
Below is the API instantiation of the logger and time APIs for this project. We are providing our logging implementations and time implementations. You'll notice that the time API provides a helper macro to instantiate the API. Some APIs offer existing implementations, so a helper macro is used to inform users which functions they need to implement.
};
#define JunoTime_TimeApiInit(Now, SleepTo, Sleep)
Initialize a JUNO_TIME_API_T literal with default math/conversion impls.
Definition time_api.h:197
static const JUNO_TIME_API_T gtTimeApi
Definition main.c:153
static const JUNO_LOG_API_T gtMyLoggerApi
Definition main.c:146
Vtable for time operations and conversions.
Definition time_api.h:128
Failure Handler
LibJuno utilizes a failure handler callback. This is a function that is automatically called by downstream code when a failure occurs. In an embedded system, you might not have a terminal or console log running. This enables developers to have a single implementation and methodology for handling failure within the software. The JUNO_FAIL... macros will automatically call the failure handler if one is provided.
{
(void) pvUserData;
printf("FAILURE: %u | %s\n", tStatus, pcMsg);
}
void JUNO_USER_DATA_T
Opaque user data type for failure callbacks.
Definition status.h:96
void FailureHandler(JUNO_STATUS_T tStatus, const char *pcMsg, JUNO_USER_DATA_T *pvUserData)
Definition main.c:163
The Entry Point
This example project assumes its running on a POSIX system. Therefore, we can use a main method as the entry point. Many microcontrollers have different architectures for their entry point so you'll need to consult with your architecture documentation on how to implement the entry point.
{
int main(void)
Definition main.c:176
Dependency Injection in Action
The core principle of dependency injection is "inversion of control". This is a fancy term for saying that modules don't allocate resources, up stream users do. This is done at the "composition root", or the spot in the code where all the dependencies get implemented. In this case, the main function is the composition root. Here we need to instantiate the time, logger, registry modules.
Applications in LibJuno are actually derived modules of JUNO_APP_ROOT_T. Here we will need to instantiate the engine, and system manager application modules.
struct JUNO_SB_BROKER_ROOT_TAG JUNO_SB_BROKER_ROOT_T
Definition broker_api.h:65
struct JUNO_SB_PIPE_TAG JUNO_SB_PIPE_T
Definition broker_api.h:67
struct ENGINE_APP_TAG ENGINE_APP_T
Definition engine_app.h:80
struct SYSTEM_MANAGER_APP_TAG SYSTEM_MANAGER_APP_T
Definition system_manager_app.h:43
Module Initialization
Modules typically have an Init function that specifies the required fields to initialize a module. Modules also utilize a Verify function to verify they've been initialzed. Most modules will return a JUNO_STATUS_NULLPTR_ERROR if they have not been initalized and a function has been called on them.
static JUNO_STATUS_T JunoTime_TimeInit(JUNO_TIME_ROOT_T *ptTime, const JUNO_TIME_API_T *ptApi, JUNO_FAILURE_HANDLER_T pfcnFailureHandler, JUNO_USER_DATA_T *pvUserData)
Initialize the time module root with API and failure handler.
Definition time_api.h:233
static JUNO_STATUS_T JunoLog_LogInit(JUNO_LOG_ROOT_T *ptLog, const JUNO_LOG_API_T *ptApi, JUNO_FAILURE_HANDLER_T pfcnFailureHandler, JUNO_USER_DATA_T *pvUserData)
Initialize a log instance with an API implementation and failure handler.
Definition log_api.h:66
Notice how the broker, engine, and system manager take initialized modules as dependencies to their initialization. This is DI in action. For example, the engine app is provided with a logger, time, and broker instead of instantiating it itself.
JUNO_STATUS_T JunoSb_BrokerInit(JUNO_SB_BROKER_ROOT_T *ptBroker, JUNO_SB_PIPE_T **ptPipeRegistry, size_t iRegistryCapacity, JUNO_FAILURE_HANDLER_T pfcnFailureHdlr, JUNO_USER_DATA_T *pvFailureUserData)
Initialize a broker with a pipe registry storage array.
Definition juno_broker.c:35
JUNO_STATUS_T EngineApp_Init(ENGINE_APP_T *ptEngineApp, const JUNO_LOG_ROOT_T *ptLogger, const JUNO_TIME_ROOT_T *ptTime, JUNO_SB_BROKER_ROOT_T *ptBroker, JUNO_FAILURE_HANDLER_T pfcnFailureHandler, JUNO_USER_DATA_T *pvUserData)
Definition engine_app.c:106
JUNO_STATUS_T SystemManagerApp(SYSTEM_MANAGER_APP_T *ptSystemManagerApp, JUNO_LOG_ROOT_T *ptLogger, JUNO_TIME_ROOT_T *ptTime, JUNO_SB_BROKER_ROOT_T *ptBroker, JUNO_FAILURE_HANDLER_T pfcnFailureHandler, JUNO_USER_DATA_T *pvUserData)
Definition system_manager_app.c:70
Runtime
For this example we are going to create a very simple "schedule table" or table of applications that will run in a specific order. This schedule table will run at best-effort. This means that when one application is done, the next will start.
&tSystemManagerApp.tRoot,
&tEngineApp.tRoot
};
struct JUNO_APP_ROOT_TAG JUNO_APP_ROOT_T
The application module.
Definition app_api.h:50
Since all applications have the same interface, we can run their init function in a for-loop and run the application OnProcess function in a while(true) loop.
for(size_t i = 0; i < 2; i++)
{
tStatus = ptAppList[i]->ptApi->OnStart(ptAppList[i]);
}
size_t iCounter = 0;
while(true)
{
ptAppList[iCounter]->ptApi->OnProcess(ptAppList[iCounter]);
iCounter = (iCounter + 1) % 2;
}
}
engine_app.h
The Engine Application
Next we will be going over a typical LibJuno application. For this tutorial project we are defining an engine application that manages the engine hardware for our car. In this example the engine needs to receive a command for the RPM and telemeter the actual RPM of the engine.
Deriving an Application
Applications in LibJuno are derived from the application root. This provides a common API for applications to utilize. A standard application has three lifecycle functions:
- OnStart: The first function called in the application. It is called only once at the beginning of the application lifecycle. This is where developers would initialize resources that will be utilized by the application
- OnProcess: This is the "main run-loop" function of the application. This function is called by the main run-time in a loop and is the primary run function of the app.
- OnExit: This is the last function called by the application. This is where developers would clean up resources allocated by the application. A typical runtime would call application exit functions when the software is closing.
The Application Structure
Within the derived application users are expected to place pointers to dependencies they require from the main runtime and hold resources the application needs to own and pass from one lifecycle event to the next. In this case the engine app needs the following from the runtime:
And the application needs to allocate:
- A command buffer for engine commands. This is a memory allocation for commands that will be received by the application
- A command pipe. This is the module that manages the command buffer
- The current RPM. This is the current state of our engine.
#define ENGINE_CMD_MSG_PIPE_DEPTH (1)
ENGINE_CMD_MSG_ARRAY_T tCmdArray;
float fCurrentRpm;
);
#define ENGINE_CMD_MSG_PIPE_DEPTH
Definition engine_app.h:81
#define JUNO_MODULE_DERIVE(ROOT_T,...)
Implement a derived module embedding the root as the first member.
Definition module.h:157
Definition engine_cmd_msg.h:53
The App Init Function
The application also needs to provide a concrete application initialization function. This function sets dependencies and the API pointers within the application. The application has an internal "Verify" function that checks if any of these dependencies are null.
void(* JUNO_FAILURE_HANDLER_T)(JUNO_STATUS_T tStatus, const char *pcCustomMessage, JUNO_USER_DATA_T *pvUserData)
Failure handler callback signature.
Definition status.h:104
engine_cmd_msg.h
This file is auto-generated from a LibJuno script (scripts/create_msg.py). Similarly the engine_tlm_msg.h and engine_tlm_msg.c files are also auto-generated. We will only go over the command file since it's identical to the telemetry file in terms of architecture.
Engine Application Commands
In LibJuno the software bus is operated on a single thread. A single broker is implemented for each thread of execution and the broker distributes messages to various queues that are subscribed on the software bus. These queues are called pipes. Pipes are derived LibJuno queues with a message ID subscription.
The message ID is a unique identifier for the type of message. Each message type will have its own message ID. In this example the engine command MID is 0x1800. This number can be arbitrary. The only requirement is the MIDs are unique for every message type.
#define ENGINE_CMD_MSG_MID (0x1800)
The command message contains the RPM. Another application can send this command on the software bus and tell the engine what RPM it should be set to. The engine app will then control the engine so it's set to the new RPM.
{
struct ENGINE_CMD_MSG_TAG ENGINE_CMD_MSG_T
float fRpm
Definition engine_cmd_msg.h:54
Engine Command Pipe
Below is the definition for the engine command pipe. This is derived from the JUNO_SB_PIPE_T using the DERIVE_WITH_API macro. This macro enables users to specify which API they are using. The inheritance for a pipe is as follows:
JUNO_DS_ARRAY_ROOT_T -> JUNO_DS_QUEUE_ROOT_T -> JUNO_SB_PIPE_T -> Custom Pipe Type
Because this is a longer chain of inheritance, it's convenient to specify the API for ease-of-use. In this case we are specifying the QUEUE api.
The pipe holds a pointer to a command buffer, which is specific to the command message type defined earlier. This enables the derived pipe to have specific type information (vs using a void pointer in the pipe).
) ENGINE_CMD_MSG_ARRAY_T;
struct JUNO_DS_ARRAY_ROOT_TAG JUNO_DS_ARRAY_ROOT_T
Opaque array root carrying capacity and API pointer.
Definition array_api.h:58
The queue api relies on LibJuno pointers. This enables the modules to write generic code safely without specific type information. As a result, we need to specify a pointer API implementation for this command type.
const JUNO_POINTER_API_T gtEngineCmdMsgPointerApi
Definition engine_cmd_msg.c:50
Pointer operations API (copy/reset).
Definition pointer_api.h:74
We also define convenience macros for initializing and verifying this type of pointer. This makes working with LibJuno pointers easy.
#define EngineCmdMsg_PointerInit(addr) JunoMemory_PointerInit(>EngineCmdMsgPointerApi, ENGINE_CMD_MSG_T, addr)
#define EngineCmdMsg_PointerVerify(tPointer) JunoMemory_PointerVerifyType(tPointer, ENGINE_CMD_MSG_T, gtEngineCmdMsgPointerApi)
Finally we define a Pipe Init function for this pipe type. This function will initialize the pipe with the message buffer and capacity.
JUNO_STATUS_T EngineCmdMsg_ArrayInit(ENGINE_CMD_MSG_ARRAY_T *ptEngineCmdMsgPipe, ENGINE_CMD_MSG_T *ptArrEngineCmdMsgBuffer, size_t iCapacity, JUNO_FAILURE_HANDLER_T pfcnFailureHdlr, JUNO_USER_DATA_T *pvUserData)
Definition engine_cmd_msg.c:111
engine_cmd_msg.c
Engine Command Pipe Implementation
In .c source file we will need to implement the following functions for the pointer and queue api:
static JUNO_STATUS_T SetAt(JUNO_DS_ARRAY_ROOT_T *ptArray, JUNO_POINTER_T tItem, size_t iIndex)
Set the value at an index.
Definition engine_cmd_msg.c:131
static JUNO_STATUS_T RemoveAt(JUNO_DS_ARRAY_ROOT_T *ptArray, size_t iIndex)
Remove a value at an index.
Definition engine_cmd_msg.c:176
static JUNO_RESULT_POINTER_T GetAt(JUNO_DS_ARRAY_ROOT_T *ptArray, size_t iIndex)
Get the value at an index.
Definition engine_cmd_msg.c:153
static JUNO_STATUS_T EngineCmdMsg_Reset(JUNO_POINTER_T tPointer)
Definition engine_cmd_msg.c:88
static JUNO_STATUS_T EngineCmdMsg_Copy(JUNO_POINTER_T tDest, const JUNO_POINTER_T tSrc)
Definition engine_cmd_msg.c:69
struct JUNO_POINTER_TAG JUNO_POINTER_T
Definition pointer_api.h:49
These function will provide an interface to our specific message type and enable users to write type-safe code within LibJuno.
We need to forward-declare these API functions so we can use the API pointer to verify the type of the queue and pointer.
Below we will instantiate the pointer and pipe API tables.
{
};
{
};
static const JUNO_DS_ARRAY_API_T gtEngineCmdMsgPipeApi
Definition engine_cmd_msg.c:57
Array API vtable.
Definition array_api.h:75
Pointer Copy
The pointer copy function is responsible for copy memory from one pointer of the same type to another. We verify the pointers are implemented and are of the same type by checking the alignment, size, and api pointer. We then dereference the pointer and copy the values since we have verified the type
{
return tStatus;
}
#define EngineCmdMsg_PointerVerify(tPointer)
Definition engine_cmd_msg.h:86
Pointer Reset
The reset function will reinitialize the memory of a pointer of this message type. In this case, it means setting the memory to 0. Similar to the copy function, we verify the pointer type and api before dereferencing the pointer.
Pipe Api Assert
We also define a macro to easily assert that the pipe type matches our implementation. This is done by checking the pipe api pointer.
#define ENGINE_CMD_MSG_PIPE_ASSERT_API(ptArray, ...) if(ptArray->ptApi != >EngineCmdMsgPipeApi) { __VA_ARGS__; }
Pipe Init
We also implement the pipe init function, which sets the API pointer as well as the message buffer and capacity.
{
ptEngineCmdMsgPipe->ptArrEngineCmdMsgBuffer = ptArrEngineCmdMsgBuffer;
return tStatus;
}
static JUNO_STATUS_T JunoDs_ArrayInit(JUNO_DS_ARRAY_ROOT_T *ptArray, const JUNO_DS_ARRAY_API_T *ptArrayApi, size_t iCapacity, JUNO_FAILURE_HANDLER_T pfcnFailureHdlr, JUNO_USER_DATA_T *pvUserData)
Initialize an array root with capacity and API.
Definition array_api.h:158
#define JUNO_ASSERT_EXISTS(ptr)
Returns JUNO_STATUS_NULLPTR_ERROR if the expression is falsy.
Definition macros.h:50
Pipe Queue Implementation
Finally we implement the SetAt, GetAt, and RemoveAt functions. These functions provide a type-safe interface to setting, getting, and removing values within the command buffer at specific indicies. It essentially acts as an API to the array.
{
ENGINE_CMD_MSG_ARRAY_T *ptEngineCmdMsgPipe = (ENGINE_CMD_MSG_ARRAY_T *)ptArray;
tStatus = tIndexPointer.ptApi->Copy(tIndexPointer, tItem);
return tStatus;
}
{
JUNO_RESULT_POINTER_T tResult = {0};
return tResult;
);
ENGINE_CMD_MSG_ARRAY_T *ptEngineCmdMsgPipe = (ENGINE_CMD_MSG_ARRAY_T *)ptArray;
tResult.tOk = tIndexPointer;
return tResult;
}
{
ENGINE_CMD_MSG_ARRAY_T *ptEngineCmdMsgPipe = (ENGINE_CMD_MSG_ARRAY_T *)ptArray;
tStatus = tIndexPointer.ptApi->Reset(tIndexPointer);
return tStatus;
}
#define ENGINE_CMD_MSG_PIPE_ASSERT_API(ptArray,...)
Asserts the api is for the pipe.
Definition engine_cmd_msg.c:104
#define EngineCmdMsg_PointerInit(addr)
Definition engine_cmd_msg.h:85
static JUNO_STATUS_T JunoDs_ArrayVerify(const JUNO_DS_ARRAY_ROOT_T *ptArray)
Verify an array instance's capacity and API.
Definition array_api.h:117
static JUNO_STATUS_T JunoDs_ArrayVerifyIndex(const JUNO_DS_ARRAY_ROOT_T *ptArray, size_t iIndex)
Verify an index is within array capacity.
Definition array_api.h:136
#define JUNO_STATUS_INVALID_TYPE_ERROR
Provided type or trait did not match the expected one.
Definition status.h:66
engine_app.c
Next we need to implement the application. engine_app.c will act as a composition root for application specific logic. This means that the application can own some dependencies, like its message buffers and pipes. The line between dependencies an application should own and dependencies the main.c root should own is fuzzy. It depends on developers specific use cases. In general, items like the pipes, memory buffers, and application specific dependencies should be owned by the application. Dependencies that reach to more than one application should be owned by the project composition root (main.c).
Application Api
As stated previously, LibJuno applications are derived from the JUNO_APP_ROOT_T. This means we need to implement the API defined by the root module. We forward declare the api functions and the verification function so we can check and verify that the pointer being passed to our function is in fact a ENGINE_APP_T type by asserting that the api pointers match.
};
static JUNO_STATUS_T OnProcess(JUNO_APP_ROOT_T *ptJunoApp)
Definition engine_app.c:168
static JUNO_STATUS_T OnStart(JUNO_APP_ROOT_T *ptJunoApp)
Definition engine_app.c:128
static JUNO_STATUS_T OnExit(JUNO_APP_ROOT_T *ptJunoApp)
Definition engine_app.c:248
static const JUNO_APP_API_T tEngineAppApi
Definition engine_app.c:54
static JUNO_STATUS_T Verify(JUNO_MEMORY_ALLOC_ROOT_T *ptJunoMemory)
Definition juno_memory_block.c:26
JUNO_STATUS_T(* OnStart)(JUNO_APP_ROOT_T *ptJunoApp)
Called once to initialize resources before processing.
Definition app_api.h:57
Verification Function
The verification function is called as the one of the very first lines in a LibJuno API function. Its purpose is to assert that the module contract is met. This means that pointers are initialized, and the APIs match.
In LibJuno we can use JUNO_ASSERT macros that will check for certain common conditions like null pointers. These macros will automatically return JUNO_STATUS_ERR or JUNO_STATUS_NULLPTR_ERROR on failure.
{
ptEngineApp &&
ptEngineApp->tRoot.ptApi &&
ptEngineApp->ptLogger &&
ptEngineApp->ptTime &&
ptEngineApp->ptBroker,
ptEngineApp,
"Module does not have all dependencies"
);
{
}
}
#define JUNO_ASSERT_EXISTS_MODULE(ptr, ptMod, str)
Like JUNO_ASSERT_EXISTS but also calls the module's failure handler.
Definition macros.h:66
#define JUNO_FAIL_MODULE(tStatus, ptMod, pcMessage)
Invoke a module instance's failure handler if available.
Definition status.h:138
The Application Init Function
The init function's primary responsibility is to assign dependencies. This function enables the main.c project composition root to pass application dependencies to the application. This function is a concrete implementation because it depends entirely on application specifics.
This function asserts the module pointer exists, assigns the dependency pointers, and finally calls the verification function. The verification function is called last because it assumes the pointers have been instantiated.
{
ptEngineApp->tRoot.JUNO_FAILURE_HANDLER = pfcnFailureHandler;
ptEngineApp->tRoot.JUNO_FAILURE_USER_DATA = pvFailureUserData;
ptEngineApp->ptLogger = ptLogger;
ptEngineApp->ptTime = ptTime;
ptEngineApp->ptBroker = ptBroker;
return tStatus;
}
<tt>OnStart</tt>
OnStart is the very first function called in a LibJuno project and is typically called only once. The primary purpose of OnStart is to initialize state variables, perform initialization tasks specific to the application, and allocate global application resources.
{
ptLoggerApi->
LogInfo(ptLogger,
"Engine App Initialized");
JUNO_STATUS_T(* LogInfo)(const JUNO_LOG_ROOT_T *ptJunoLog, const char *pcMsg,...)
Log an info-level message.
Definition log_api.h:58
Within OnStart we will initialize our command message pipe so we can receive commands. We will also register this pipe with the broker so it can be active.
tStatus = ptEngineApp->ptBroker->ptApi->RegisterSubscriber(ptEngineApp->ptBroker, &ptEngineApp->tCmdPipe);
static JUNO_STATUS_T JunoSb_PipeInit(JUNO_SB_PIPE_T *ptPipe, JUNO_SB_MID_T iMid, JUNO_DS_ARRAY_ROOT_T *ptArray, JUNO_FAILURE_HANDLER_T pfcnFailureHandler, JUNO_USER_DATA_T *pvUserData)
Initialize a pipe for a specific MID backed by a queue over ptArray.
Definition broker_api.h:132
#define ENGINE_CMD_MSG_MID
Definition engine_cmd_msg.h:45
For this example we will also set the current RPM to 0
ptEngineApp->fCurrentRpm = 0.0;
return tStatus;
}
<tt>OnProcess</tt>
OnProcess is the main run-loop function of the application. It is called on a periodic basis, typically at a set frequency (like 1Hz, 5Hz, 50Hz, 100Hz, etc).
This is where the main functionality of the application will reside.
{
ptLoggerApi->
LogDebug(ptLogger,
"Engine App Running");
JUNO_STATUS_T(* LogDebug)(const JUNO_LOG_ROOT_T *ptJunoLog, const char *pcMsg,...)
Log a debug-level message (most verbose).
Definition log_api.h:56
In this example we will sleep the application for 500ms to simulate extensive work being done within the application. We will also create a convenience variable to access the pipe's queue API.
usleep(500E3);
Queue API vtable.
Definition queue_api.h:84
For this example we will allocate a buffer to attempt to receive a command from the message pipe. We will then initialize an engine command pointer with the address to our allocated buffer.
We will then get the current timestamp using the time api provided by the project root.
JUNO_TIMESTAMP_RESULT_T tTimestampResult = ptTime->ptApi->Now(ptTime);
Next we initialize a telemetry message with the current RPM and timestamp. This is done at this step because in the case of an error, we will jump to the exit point with a goto statement so we always publish this telemetry.
#define EngineTlmMsg_PointerInit(addr)
Definition engine_tlm_msg.h:46
Definition engine_tlm_msg.h:34
We will calculate some artificial noise to simulate an actual sensor measurement.
float noise = rand();
noise = noise - RAND_MAX/2;
noise = 2 * noise / RAND_MAX;
Next we will attempt to dequeue a command from the software bus. This operation will fail if there is no command in the pipe and will jump to the exit point of this function.
tStatus = ptCmdPipeApi->
Dequeue(&ptEngineApp->tCmdPipe.tRoot, tEngineCmdPointer);
ptEngineApp->fCurrentRpm = tEngineCmd.
fRpm;
ptLoggerApi->
LogInfo(ptLogger,
"RPM Commanded to: %f", tEngineCmd.
fRpm);
JUNO_STATUS_T(* Dequeue)(JUNO_DS_QUEUE_ROOT_T *ptQueue, JUNO_POINTER_T tReturn)
Dequeue the item at the front of the queue.
Definition queue_api.h:94
Finally we will add some noise to the current RPM in all cases to simulate a real measurement and publish the telemetry.
exit:
tEngineTlm.
fRpm = ptEngineApp->fCurrentRpm + 10*noise;
return tStatus;
}
#define ENGINE_TLM_MSG_MID
Definition engine_tlm_msg.h:31
float fRpm
Definition engine_tlm_msg.h:36
<tt>OnExit</tt>
The OnExit function is typically the last function called in the application. It is used to clean-up globally allocated resources like sockets and file descriptors before the main run-time exits. This application does not have any resources to clean up so it just exits.
{
ptLoggerApi->
LogInfo(ptLogger,
"Engine App Exiting");
return tStatus;
}
system_manager_app.c
The System Manager application for this car example will subscribe to the engine apps's telemetry. Similar to the engine app's command pipe, the system manager will read the telemetry off its telemetry pipe. This is done by allocating a telemetry buffer.
Additionally the system manager will command the engine to certain RPMs. It will create the command and command pointer here.
The system manager will attempt to read telemetry off the software bus from the engine. If there is telemetry it will process it and set a new RPM. If there is no telemetry it will command the engine to the same target as before.
JUNO_TIME_MILLIS_RESULT_T tMillisResult = {0};
tStatus = ptSystemManagerApp->tEngineTlmPipe.tRoot.ptApi->Dequeue(&ptSystemManagerApp->tEngineTlmPipe.tRoot, tTlmMsgPointer);
The system manager will substract the time from engine start to get an elapsed time. I will then check if the engine is within the target RPM. If it is it will increment the RPM by 10RPM. If not it will send the same target RPM as before.
tStatus = ptTime->ptApi->SubtractTime(ptTime, &tTlmMsg.
tTimestamp, ptSystemManagerApp->tEngineStart);
tMillisResult = ptTime->ptApi->TimestampToMillis(ptTime, tTlmMsg.
tTimestamp);
ptLoggerApi->
LogInfo(ptLogger,
"Current Rpm: %f | Time: %ull", tTlmMsg.
fRpm, tMillisResult.tOk);
if(fabs(tTlmMsg.
fRpm - ptSystemManagerApp->fTargetRpm) < 10.0f)
{
ptSystemManagerApp->fTargetRpm += 10.0f;
}
JUNO_TIMESTAMP_T tTimestamp
Definition engine_tlm_msg.h:37
Finally the system manager sets the target RPM and publishes the message
exit:
tEngineCmd.
fRpm = ptSystemManagerApp->fTargetRpm;
return tStatus;
}
Conclusion
Hopefully this tutorial helped demonstrate how to utilize the LibJuno micro-framework within a software project. Its intent is to provide a toolbox to developers of interfaces and implementations that they can choose to use. Ultimately the developers will implement solutions targeted for their project so much of the architecture is dependent on specific project needs that can't possibly be captured here. This example project is to showcase the various capabilities a user can choose to implement.