Template Component

Introduction

CalAmp has made creating new components easy by including a template component to get the developer going. The template component contains all the basic features needed to create a custom component. It can be built and deployed without adding any new code except for specifying the EdgeApp Component ID supplied by Customer Service. The developer simply provides the component name to a script which will create a new component based on the template component using the newly specified component name. In order to use this script, perform the following steps:

  1. cd source/components/src/template/

  2. ./create_new_comp.sh <comp_name>

Component Architecture

Basic Process Model
EdgeApp and the components are based on the same basic process model, as shown below.

345

Basic process model

Three binaries are created: the process daemon, which includes the main thread, an API library, and a CLI executable. The more detailed view below shows how different handlers call the public and private APIs to interact with the process.

575

Detailed process model

EdgeApp uses a similar model, except that only one binary is created for each component. The EdgeApp process daemon (edgeapp_d), which starts each component, and EdgeApp CLI (edgeapp_cli) already exist in EdgeCore. The components are merely an extension of these objects.

This model can co-exist in a Linux environment or in a RTOS environment.

Basic Process/Thread Flow

Each component will have the same basic flow, as shown below

648

The component exists as one library, guarded by the Public and Private APIs. These API sets make asynchronous and synchronous calls to the component thread via Posix message queues. This architecture protects the internal data class from being manipulated via multiple threads of differing priorities and causing race conditions or mutual exclusion issue. All event, timer, and general API access and processing occurs in the context of the thread via the API's which blocks on the Message queue. This architecture is similar to using a semaphore on each function call as a queue is simply a list and a semaphore but, in this case, the semaphore is hidden.

In this model, asynchronous and callback type function calls are not blocked using a message queue as it would be just simply using a semaphore. Only functions requiring synchronous access are blocked until the thread get around to its request. This architecture reduces the possibilities of priority inversion and thread starvation.

Template Component Details

This section describes the main parts of the template component.

Directory Structure

The directory structure of the template component is as follows:

+> template/
|
+> include/
|  |
|  + template_api.h
|
+> src/
|  |
|  + template.c 
|  |
|  + template.h
|  |
|  + template_api.c
|  |
|  + template_api_private.h
|  |
|  + template_cli.c
|  |
|  + template_mbus.c
|  |
|  + template_mbus.h
|
+ CMakeLists.h
|
+ create_new_comp.sh
|
+Kconfig

Registration

In template.c, a function called template_register() is provided to allow the component to be registered. The call to this function is automatically added to the eac_hooks.c file. The function is shown below and does not need to be modified.

363: void template_register(register_eac_cb reg_cb, eac_reg_def_t *reg_def)
364: {
365:   frm_debug_log(DBG_M_EACOMP, DBG_LOG_INFO, "%s", __FUNCTION__);
366:
367:   /* Distinguish this EdgeApp component from other components */
368:   memcpy(g_template_eac_id, reg_def->eac_id.license, sizeof(g_template_eac_id));
369:   get_eac_id(&reg_def->eac_id);
370:
371:   reg_def->eac_cb.start = start_cmp;
372:   reg_def->eac_cb.stop = stop_cmp;
373:
374:   /* Register the component */
375:   reg_cb(reg_def);
376: }

[Excerpt from src/template.c]

This function provides the information that EdgeCore needs in order to register, start and stop the component. On line 369, the function get_eac_id() is called. The get_eac_id() function provides some of the details of this component. The developer needs to update some of this information.

31: #define TEMPLATE_EAC_VER "1.0.0.0"
32:
33: static uint8_t g_template_eac_id[EAC_LIC_LEN + 1];

[Excerpt from src/template.c]

On line 31, the developer can use up to 8 characters (plus the ‘\0’ character) to specify the version of this component. Line 33 contains storage for the license information retrieved from the license file at runtime. Contact Customer Support to get a license file for each component.

The registration function also assigns function pointers to start and stop the component, as seen on line 371 and 372. These functions are described in the Starting and Stopping the Component section.

The last line of the template_register() function, line 375, calls the callback into EdgeApp Host to start the registration process for the component.

Starting and Stopping the Component

If registration was successful, EdgeApp Host will call the start_cmp() specified by the function pointer in the registration function and is shown below.

299: static int32_t start_cmp(register_mbus_cb reg_mbus_cb)
300: {
301:   int32_t rc = EACOMP_SUCCESS;
302:
303:   /* Create a pthread to process messages. This thread will block on a queue that is written to via
304:    * the API.
305:    */
306:   if (pthread_create(&g_template.task.tid1, NULL, template_main, reg_mbus_cb))
307:   {
308:     rc = EACOMP_ERR;
309:   }
310:   else
311:   {
312:     pthread_setname_np(g_template.task.tid1, TEMPLATE_ID);
313:
314: #ifdef FRM_OS_TYPE_RTOS
315:      template_install_cmd_table(true);
316: #endif /* FRM_OS_TYPE_RTOS */
317:   }
318:
319:   return rc;
320: }

[Excerpt from src/template.c]

The start_cmp() function simply starts the main thread for the component, passes the MBUS registration callback and names the thread. On line 315, the CLI command table is installed. Note that this is only for RTOS environments. The start_cmp function does not need to be modified.

On shutdown, the stop_cmp() is called to terminate the component and is shown below.

328: static int32_t stop_cmp(void)
329: {
330:   template_msg_t evt_msg;
331:
332:   if (frm_mque_does_queue_exist(TEMPLATE_QUEUE_NAME) == 0)
333:   {
334:     return EACOMP_ERR_PROCESS_NOT_UP;
335:   }
336:
337:   /* Send a message to the thread to exit */
338:   memset(&evt_msg, 0, sizeof(evt_msg));
339:   evt_msg.type = TEMPLATE_MSG_EXIT;
340:
341:   frm_mque_send_async(TEMPLATE_QUEUE_NAME, &evt_msg, sizeof(evt_msg));
342:
343:   frm_debug_log(DBG_M_EACOMP, DBG_LOG_NOTICE, "%s=Terminate TEMPLATE;TID=%lu.", TEMPLATE_ID,
344:   g_template.task.tid1);
345:
346:   /* Wait until the thread exits */
347:   if (g_template.task.tid1)
348:   {
349:     pthread_join(g_template.task.tid1, NULL);
350:
351: #ifdef FRM_OS_TYPE_RTOS
352:     template_install_cmd_table(false);
353: #endif /* FRM_OS_TYPE_RTOS */
354:   }
355:
356:   return EACOMP_SUCCESS;
357: }

[Excerpt from src/template.c]

This command sends a message to the component thread to exit and then waits until the thread exits. It does not need to be modified.

Main Function

This is the starting point, or main function, for the thread of the component.

217: static void *template_main(void *arg)
218: {
219:   register_mbus_cb reg_mbus_cb = (register_mbus_cb)arg;
220:   frm_debug_log(DBG_M_EACOMP, DBG_LOG_INFO, "Starting %s", TEMPLATE_ID);
221:
222:   /* initialize the object vars */
223:   memset(&g_template, 0, sizeof(g_template));
224:   get_eac_id(&g_template.eac_id);
225:
226:   /* initialize the task structure */
227:   frm_task_init(&g_template.task, NULL, (char *)TEMPLATE_ID, TEMPLATE_QUEUE_NAME,
228:                 TEMPLATE_QUEUE_SIZE, (frm_mque_resp_t *)&g_template.evt_msg,
229:                 sizeof(template_msg_t), DBG_M_EACOMP, TEMPLATE_MAIN_TIMEOUT_PERIOD,
230:                 handle_timeout, handle_event_message, false);
231:
232:   /* FIRST: register with the watchdog */
233:   while (wdog_is_up() != WDOG_SUCCESS)
234:   {
235:     sleep(1);
236:   }
237:
238:   wdog_register((char *)TEMPLATE_ID, TEMPLATE_MAIN_TIMEOUT_PERIOD, &g_template.wdog_hdl);
239:
240:   /* start a timer */
241:   if (frm_task_make_timer((char *)TEMPLATE_TIMER_NAME, &g_template.timer_id,
242:                           TEMPLATE_TIMER_PERIOD,
243:                           TEMPLATE_TIMER_PERIOD,
244:                           template_timer_handler) != RC_SUCCESS)
245:   {
246:     frm_debug_log(DBG_M_EACOMP, DBG_LOG_ERR, "%s Timer Create error.", TEMPLATE_ID);
247:   }
248:
249:   /* Register for mbus channels */
250:   template_mbus_register(reg_mbus_cb);
251:
252:   /* Get the power state and boot info from PSM module. May need to do some action based on that. */
253:   {
254:     int32_t             rc;
255:     mbus_channel_type_e type;
256:     psm_state_notify_t  psm_info;
257:     int32_t             size;
258:
259:     rc = mbus_read_channel(MBUS_CHAN_POWER_STATE, &type, &psm_info, &size);
260:
261:     if (rc == MBUS_SUCCESS)
262:     {
263:       if (type == MBUS_CHAN_TYPE_BIN)
264:       {
265:         g_template.power_state = psm_info.power_state;
266:         frm_debug_log(DBG_M_EACOMP, DBG_LOG_NOTICE, "@Bootup Power state %d %d %d 0x%x",
267:                       psm_info.power_state, psm_info.boot_state, psm_info.boot_reason,
268:                       psm_info.triggers);
269:       }
270:     }
271:     else
272:     {
273:       frm_debug_log(DBG_M_EACOMP, DBG_LOG_ERR, "%s PSM/MBUS is not up", TEMPLATE_ID);
274:     }
275:   }
276:
277:   /* main execution loop ************************************ */
278:   frm_task_execute(&g_template.task);
279:
280:   /* delete the timer */
281:   frm_task_timer_delete(g_template.timer_id);
282:
283:   /* de-register with the watchdog */
284:   wdog_deregister(g_template.wdog_hdl);
285:
286:   frm_debug_log(DBG_M_EACOMP, DBG_LOG_NOTICE, "%s Component Terminating.", TEMPLATE_ID);
287:
288:   FRM_EXIT;
289: }

[Excerpt from src/template.c]

The main thread is split into two halves. The top half, lines 217 – 277, initializes the task structure, registers for a software watchdog to ensure the thread is healthy, creates a timer, and registers for MBUS channels. The bottom half, lines 279 – 289, cleans up the memory and resources claimed by the top half. In between the two is the main execution loop on line 278.

Note that the creation of the timer and its usage throughout the file is done for example purposes. If the timer is not needed, consider commenting it out.

The top half of the main thread starts off by saving the incoming argument, which is a callback to be used to register for MBUS channels on line 219. Then object variables are initialized on lines 223-4. On lines 227-30, the task structure is initialized. If required, arguments may be specified which are returned when the handle_event_message() and handle_timeout() callbacks are called. This initialization creates a task queue to the specifications provided, such as name and number of elements. This number can be adjusted as needed by setting the TEMPLATE_QUEUE_SIZE definition in template.h. When a message is queued, the handle_event_message() function is called. See the Creating APIs section. A timeout period is also passed. When the timeout period elapses, the handle_timeout() function is called back to pet the watchdog. See the Handling Timeout section. This timeout period can be modified by setting the TEMPLATE_MAIN_TIMEOUT_PERIOD definition in template.h.

Next, the task registers for the watchdog on lines 233 – 238. Note that the watchdog timeout value is the same definition passed for task initialization.

Lines 241-247 start a timer. This is done purely as an example. It may be modified or removed to suit the needs of the component. There are a couple things to consider when creating a timer. The first is the initial timeout period (line 242). The second is the recurring timeout period (line 243). This period is the second, third, etc. timeout period. If a one-shot timer is needed, configure the initial time out period to meet the requirements, but configure the recurring timeout period to be 0. If both values are the same, then all timeouts occur at the same interval until the timer is disarmed. A callback, template_timer_handler(), is specified and is called when the timer expires. See the Timer Handler section. If the timer is not needed by the component, consider commenting out this section and other related sections in the created component.

Next, the template_mbus_register() function is called on line 250, passing the registration callback. See Registering for MBUS Events section for more details.

The last part of the top half of the main thread reads the power state and boot information from the PSM module on lines 253 – 275. The component may need to behave differently on different boot conditions, such as a warm or cold boot, or based on the current power state, such as FOTA.

Splitting the two halves, is the main execution loop on line 278. This is handled behind the scenes to ensure that all tasks are handled the same way, regardless of the operating system. It basically services the queue and calls the appropriate callbacks when necessary.

The bottom half of the main thread is executed when the main thread receives a message to exit from the stop_cmp() function. It does not need to deregister from MBUS events, as that is taken care of by the EdgeApp Host. Line 281 deletes the example timer. If the timer is not needed by the component, then this line may be removed. Then, the task deregisters from the watchdog. Lastly, the thread is exited with the call to FRM_EXIT. It is important not to remove this line, as it exits the thread properly based on the operating system that is present.

Registering for MBUS Events

Registering for MBUS events allows the developer to receive only the events that are required for the component. This is handled in the template_mbus.c file with the provided function, template_mbus_register().

98:  int32_t template_mbus_register(register_mbus_cb reg_mbus_cb)
99:  {
100:   eac_mbus_def_t mbus_def;
101:   int32_t        rc = EACOMP_ERR;
102:
103:   if (reg_mbus_cb)
104:   {
105:     memcpy(mbus_def.license, license, sizeof(mbus_def.license));
106:     mbus_def.chans = template_mbus_chans;
107:     mbus_def.num_chans = COUNT_OF(template_mbus_chans);
108:     mbus_def.mbus_hdlr = mbus_handler;
109:
110:     rc = reg_mbus_cb(&mbus_def);
111: 
112:     if (EACOMP_SUCCESS == rc)
113:     {
114:       frm_debug_log(DBG_M_EACOMP, DBG_LOG_NOTICE, "%s: registered and subscribed for mbus events.",
115:                     TEMPLATE_ID);
116:     }
117:   }
118: 
119:   return (rc);
120: }

[Excerpt from src/template_mbus.c]

This function does not need to be modified. However, the following array needs to be modified to include any channels that are desired.

34:   static const mbus_channel_spec_e template_mbus_chans[] =
35:   {
36:     MBUS_CHAN_POWER_STATE,
37:     MBUS_CHAN_WDOG_SHUTDOWN_EVENT,
38:   };

[Excerpt from src/template_mbus.c]

For each event added to the above array, a case statement should be added to the mbus_handler() function.

62:   static void mbus_handler(uint32_t chan, mbus_channel_type_e type, mbus_data_t *data, uint16_t size)
63:   {
64:     (void)size;
65:   
66:     switch (chan)
67:     {
68:       case MBUS_CHAN_POWER_STATE:
69:         if (type == MBUS_CHAN_TYPE_BIN)
70:         {
71:           psm_state_notify_t *psm_info = (psm_state_notify_t *)(void *)data->buff;
72:           template_power_state(psm_info);
73:         }
74:   
75:         break;
76:   
77:       case MBUS_CHAN_WDOG_SHUTDOWN_EVENT:
78:         if (type == MBUS_CHAN_TYPE_UINT32)
79:         {
80:           /* no action required by template */
81:           frm_debug_log(DBG_M_EACOMP, DBG_LOG_NOTICE, "%s: Watchdog power down event.",
82:                         TEMPLATE_ID);
83:         }
84:   
85:         break;
86:   
87:       default:
88:         frm_debug_log(DBG_M_EACOMP, DBG_LOG_ERR, "%s: subscribed channel %d not handled.",
89:                       TEMPLATE_ID, chan);
90:         break;
91:     }
92:   }

[Excerpt from src/template_mbus.c]

Note that when this function is called, it is not on the task thread. Therefore, an API, such as template_power_state() on line 72, should be created to handle the event. See the Creating APIs section.

Creating APIs

There are two types of APIs, public and private. The public APIs have prototypes in template_api.h file in the include directory. These APIs may be called by other components or special ones can be called by the EdgeApp Host, such as template_register(), template_cli_search() and template_cli_help(). The function template_register() is discussed in the Registration section. The functions template_cli_search() and template_cli_help() are discussed in the Creating CLIs section. Private APIs are to be used in callback functions that are called back on another thread, such as mbus_handler(). The prototypes for private APIs reside in template_api_private.h.

Except for the special APIs called by EdgeApp Host, both public and private APIs follow the same model.

  1. Validate parameters (not required for private APIs)
  2. Ensure the thread is running
  3. Queue a message_

For example, in the Registering for MBUS Events section, the private API template_power_state() is called in the mbus_handler() function and shown below.

61: int32_t template_power_state(psm_state_notify_t *psm_info)
62: {
63:   template_msg_t evt_msg;
64:
65:   /* The rest of this API requires the process executing */
66:   if (template_is_up() != EACOMP_SUCCESS)
67:   {
68:     return (EACOMP_ERR_PROCESS_NOT_UP);
69:   }
70:
71:   memset(&evt_msg, 0, sizeof(evt_msg));
72:   evt_msg.type = TEMPLATE_MSG_POWER_STATE;
73:   memcpy(&evt_msg.data.psm_info, psm_info, sizeof(psm_state_notify_t));
74:
75:   frm_debug_log(DBG_M_EACOMP, DBG_LOG_DEBUG, "Sending async Power state to %s thread.",
76:                 TEMPLATE_ID);
77:   frm_mque_send_async((char *)TEMPLATE_QUEUE_NAME, &evt_msg, sizeof(evt_msg));
78:
79:   return (EACOMP_SUCCESS);
80: }

[Excerpt from src/template_api.c]

Since this is a private API, the incoming parameter validity isn’t checked. Lines 65-69 ensure that the thread is running. Lines 71-77 queue a message to the component task.

When creating an API, consider the following tasks:

  1. Determine whether the API is public or private and put the prototype in the appropriate header file.
  2. Create a new message enumeration.
  3. Create data space in the queue message (if necessary)
  4. Create a case statement using the new enumeration.
  5. Determine if the message should by asynchronous or synchronous

The function template_power_state() is a private API because the prototype exists in template_api_private.h. The message enumeration member, TEMPLATE_MSG_POWER_STATE, is defined in the file template.h, as shown below.

40:   typedef enum
41:   {
42:     TEMPLATE_MSG_EXIT = 0,                 /*!< The default id representing no event, or exit. */
43:     TEMPLATE_MSG_GET_VERSION,              /*!< Sync message to get component version. */
44:     TEMPLATE_MSG_TIMER,                    /*!< example of timer module. */
45:     TEMPLATE_MSG_POWER_STATE,              /*!< Power state from message bus. */
46:     TEMPLATE_MSG_MAX                       /*!< The max id for an event type. */
47:   } template_msg_type_e;

[Excerpt from src/template.h]

New message types should be added to this enumeration.

The data space for APIs are also defined in template.h as shown below.

52:   typedef struct
53:   {
54:     frm_mque_resp_t              resp;      /*!< Required structure for mque api */
55:     template_msg_type_e      type;      /*!< An template_msg_type_e value. */
56:     union
57:     {
58:       psm_state_notify_t       psm_info;    /*!< Message content for powerstate. */
59:       eac_identity_t           eac_id;      /*!< Struct to hold component identity */
60:     } data;
61:   } template_msg_t;

[Excerpt from src/template.h]

The psm_info struct used in the template_power_state() API is defined in the data union. New message data should be added to the union to minimize the memory needed for the message queue.

When the thread receives a message on the queue, handle_event_message() in template.c is called on the component thread.

181:  static int32_t handle_event_message(void *arg, frm_mque_resp_t *msg)
182:  {
183:    template_msg_t *evt_msg = (template_msg_t *)msg;
184:  
185:    switch (evt_msg->type)
186:    {
187:      case TEMPLATE_MSG_EXIT:
188:        exit_impl();
189:        break;
190:  
191:      case TEMPLATE_MSG_POWER_STATE:
192:        pwr_state_impl(evt_msg);
193:        break;
194:  
195:      case TEMPLATE_MSG_TIMER:
196:        timer_impl(evt_msg);
197:        break;
198:  
199:      case TEMPLATE_MSG_GET_VERSION:
200:        get_version_impl(evt_msg);
201:        break;
202:  
203:      default:
204:        break;
205:    }
206:  
207:    return (EACOMP_SUCCESS);
208:  }

[Excerpt from src/template.c]

This function should be modified to add a case statement each new message enumeration. Note that the arguments specified, if any, at task initialization are passed back to this function.

Handling Timeouts

In order to ensure that the watchdog does not restart the device, each registered task must pet its own watchdog. In the Main Function section, the template component registered for a watchdog with a specified timeout value. When this time as elapsed, the handle_timeout() callback is called.

161: static int32_t handle_timeout(void *arg)
162: {
163:   /* pet the watchdog */
164:   wdog_pet_dog(g_template.wdog_hdl);
165:
166:   frm_debug_log(DBG_M_EACOMP, DBG_LOG_DEBUG, "%s: Timeout Handler", TEMPLATE_ID);
167:   return (EACOMP_SUCCESS);
168: }

[Excerpt from src/template.c]

This function does not need to be modified, but can be to fit the needs of the component. The only thing that is required of this function is that the wdog_pet_dog() function is called. Note that the arguments specified at initialization, if any, are passed back to this function.

Timer Handler

If one or more timers are created, a handler is needed for when the timer expires.

68: FRM_TIMER_HANDLER_START(template_timer_handler)
69: {
70:   FRM_TIMER_HANDLER_ARGS
71:
72:   /* multiple timer are handled in this if */
73:   if (timer_id == g_template.timer_id)
74:   {
75:     template_timer();
76:   }
77:     else
78:   {
79:     frm_debug_log(DBG_M_EACOMP, DBG_LOG_NOTICE, "Second timer %d", timer_id);
80:   }
81: }

[Excerpt from src/template.c]

A macro, FRM_TIMER_HANDLER_START, is used to cover the different ways operating systems call timer callbacks, and is required. Another macro, FRM_TIMER_HANDLER_ARGS, which is also required, streamlines the function arguments and declares a POSIX compliant typedef “timer_t timer_id”, which is set to the ID of the timer that expired.

This handler is not called on the component thread. Consider creating a private API to handle the particular timer timeout. See the Creating APIs section.

Creating CLIs

CLIs can be useful when debugging a new component. The CLIs are executed by a separate process, so all debugging must be handled via APIs, whether they are public of private. Several basic CLIs are provided with the template component to demonstrate how they may be used and to provide functionality that is common for all component, such as retrieving a component version.

224: static int32_t cmd_pwr_state(int32_t argc, char **argv)
225: {
226:   psm_pwr_state_e    power_state;
227:   psm_state_notify_t psm_info;
228:   int32_t            rc = EACOMP_SUCCESS;
229:
230:   /* if command line option */
231:   if (3 == argc)
232:   {
233:     power_state = (psm_pwr_state_e)strtol(argv[2], NULL, 0);
234:   }
235:   else
236:   {
237:     frm_printf("Enter power state [0=ONLINE, 1=LPM, 2=TEST, 3=FOTA]: ");
238:     power_state = (psm_pwr_state_e)get_unsigned_from_buff();
239:   }
240:
241:   memset(&psm_info, 0, sizeof(psm_info));
242:   psm_info.power_state = (uint8_t)power_state;
243:
244:   rc = template_power_state(&psm_info);
245:   frm_printf("%s - [%d] %s\n", argv[1], RC_FROM_RC(rc), get_rc_str(rc));
246:   return (rc);
247: }

[Excerpt from src/template_cli.c]

The above example shows how an API may be tested. The usage for the command is “edgeapp_cli tmp_pwr_state []”. If the state is specified on the command line, it is read in from the function arguments list on line 233. If the state was not specified, the user is prompted for input on line 237, and the input is read on line 238. Once the input is obtained, the API function is called. The API, as described in the Creating APIs section, queues a message to the main component thread. This is essential, as the CLI runs in a separate executable process.

Note that in order to get a number from the user, a local function, get_unsigned_from_buff() is called on line 238.

112: static uint32_t get_unsigned_from_buff(void)
113: {
114:   char buff[MAX_INSTR_SIZE];
115:
116:   get_trim_buff(buff, sizeof(buff));
117:
118:   return (uint32_t)strtoul(buff, NULL, 0);
119: }

[Excerpt from src/template_cli.c]

This simple function is shown above to illustrate two things. First, if a signed number needed to be retrieved, another function could be created identical to this one, except that instead of calling stroult(), strol() would be called on line 118. Second, if a string needs to be retrieved, simply call get_trim_buff(), with the appropriate parameters.

Linking with EdgeCore

In order for components to call EdgeCore APIs, the component must link to the specific EdgeCore API library. This can be done in the CMakeLists.txt file in the component directory. The template component starts this process by linking to a set of BASE_LIBS, as shown below.

29:   # Link libraries
30:   target_link_libraries(${PROJECT_NAME}${CALAMP_LIB_SUFFIX}
31:                         ${BASE_LIBS})

[Excerpt from CMakeLists.txt]

The set of modules included in BASE_LIBS are config, framework, mbus, wand wdog. The pthread library is also included, although it is not an EdgeCore library. This group is defined in the top-level CMakeLists.txt.

If your component needs to call APIs lmdirect module, simple add a line as shown below

# Link libraries
target_link_libraries(${PROJECT_NAME}${CALAMP_LIB_SUFFIX}
                      ${BASE_LIBS}
                      lmdirect${CALAMP_LIB_SUFFIX})