]> git.saurik.com Git - apple/boot.git/blob - i386/boot2/options.c
boot-122.tar.gz
[apple/boot.git] / i386 / boot2 / options.c
1 /*
2 * Copyright (c) 1999-2004 Apple Computer, Inc. All rights reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * Portions Copyright (c) 1999-2004 Apple Computer, Inc. All Rights
7 * Reserved. This file contains Original Code and/or Modifications of
8 * Original Code as defined in and that are subject to the Apple Public
9 * Source License Version 2.0 (the "License"). You may not use this file
10 * except in compliance with the License. Please obtain a copy of the
11 * License at http://www.apple.com/publicsource and read it before using
12 * this file.
13 *
14 * The Original Code and all software distributed under the License are
15 * distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER
16 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
17 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE OR NON- INFRINGEMENT. Please see the
19 * License for the specific language governing rights and limitations
20 * under the License.
21 *
22 * @APPLE_LICENSE_HEADER_END@
23 */
24
25 #include "boot.h"
26 #include "bootstruct.h"
27 #include "fdisk.h"
28
29 enum {
30 kReturnKey = 0x0d,
31 kEscapeKey = 0x1b,
32 kBackspaceKey = 0x08,
33 kASCIIKeyMask = 0x7f
34 };
35
36 enum {
37 kMenuTopRow = 5,
38 kMenuMaxItems = 6,
39 kScreenLastRow = 24
40 };
41
42 static void showHelp();
43
44 //==========================================================================
45
46 enum {
47 kCursorTypeHidden = 0x0100,
48 kCursorTypeUnderline = 0x0607
49 };
50
51 typedef struct {
52 int x;
53 int y;
54 int type;
55 } CursorState;
56
57 static void changeCursor( int col, int row, int type, CursorState * cs )
58 {
59 if (cs) getCursorPositionAndType( &cs->x, &cs->y, &cs->type );
60 setCursorType( type );
61 setCursorPosition( col, row, 0 );
62 }
63
64 static void moveCursor( int col, int row )
65 {
66 setCursorPosition( col, row, 0 );
67 }
68
69 static void restoreCursor( const CursorState * cs )
70 {
71 setCursorPosition( cs->x, cs->y, 0 );
72 setCursorType( cs->type );
73 }
74
75 //==========================================================================
76
77 /* Flush keyboard buffer; returns TRUE if any of the flushed
78 * characters was F8.
79 */
80
81 static BOOL flushKeyboardBuffer()
82 {
83 BOOL status = FALSE;
84
85 while ( readKeyboardStatus() ) {
86 if (bgetc() == 0x4200) status = TRUE;
87 }
88 return status;
89 }
90
91 //==========================================================================
92
93 static int countdown( const char * msg, int row, int timeout )
94 {
95 unsigned long time;
96 int ch = 0;
97 int col = strlen(msg) + 1;
98
99 flushKeyboardBuffer();
100
101 moveCursor( 0, row );
102 printf(msg);
103
104 for ( time = time18(), timeout++; timeout; )
105 {
106 if (ch = readKeyboardStatus())
107 break;
108
109 // Count can be interrupted by holding down shift,
110 // control or alt key
111 if ( ( readKeyboardShiftFlags() & 0x0F ) != 0 ) {
112 ch = 1;
113 break;
114 }
115
116 if ( time18() >= time )
117 {
118 time += 18;
119 timeout--;
120 moveCursor( col, row );
121 printf("(%d) ", timeout);
122 }
123 }
124
125 flushKeyboardBuffer();
126
127 return ch;
128 }
129
130 //==========================================================================
131
132 static char gBootArgs[BOOT_STRING_LEN];
133 static char * gBootArgsPtr = gBootArgs;
134 static char * gBootArgsEnd = gBootArgs + BOOT_STRING_LEN - 1;
135
136 static void clearBootArgs()
137 {
138 gBootArgsPtr = gBootArgs;
139 memset( gBootArgs, '\0', BOOT_STRING_LEN );
140 }
141
142 //==========================================================================
143
144 static void showBootPrompt( int row, BOOL visible )
145 {
146 extern char bootPrompt[];
147
148 changeCursor( 0, row, kCursorTypeUnderline, 0 );
149 clearScreenRows( row, kScreenLastRow );
150 clearBootArgs();
151
152 if ( visible )
153 {
154 printf( bootPrompt );
155 }
156 else
157 {
158 printf("Press Enter to start up the foreign OS. ");
159 }
160 }
161
162 //==========================================================================
163
164 static void updateBootArgs( int key )
165 {
166 key &= kASCIIKeyMask;
167
168 switch ( key )
169 {
170 case kBackspaceKey:
171 if ( gBootArgsPtr > gBootArgs )
172 {
173 int x, y, t;
174 getCursorPositionAndType( &x, &y, &t );
175 if ( x == 0 && y )
176 {
177 x = 80; y--;
178 }
179 if (x) x--;
180 setCursorPosition( x, y, 0 );
181 putca(' ', 0x07, 1);
182 *gBootArgsPtr-- = '\0';
183 }
184 break;
185
186 default:
187 if ( key >= ' ' && gBootArgsPtr < gBootArgsEnd)
188 {
189 putchar(key); // echo to screen
190 *gBootArgsPtr++ = key;
191 }
192 break;
193 }
194 }
195
196 //==========================================================================
197
198 typedef struct {
199 char name[80];
200 void * param;
201 } MenuItem;
202
203 static const MenuItem * gMenuItems = NULL;
204 static int gMenuItemCount;
205 static int gMenuRow;
206 static int gMenuHeight;
207 static int gMenuTop;
208 static int gMenuBottom;
209 static int gMenuSelection;
210
211 static void printMenuItem( const MenuItem * item, int highlight )
212 {
213 printf(" ");
214
215 if ( highlight )
216 putca(' ', 0x70, strlen(item->name) + 4);
217 else
218 putca(' ', 0x07, 40);
219
220 printf(" %40s\n", item->name);
221 }
222
223 //==========================================================================
224
225 static void showMenu( const MenuItem * items, int count,
226 int selection, int row, int height )
227 {
228 int i;
229 CursorState cursorState;
230
231 if ( items == NULL || count == 0 ) return;
232
233 // head and tail points to the start and the end of the list.
234 // top and bottom points to the first and last visible items
235 // in the menu window.
236
237 gMenuItems = items;
238 gMenuRow = row;
239 gMenuHeight = height;
240 gMenuItemCount = count;
241 gMenuTop = 0;
242 gMenuBottom = min( count, height ) - 1;
243 gMenuSelection = selection;
244
245 // If the selected item is not visible, shift the list down.
246
247 if ( gMenuSelection > gMenuBottom )
248 {
249 gMenuTop += ( gMenuSelection - gMenuBottom );
250 gMenuBottom = gMenuSelection;
251 }
252
253 // Draw the visible items.
254
255 changeCursor( 0, row, kCursorTypeHidden, &cursorState );
256
257 for ( i = gMenuTop; i <= gMenuBottom; i++ )
258 {
259 printMenuItem( &items[i], (i == gMenuSelection) );
260 }
261
262 restoreCursor( &cursorState );
263 }
264
265 //==========================================================================
266
267 static int updateMenu( int key, void ** paramPtr )
268 {
269 int moved = 0;
270
271 union {
272 struct {
273 unsigned int
274 selectionUp : 1,
275 selectionDown : 1,
276 scrollUp : 1,
277 scrollDown : 1;
278 } f;
279 unsigned int w;
280 } draw = {{0}};
281
282 if ( NULL == gMenuItems ) return 0;
283
284 // Look at the scan code.
285
286 switch ( key )
287 {
288 case 0x4800: // Up Arrow
289 if ( gMenuSelection != gMenuTop )
290 draw.f.selectionUp = 1;
291 else if ( gMenuTop > 0 )
292 draw.f.scrollDown = 1;
293 break;
294
295 case 0x5000: // Down Arrow
296 if ( gMenuSelection != gMenuBottom )
297 draw.f.selectionDown = 1;
298 else if ( gMenuBottom < (gMenuItemCount - 1) )
299 draw.f.scrollUp = 1;
300 break;
301 }
302
303 if ( draw.w )
304 {
305 if ( draw.f.scrollUp )
306 {
307 scollPage(0, gMenuRow, 40, gMenuRow + gMenuHeight - 1, 0x07, 1, 1);
308 gMenuTop++; gMenuBottom++;
309 draw.f.selectionDown = 1;
310 }
311
312 if ( draw.f.scrollDown )
313 {
314 scollPage(0, gMenuRow, 40, gMenuRow + gMenuHeight - 1, 0x07, 1, -1);
315 gMenuTop--; gMenuBottom--;
316 draw.f.selectionUp = 1;
317 }
318
319 if ( draw.f.selectionUp || draw.f.selectionDown )
320 {
321 CursorState cursorState;
322
323 // Set cursor at current position, and clear inverse video.
324
325 changeCursor( 0, gMenuRow + gMenuSelection - gMenuTop,
326 kCursorTypeHidden, &cursorState );
327
328 printMenuItem( &gMenuItems[gMenuSelection], 0 );
329
330 if ( draw.f.selectionUp ) gMenuSelection--;
331 else gMenuSelection++;
332
333 moveCursor( 0, gMenuRow + gMenuSelection - gMenuTop );
334
335 printMenuItem( &gMenuItems[gMenuSelection], 1 );
336
337 restoreCursor( &cursorState );
338 }
339
340 *paramPtr = gMenuItems[gMenuSelection].param;
341 moved = 1;
342 }
343
344 return moved;
345 }
346
347 //==========================================================================
348
349 static void skipblanks( const char ** cpp )
350 {
351 while ( **(cpp) == ' ' || **(cpp) == '\t' ) ++(*cpp);
352 }
353
354 //==========================================================================
355
356 static const char * extractKernelName( char ** cpp )
357 {
358 char * kn = *cpp;
359 char * cp = *cpp;
360 char c;
361
362 // Convert char to lower case.
363
364 c = *cp | 0x20;
365
366 // Must start with a letter or a '/'.
367
368 if ( (c < 'a' || c > 'z') && ( c != '/' ) )
369 return 0;
370
371 // Keep consuming characters until we hit a separator.
372
373 while ( *cp && (*cp != '=') && (*cp != ' ') && (*cp != '\t') )
374 cp++;
375
376 // Only SPACE or TAB separator is accepted.
377 // Reject everything else.
378
379 if (*cp == '=')
380 return 0;
381
382 // Overwrite the separator, and move the pointer past
383 // the kernel name.
384
385 if (*cp != '\0') *cp++ = '\0';
386 *cpp = cp;
387
388 return kn;
389 }
390
391 //==========================================================================
392
393 static void
394 printMemoryInfo(void)
395 {
396 int line;
397 int i;
398 MemoryRange *mp = bootArgs->memoryMap;
399
400 // Activate and clear page 1
401 setActiveDisplayPage(1);
402 clearScreenRows(0, 24);
403 setCursorPosition( 0, 0, 1 );
404
405 printf("BIOS reported memory ranges:\n");
406 line = 1;
407 for (i=0; i<bootArgs->memoryMapCount; i++) {
408 printf("Base 0x%08x%08x, ",
409 (unsigned long)(mp->base >> 32),
410 (unsigned long)(mp->base));
411 printf("length 0x%08x%08x, type %d\n",
412 (unsigned long)(mp->length >> 32),
413 (unsigned long)(mp->length),
414 mp->type);
415 if (line++ > 20) {
416 printf("(Press a key to continue...)");
417 getc();
418 line = 0;
419 }
420 mp++;
421 }
422 if (line > 0) {
423 printf("(Press a key to continue...)");
424 getc();
425 }
426
427 setActiveDisplayPage(0);
428 }
429
430 //==========================================================================
431
432 int
433 getBootOptions(BOOL firstRun)
434 {
435 int i;
436 int key;
437 int selectIndex = 0;
438 int bvCount;
439 int nextRow;
440 int timeout;
441 BVRef bvr;
442 BVRef bvChain;
443 BVRef menuBVR;
444 BOOL showPrompt, newShowPrompt;
445 MenuItem * menuItems = NULL;
446
447 // Allow user to override default timeout.
448
449 if ( getIntForKey(kTimeoutKey, &timeout) == NO )
450 {
451 timeout = kBootTimeout;
452 }
453
454 // If the user is holding down a shift key,
455 // abort quiet mode.
456 if ( ( readKeyboardShiftFlags() & 0x0F ) != 0 ) {
457 gBootMode &= ~kBootModeQuiet;
458 }
459
460 // If user typed F8, abort quiet mode,
461 // and display the menu.
462 if (flushKeyboardBuffer()) {
463 gBootMode &= ~kBootModeQuiet;
464 timeout = 0;
465 }
466
467 clearBootArgs();
468
469 setCursorPosition( 0, 0, 0 );
470 clearScreenRows( 0, kScreenLastRow );
471 if ( ! ( gBootMode & kBootModeQuiet ) ) {
472 // Display banner and show hardware info.
473 printf( bootBanner, (bootArgs->convmem + bootArgs->extmem) / 1024 );
474 printVBEInfo();
475 }
476
477 changeCursor( 0, kMenuTopRow, kCursorTypeUnderline, 0 );
478
479 verbose("Scanning device %x...", gBIOSDev);
480
481 // Get a list of bootable volumes on the device.
482
483 bvChain = scanBootVolumes( gBIOSDev, &bvCount );
484 gBootVolume = menuBVR = selectBootVolume( bvChain );
485
486 #if 0
487 // When booting from CD (via HD emulation), default to hard
488 // drive boot when possible.
489
490 if ( gBootVolume->part_type == FDISK_BOOTER &&
491 gBootVolume->biosdev == 0x80 )
492 {
493 // Scan the original device 0x80 that has been displaced
494 // by the CD-ROM.
495
496 BVRef hd_bvr = selectBootVolume(scanBootVolumes(0x81, 0));
497 if ( hd_bvr->flags & kBVFlagNativeBoot )
498 {
499 int key = countdown("Press C to start up from CD-ROM.",
500 kMenuTopRow, 5);
501
502 if ( (key & 0x5f) != 'c' )
503 {
504 gBootVolume = hd_bvr;
505 gBIOSDev = hd_bvr->biosdev;
506 initKernBootStruct( gBIOSDev );
507 goto done;
508 }
509 }
510 }
511 #endif
512
513 if ( gBootMode & kBootModeQuiet )
514 {
515 // No input allowed from user.
516 goto done;
517 }
518
519 if ( firstRun && ( timeout > 0 ) &&
520 ( countdown("Press any key to enter startup options.",
521 kMenuTopRow, timeout) == 0 ) )
522 {
523 goto done;
524 }
525
526 if ( bvCount )
527 {
528 // Allocate memory for an array of menu items.
529
530 menuItems = (MenuItem *) malloc( sizeof(MenuItem) * bvCount );
531 if ( menuItems == NULL ) goto done;
532
533 // Associate a menu item for each BVRef.
534
535 for ( bvr = bvChain, i = bvCount - 1, selectIndex = 0;
536 bvr; bvr = bvr->next, i-- )
537 {
538 getBootVolumeDescription( bvr, menuItems[i].name, 80, YES );
539 menuItems[i].param = (void *) bvr;
540 if ( bvr == menuBVR ) selectIndex = i;
541 }
542 }
543
544 // Clear screen and hide the blinking cursor.
545
546 clearScreenRows( kMenuTopRow, kMenuTopRow + 2 );
547 changeCursor( 0, kMenuTopRow, kCursorTypeHidden, 0 );
548 nextRow = kMenuTopRow;
549 showPrompt = YES;
550
551 // Show the menu.
552
553 if ( bvCount )
554 {
555 printf("Use \30\31 keys to select the startup volume.");
556 showMenu( menuItems, bvCount, selectIndex, kMenuTopRow + 2, kMenuMaxItems );
557 nextRow += min( bvCount, kMenuMaxItems ) + 3;
558 }
559
560 // Show the boot prompt.
561
562 showPrompt = (bvCount == 0) || (menuBVR->flags & kBVFlagNativeBoot);
563 showBootPrompt( nextRow, showPrompt );
564
565 do {
566 key = getc();
567
568 updateMenu( key, (void **) &menuBVR );
569
570 newShowPrompt = (bvCount == 0) ||
571 (menuBVR->flags & kBVFlagNativeBoot);
572
573 if ( newShowPrompt != showPrompt )
574 {
575 showPrompt = newShowPrompt;
576 showBootPrompt( nextRow, showPrompt );
577 }
578 if ( showPrompt ) updateBootArgs( key );
579
580 switch ( key & kASCIIKeyMask )
581 {
582 case kReturnKey:
583 if ( *gBootArgs == '?' )
584 {
585 if ( strcmp( gBootArgs, "?video" ) == 0 ) {
586 printVBEModeInfo();
587 } else if ( strcmp( gBootArgs, "?memory" ) == 0 ) {
588 printMemoryInfo();
589 } else {
590 showHelp();
591 }
592 key = 0;
593 showBootPrompt( nextRow, showPrompt );
594 break;
595 }
596 gBootVolume = menuBVR;
597 break;
598
599 case kEscapeKey:
600 clearBootArgs();
601 break;
602
603 default:
604 key = 0;
605 }
606 }
607 while ( 0 == key );
608
609 done:
610 firstRun = NO;
611
612 clearScreenRows( kMenuTopRow, kScreenLastRow );
613 changeCursor( 0, kMenuTopRow, kCursorTypeUnderline, 0 );
614
615 if ( menuItems ) free(menuItems);
616
617 return 0;
618 }
619
620 //==========================================================================
621
622 extern unsigned char chainbootdev;
623 extern unsigned char chainbootflag;
624
625 int processBootOptions()
626 {
627 const char * cp = gBootArgs;
628 const char * val = 0;
629 const char * kernel;
630 int cnt;
631 int userCnt;
632 int cntRemaining;
633 char * argP;
634
635 skipblanks( &cp );
636
637 // Update the unit and partition number.
638
639 if ( gBootVolume )
640 {
641 if ( gBootVolume->flags & kBVFlagForeignBoot )
642 {
643 readBootSector( gBootVolume->biosdev, gBootVolume->part_boff,
644 (void *) 0x7c00 );
645
646 //
647 // Setup edx, and signal intention to chain load the
648 // foreign booter.
649 //
650
651 chainbootdev = gBootVolume->biosdev;
652 chainbootflag = 1;
653
654 return 1;
655 }
656
657 bootArgs->kernDev &= ~((B_UNITMASK << B_UNITSHIFT ) |
658 (B_PARTITIONMASK << B_PARTITIONSHIFT));
659
660 bootArgs->kernDev |= MAKEKERNDEV( 0,
661 /* unit */ BIOS_DEV_UNIT(gBootVolume),
662 /* partition */ gBootVolume->part_no );
663 }
664
665 // Load config table specified by the user, or use the default.
666
667 if (getValueForBootKey( cp, "config", &val, &cnt ) == FALSE) {
668 val = 0;
669 cnt = 0;
670 }
671 loadSystemConfig(val, cnt);
672 if ( !sysConfigValid ) return -1;
673
674 // Use the kernel name specified by the user, or fetch the name
675 // in the config table, or use the default if not specified.
676 // Specifying a kernel name on the command line, or specifying
677 // a non-default kernel name in the config file counts as
678 // overriding the kernel, which causes the kernelcache not
679 // to be used.
680
681 gOverrideKernel = NO;
682 if (( kernel = extractKernelName((char **)&cp) )) {
683 strcpy( bootArgs->bootFile, kernel );
684 gOverrideKernel = YES;
685 } else {
686 if ( getValueForKey( kKernelNameKey, &val, &cnt ) ) {
687 strlcpy( bootArgs->bootFile, val, cnt+1 );
688 if (strcmp( bootArgs->bootFile, kDefaultKernel ) != 0) {
689 gOverrideKernel = YES;
690 }
691 } else {
692 strcpy( bootArgs->bootFile, kDefaultKernel );
693 }
694 }
695
696 cntRemaining = BOOT_STRING_LEN - 2; // save 1 for NULL, 1 for space
697
698 // Check to see if we need to specify root device.
699 // If user types "rd=.." on the boot line, it overrides
700 // the boot device key in the boot arguments file.
701 //
702 argP = bootArgs->bootString;
703 if ( getValueForBootKey( cp, kRootDeviceKey, &val, &cnt ) == FALSE &&
704 getValueForKey( kRootDeviceKey, &val, &cnt ) == FALSE ) {
705 if ( getValueForKey( kBootDeviceKey, &val, &cnt ) ) {
706 strcpy( argP, "rd=*" );
707 argP += 4;
708 strlcpy( argP, val, cnt+1);
709 cntRemaining -= cnt;
710 argP += cnt;
711 *argP++ = ' ';
712 }
713 }
714
715 // Check to see if we should ignore saved kernel flags.
716 if (getValueForBootKey(cp, kIgnoreBootFileFlag, &val, &cnt) == FALSE) {
717 if (getValueForKey( kKernelFlagsKey, &val, &cnt ) == FALSE) {
718 val = 0;
719 cnt = 0;
720 }
721 }
722
723 // Store the merged kernel flags and boot args.
724
725 if (cnt > cntRemaining) {
726 error("Warning: boot arguments too long, truncated\n");
727 cnt = cntRemaining;
728 }
729 if (cnt) {
730 strncpy(argP, val, cnt);
731 argP[cnt++] = ' ';
732 }
733 cntRemaining = cntRemaining - cnt;
734 userCnt = strlen(cp);
735 if (userCnt > cntRemaining) {
736 error("Warning: boot arguments too long, truncated\n");
737 userCnt = cntRemaining;
738 }
739 strncpy(&argP[cnt], cp, userCnt);
740 argP[cnt+userCnt] = '\0';
741
742 gVerboseMode = getValueForKey( kVerboseModeFlag, &val, &cnt ) ||
743 getValueForKey( kSingleUserModeFlag, &val, &cnt );
744
745 gBootMode = ( getValueForKey( kSafeModeFlag, &val, &cnt ) ) ?
746 kBootModeSafe : kBootModeNormal;
747
748 if ( getValueForKey( kPlatformKey, &val, &cnt ) ) {
749 strlcpy(gPlatformName, val, cnt + 1);
750 } else {
751 strcpy(gPlatformName, "ACPI");
752 }
753
754 if ( getValueForKey( kMKextCacheKey, &val, &cnt ) ) {
755 strlcpy(gMKextName, val, cnt + 1);
756 }
757
758 return 0;
759 }
760
761 //==========================================================================
762 // Load the help file and display the file contents on the screen.
763
764 static void showHelp()
765 {
766 #define BOOT_HELP_PATH "/usr/standalone/i386/BootHelp.txt"
767
768 int fd;
769 int size;
770 int line;
771 int line_offset;
772 int c;
773
774 if ( (fd = open(BOOT_HELP_PATH, 0)) >= 0 )
775 {
776 char * buffer;
777 char * bp;
778
779 size = file_size(fd);
780 buffer = malloc( size + 1 );
781 read(fd, buffer, size);
782 close(fd);
783
784 bp = buffer;
785 while (size > 0) {
786 while (*bp != '\n') {
787 bp++;
788 size--;
789 }
790 *bp++ = '\0';
791 size--;
792 }
793 *bp = '\1';
794 line_offset = 0;
795
796 setActiveDisplayPage(1);
797
798 while (1) {
799 clearScreenRows(0, 24);
800 setCursorPosition(0, 0, 1);
801 bp = buffer;
802 for (line = 0; *bp != '\1' && line < line_offset; line++) {
803 while (*bp != '\0') bp++;
804 bp++;
805 }
806 for (line = 0; *bp != '\1' && line < 23; line++) {
807 setCursorPosition(0, line, 1);
808 printf("%s\n", bp);
809 while (*bp != '\0') bp++;
810 bp++;
811 }
812
813 setCursorPosition(0, 23, 1);
814 if (*bp == '\1') {
815 printf("[Type %sq or space to quit help]",
816 (line_offset > 0) ? "p for previous page, " : "");
817 } else {
818 printf("[Type %s%sq to quit help]",
819 (line_offset > 0) ? "p for previous page, " : "",
820 (*bp != '\1') ? "space for next page, " : "");
821 }
822
823 c = getc();
824 if (c == 'q' || c == 'Q') {
825 break;
826 }
827 if ((c == 'p' || c == 'P') && line_offset > 0) {
828 line_offset -= 23;
829 }
830 if (c == ' ') {
831 if (*bp == '\1') {
832 break;
833 } else {
834 line_offset += 23;
835 }
836 }
837 }
838
839 free(buffer);
840 setActiveDisplayPage(0);
841 }
842 }