Modbus Library Test Coverage Report


lib/MENU_LIB/
File: menu_lib.c
Date: 2025-11-17 20:30:01
Lines:
159 of 168, 0 excluded
94.6%
Functions:
21 of 21, 0 excluded
100.0%
Branches:
61 of 74, 0 excluded
82.4%

Line Branch Exec Source
1 /**
2 * @file menu_lib.c
3 * @author niwciu (niwciu@gmail.com)
4 * @brief Implementation of hierarchical menu library with display driver abstraction.
5 * @version 1.0.0
6 * @date 2025-10-27
7 *
8 * Provides menu initialization, navigation, and rendering logic for embedded systems.
9 * Includes validation of menu depth, screen drawing, and safe handling of pointers.
10 *
11 * @copyright Copyright (c) 2025
12 *
13 * @ingroup MenuLib
14 * @{
15 */
16
17 #include "menu_lib.h"
18 #include "menu.h"
19 #include "menu_screen_driver_interface.h"
20 #include <string.h>
21 #include <stdbool.h>
22
23 #ifndef UNIT_TESTS
24 #define PRIVATE static
25 #else
26 #define PRIVATE
27 #endif
28
29 /* --- Constants --- */
30 #define ADDITIONAL_SPACE_CHAR_QTY 2U /**< Space padding in submenu headers */
31 #define DEFAULT_MENU_HEADER_COLUMN 7U /**< Default column for centered header text */
32 #define STRING_START_POSITION 1U /**< Column index for text after cursor marker */
33 #define CURSOR_COLUMN_POSITION 0U /**< Column index of cursor marker '>' */
34 #define FIRST_ROW 0U /**< Index of the first display row */
35 #define MENU_VIEW_FIRST_ROW 1U /**< First row used for displaying menu items */
36 #define FIRST_COLUMN 0U /**< Index of the first display column */
37 #define HEADER_ROW_OFFSET 1U /**< Offset between header and menu view */
38
39 /* --- Internal state --- */
40 PRIVATE bool menu_initialized = false; /**< Indicates if menu system has been initialized */
41
42 PRIVATE uint8_t menu_number_of_chars_per_line;
43 PRIVATE uint8_t menu_number_of_screen_lines;
44
45 static menu_t *current_menu_pointer;
46 static menu_t *menu_item_2_print = NULL;
47 static menu_t *menu_1st_item = NULL;
48
49 static uint8_t cursor_selection_menu_index[MAX_MENU_DEPTH];
50 static uint8_t cursor_row_position[MAX_MENU_DEPTH];
51 static uint8_t menu_level;
52
53 PRIVATE const struct menu_screen_driver_interface_struct *DISPLAY = NULL;
54 static menu_exit_cb_t menu_top_level_exit_cb = NULL;
55
56 static const char *default_header = " MENU ";
57 static const char *custom_header = NULL;
58
59 /* --- Private function declarations --- */
60 static bool validate_display_interface(const struct menu_screen_driver_interface_struct *disp);
61 static uint8_t compute_menu_depth(const menu_t *menu);
62 static menu_status_t validate_menu_view(const menu_t *root_menu);
63 static void initialize_menu_view_state(menu_t *root_menu, menu_exit_cb_t menu_exit_cb, const char *custom_menu_header);
64 static void display_menu_header(void);
65 static void display_main_menu_header(void);
66 static void display_sub_menu_header(void);
67 static void fill_header_with_dashes(void);
68 static void clear_current_menu_view_with_cursor(void);
69 static void update_menu_item_pointer_to_print(void);
70 static void update_current_menu_view_with_cursor(void);
71 static void display_cursor_marker_if_needed(uint8_t row);
72 static void display_menu_item_name(uint8_t row, const char *name);
73
74 /* --- Implementation --- */
75
76 /**
77 * @brief Initialize the menu system and display interface.
78 *
79 * Validates that a display driver is available and that all required
80 * function pointers are implemented.
81 *
82 * @return menu_status_t
83 * - MENU_OK — Initialization successful
84 * - MENU_ERR_NO_DISPLAY_INTERFACE — Display driver interface missing
85 * - MENU_ERR_INCOMPLETE_INTERFACE — Display driver missing required functions
86 */
87 52 menu_status_t menu_init(void)
88 {
89 52 menu_status_t status = MENU_OK;
90
91 52 DISPLAY = get_menu_display_driver_interface();
92
93
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 51 times.
52 if (DISPLAY == NULL)
94 {
95 1 status = MENU_ERR_NO_DISPLAY_INTERFACE;
96 }
97
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 50 times.
51 else if (!validate_display_interface(DISPLAY))
98 {
99 1 status = MENU_ERR_INCOMPLETE_INTERFACE;
100 }
101 else
102 {
103 50 menu_number_of_chars_per_line = DISPLAY->get_number_of_chars_per_line();
104 50 menu_number_of_screen_lines = DISPLAY->get_number_of_screen_lines();
105 50 DISPLAY->screen_init();
106 }
107
108 52 menu_initialized = (status == MENU_OK);
109 52 return status;
110 }
111
112 /**
113 * @brief Initialize and display a menu view.
114 *
115 * Performs validation of menu structure and prepares the screen for rendering.
116 * Initializes cursor, header, and root menu pointers.
117 *
118 * @param root_menu Pointer to the root menu.
119 * @param menu_exit_cb Callback executed when the top-level menu is exited.
120 * @param custom_menu_header Optional custom header string (NULL for default).
121 * @return menu_status_t
122 * - MENU_OK — Menu view successfully initialized
123 * - MENU_ERR_NOT_INITIALIZED — Menu engine not initialized
124 * - MENU_ERR_NO_MENU — root_menu pointer is NULL
125 * - MENU_ERR_MENU_TOO_DEEP — Menu depth exceeds MAX_MENU_DEPTH
126 */
127 49 menu_status_t menu_view_init(menu_t *root_menu, menu_exit_cb_t menu_exit_cb, const char *custom_menu_header)
128 {
129 49 menu_status_t status = validate_menu_view(root_menu);
130
131
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 3 times.
49 if (status == MENU_OK)
132 {
133 46 initialize_menu_view_state(root_menu, menu_exit_cb, custom_menu_header);
134 }
135
136 49 return status;
137 }
138
139 /**
140 * @brief Move cursor to the next menu item.
141 *
142 * If the next menu item exists, updates the current pointer,
143 * cursor selection index, and screen row position.
144 */
145 1209 void menu_next(void)
146 {
147
2/2
✓ Branch 0 taken 156 times.
✓ Branch 1 taken 1053 times.
1209 if (current_menu_pointer->next != NULL)
148 {
149 156 current_menu_pointer = current_menu_pointer->next;
150 156 cursor_selection_menu_index[menu_level]++;
151
2/2
✓ Branch 0 taken 105 times.
✓ Branch 1 taken 51 times.
156 if (cursor_row_position[menu_level] < (menu_number_of_screen_lines - 2U))
152 {
153 105 cursor_row_position[menu_level]++;
154 }
155 156 update_screen_view();
156 }
157 1209 }
158
159 /**
160 * @brief Move cursor to the previous menu item.
161 *
162 * If the previous menu item exists, updates the current pointer,
163 * cursor selection index, and screen row position.
164 */
165 185 void menu_prev(void)
166 {
167
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 118 times.
185 if (current_menu_pointer->prev != NULL)
168 {
169 67 current_menu_pointer = current_menu_pointer->prev;
170 67 cursor_selection_menu_index[menu_level]--;
171
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 17 times.
67 if (cursor_row_position[menu_level] != 0U)
172 {
173 50 cursor_row_position[menu_level]--;
174 }
175 67 update_screen_view();
176 }
177 185 }
178
179 /**
180 * @brief Enter submenu or execute item callback.
181 *
182 * If current item has a child menu, descend into submenu.
183 * Otherwise, execute the callback function if present.
184 */
185 49 void menu_enter(void)
186 {
187
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 3 times.
49 if (current_menu_pointer->child != NULL)
188 {
189 46 menu_level++;
190
1/2
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
46 if (menu_level < MAX_MENU_DEPTH)
191 {
192 46 current_menu_pointer = current_menu_pointer->child;
193 46 cursor_selection_menu_index[menu_level] = 0U;
194 46 cursor_row_position[menu_level] = 0U;
195 }
196 else
197 {
198 menu_level--;
199 }
200 46 update_screen_view();
201 }
202
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
3 else if (current_menu_pointer->callback != NULL)
203 {
204 1 current_menu_pointer->callback();
205 }
206 49 }
207
208 /**
209 * @brief Exit current submenu or call top-level exit callback.
210 *
211 * Moves up one menu level if possible, or calls the top-level exit callback.
212 */
213 29 void menu_esc(void)
214 {
215
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 18 times.
29 if (current_menu_pointer->parent != NULL)
216 {
217 11 menu_level--;
218 11 current_menu_pointer = current_menu_pointer->parent;
219 11 update_screen_view();
220 }
221
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 17 times.
18 else if (menu_top_level_exit_cb != NULL)
222 {
223 1 menu_top_level_exit_cb();
224 }
225 29 }
226
227 /**
228 * @brief Refresh the current screen view.
229 *
230 * Updates header, clears menu area, adjusts item pointers,
231 * and redraws menu items including cursor.
232 */
233 326 void update_screen_view(void)
234 {
235 326 display_menu_header();
236 326 clear_current_menu_view_with_cursor();
237 326 update_menu_item_pointer_to_print();
238 326 update_current_menu_view_with_cursor();
239 326 }
240
241 /**
242 * @brief Get pointer to the currently selected menu item.
243 *
244 * @return menu_t* Pointer to the current menu.
245 */
246 2 menu_t *get_current_menu_position(void)
247 {
248 2 return current_menu_pointer;
249 }
250
251 /* --- Private helper functions --- */
252
253 /**
254 * @brief Validate display driver interface.
255 *
256 * Checks that all required function pointers are non-NULL.
257 *
258 * @param disp Pointer to the display driver interface.
259 * @return true if valid, false otherwise.
260 */
261 51 static bool validate_display_interface(const struct menu_screen_driver_interface_struct *disp)
262 {
263 51 return (disp != NULL) &&
264
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 1 time.
51 (disp->get_number_of_chars_per_line != NULL) &&
265
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 (disp->get_number_of_screen_lines != NULL) &&
266
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 (disp->cursor_position != NULL) &&
267
2/4
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✗ Branch 3 not taken.
152 (disp->print_string != NULL) &&
268
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 (disp->print_char != NULL);
269 }
270
271 /**
272 * @brief Recursively compute menu depth.
273 *
274 * Determines maximum depth of menu tree starting from given node.
275 *
276 * @param menu Pointer to a menu item.
277 * @return uint8_t Depth level (1 for leaf node, 0 for NULL).
278 */
279 231 static uint8_t compute_menu_depth(const menu_t *menu)
280 {
281 231 uint8_t depth = 0U;
282
283
1/2
✓ Branch 0 taken 231 times.
✗ Branch 1 not taken.
231 if (menu != NULL)
284 {
285 231 uint8_t max_child_depth = 0U;
286 231 const menu_t *child = menu->child;
287
288
2/2
✓ Branch 0 taken 184 times.
✓ Branch 1 taken 231 times.
415 while (child != NULL)
289 {
290 184 uint8_t child_depth = compute_menu_depth(child);
291
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 135 times.
184 if (child_depth > max_child_depth)
292 {
293 49 max_child_depth = child_depth;
294 }
295 184 child = child->next;
296 }
297
298 231 depth = 1U + max_child_depth;
299 }
300
301 231 return depth;
302 }
303
304 /**
305 * @brief Validate menu view configuration.
306 *
307 * Ensures engine is initialized, the menu root is valid,
308 * and the menu depth does not exceed the maximum allowed.
309 *
310 * @param root_menu Pointer to the root menu.
311 * @return menu_status_t Validation result.
312 */
313 49 static menu_status_t validate_menu_view(const menu_t *root_menu)
314 {
315 49 menu_status_t status = MENU_OK;
316
317
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 48 times.
49 if (!menu_initialized)
318 {
319 1 status = MENU_ERR_NOT_INITIALIZED;
320 }
321
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 47 times.
48 else if (root_menu == NULL)
322 {
323 1 status = MENU_ERR_NO_MENU;
324 }
325
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 46 times.
47 else if (compute_menu_depth(root_menu) > MAX_MENU_DEPTH)
326 {
327 1 status = MENU_ERR_MENU_TOO_DEEP;
328 }
329
330 49 return status;
331 }
332
333 /**
334 * @brief Initialize menu view internal state.
335 *
336 * Sets current menu, first item, cursor positions, and top-level exit callback.
337 *
338 * @param root_menu Root menu pointer
339 * @param menu_exit_cb Top-level exit callback
340 * @param custom_menu_header Optional custom header
341 */
342 46 static void initialize_menu_view_state(menu_t *root_menu, menu_exit_cb_t menu_exit_cb, const char *custom_menu_header)
343 {
344 46 current_menu_pointer = root_menu;
345 46 menu_1st_item = root_menu;
346 46 menu_level = 0U;
347 46 cursor_selection_menu_index[menu_level] = 0U;
348 46 cursor_row_position[menu_level] = 0U;
349 46 menu_top_level_exit_cb = menu_exit_cb;
350 46 custom_header = custom_menu_header;
351
352 46 update_screen_view();
353 46 }
354
355 /**
356 * @brief Display appropriate menu header (main or submenu).
357 */
358 326 static void display_menu_header(void)
359 {
360 326 fill_header_with_dashes();
361
362
2/2
✓ Branch 0 taken 197 times.
✓ Branch 1 taken 129 times.
326 if (current_menu_pointer->parent == NULL)
363 197 display_main_menu_header();
364 else
365 129 display_sub_menu_header();
366 326 }
367
368 /**
369 * @brief Displays the main menu header (top-level view).
370 *
371 * Centers the custom header if provided; otherwise, uses default column.
372 * Sets menu_item_2_print to first menu item for rendering.
373 */
374 197 static void display_main_menu_header(void)
375 {
376 197 const char *header_str = default_header;
377 197 uint8_t column = DEFAULT_MENU_HEADER_COLUMN;
378
379
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 197 times.
197 if (custom_header != NULL)
380 {
381 header_str = custom_header;
382 const uint8_t header_length = (uint8_t)strlen(custom_header);
383
384 /* Center custom header on the screen if it fits */
385 if (menu_number_of_chars_per_line > header_length)
386 {
387 column = (menu_number_of_chars_per_line - header_length) / 2U;
388 }
389 else
390 {
391 /* Fallback to first column if header is longer than screen width */
392 column = FIRST_COLUMN;
393 }
394 }
395
396 /* Move cursor and print header */
397 197 DISPLAY->cursor_position(FIRST_ROW, column);
398 197 DISPLAY->print_string(header_str);
399
400 /* Set pointer to first menu item for rendering */
401 197 menu_item_2_print = menu_1st_item;
402 197 }
403
404 /**
405 * @brief Display header for submenu.
406 *
407 * Centers parent menu name with padding and sets
408 * menu_item_2_print to first child menu item.
409 */
410 129 static void display_sub_menu_header(void)
411 {
412 129 const char *parent_name = current_menu_pointer->parent->name;
413 129 uint8_t header_len = (uint8_t)strlen(parent_name) + ADDITIONAL_SPACE_CHAR_QTY;
414 uint8_t header_start;
415
416 /* Center header on screen if it fits, otherwise start at first column */
417
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 if (menu_number_of_chars_per_line > header_len)
418 {
419 129 header_start = (menu_number_of_chars_per_line - header_len) / 2U;
420 }
421 else
422 {
423 header_start = FIRST_COLUMN;
424 }
425
426 /* Move cursor and print header with padding */
427 129 DISPLAY->cursor_position(FIRST_ROW, header_start);
428 129 DISPLAY->print_char(' ');
429 129 DISPLAY->print_string(parent_name);
430 129 DISPLAY->print_char(' ');
431
432 /* Set pointer to first child menu item for rendering */
433 129 menu_item_2_print = current_menu_pointer->parent->child;
434 129 }
435
436 /**
437 * @brief Fill header row with dash characters.
438 */
439 326 static void fill_header_with_dashes(void)
440 {
441 326 DISPLAY->cursor_position(FIRST_ROW, FIRST_COLUMN);
442
2/2
✓ Branch 0 taken 6520 times.
✓ Branch 1 taken 326 times.
6846 for (uint8_t i = 0U; i < menu_number_of_chars_per_line; i++)
443 6520 DISPLAY->print_char('-');
444 326 }
445
446 /**
447 * @brief Clear the menu view area on the screen.
448 */
449 326 static void clear_current_menu_view_with_cursor(void)
450 {
451
2/2
✓ Branch 0 taken 978 times.
✓ Branch 1 taken 326 times.
1304 for (uint8_t i = MENU_VIEW_FIRST_ROW; i < menu_number_of_screen_lines; i++)
452 {
453 978 DISPLAY->cursor_position(i, FIRST_COLUMN);
454
2/2
✓ Branch 0 taken 19560 times.
✓ Branch 1 taken 978 times.
20538 for (uint8_t j = 0U; j < menu_number_of_chars_per_line; j++)
455 19560 DISPLAY->print_char(' ');
456 }
457 326 }
458
459 /**
460 * @brief Adjust pointer to the first menu item to print.
461 *
462 * Calculates offset from cursor selection index to row position.
463 */
464 326 static void update_menu_item_pointer_to_print(void)
465 {
466 326 uint8_t offset = cursor_selection_menu_index[menu_level] - cursor_row_position[menu_level];
467
2/2
✓ Branch 0 taken 152 times.
✓ Branch 1 taken 326 times.
478 for (uint8_t i = 0U; i < offset; i++)
468 {
469
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 152 times.
152 if (menu_item_2_print == NULL)
470 break;
471 152 menu_item_2_print = menu_item_2_print->next;
472 }
473 326 }
474
475 /**
476 * @brief Render visible menu items with cursor.
477 */
478 326 static void update_current_menu_view_with_cursor(void)
479 {
480
2/2
✓ Branch 0 taken 977 times.
✓ Branch 1 taken 325 times.
1302 for (uint8_t row = MENU_VIEW_FIRST_ROW; row < menu_number_of_screen_lines; row++)
481 {
482 977 display_cursor_marker_if_needed(row);
483
484
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 976 times.
977 if (menu_item_2_print == NULL)
485 {
486 // Null -> no more items to print on lcd
487 1 break;
488 }
489
490 976 display_menu_item_name(row, menu_item_2_print->name);
491 976 menu_item_2_print = menu_item_2_print->next;
492 }
493 326 }
494
495 /**
496 * @brief Display '>' marker if current row is selected.
497 *
498 * @param row Screen row to draw cursor marker
499 */
500 977 static void display_cursor_marker_if_needed(uint8_t row)
501 {
502 977 DISPLAY->cursor_position(row, CURSOR_COLUMN_POSITION);
503
2/2
✓ Branch 0 taken 326 times.
✓ Branch 1 taken 651 times.
977 if (row == (cursor_row_position[menu_level] + MENU_VIEW_FIRST_ROW))
504 326 DISPLAY->print_char('>');
505 977 }
506
507 /**
508 * @brief Display the name of a menu item at the given row.
509 *
510 * @param row Screen row to display the item
511 * @param name Menu item name (fallback to "NO NAME" if NULL)
512 */
513 976 static void display_menu_item_name(uint8_t row, const char *name)
514 {
515 976 const char *text_to_print = name;
516
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 976 times.
976 if (text_to_print == NULL)
517 {
518 text_to_print = "NO NAME";
519 }
520
521 976 DISPLAY->cursor_position(row, STRING_START_POSITION);
522 976 DISPLAY->print_string(text_to_print);
523 976 }
524
525 /** @} */ /* end of MenuLib group */
526