diff --git a/cc3200/bootmgr/bootloader.mk b/cc3200/bootmgr/bootloader.mk index f8ce0095cc..3105d6666c 100644 --- a/cc3200/bootmgr/bootloader.mk +++ b/cc3200/bootmgr/bootloader.mk @@ -19,6 +19,7 @@ BOOT_CPPDEFINES = -Dgcc -DBOOTLOADER -DTARGET_IS_CC3200 -DSL_TINY BOOT_HAL_SRC_C = $(addprefix hal/,\ cpu.c \ interrupt.c \ + gpio.c \ pin.c \ prcm.c \ shamd5.c \ diff --git a/cc3200/bootmgr/flc.h b/cc3200/bootmgr/flc.h index 7415d1ef61..4b2aca9ac5 100644 --- a/cc3200/bootmgr/flc.h +++ b/cc3200/bootmgr/flc.h @@ -43,7 +43,9 @@ extern "C" *******************************************************************************/ #define IMG_BOOT_INFO "/sys/bootinfo.bin" #define IMG_FACTORY "/sys/factimg.bin" -#define IMG_UPDATE "/sys/updtimg.bin" +#define IMG_UPDATE1 "/sys/updtimg1.bin" +#define IMG_UPDATE2 "/sys/updtimg2.bin" +#define IMG_PREFIX "/sys/updtimg" #define IMG_SRVPACK "/sys/servicepack.ucf" #define SRVPACK_SIGN "/sys/servicepack.sig" @@ -55,7 +57,7 @@ extern "C" /****************************************************************************** Special file sizes *******************************************************************************/ -#define IMG_SIZE (232 * 1024) /* 16KB are reserved for the bootloader and at least 8KB for the heap*/ +#define IMG_SIZE (192 * 1024) /* 16KB are reserved for the bootloader and at least 48KB for the heap*/ #define SRVPACK_SIZE (16 * 1024) #define SIGN_SIZE (2 * 1024) #define CA_KEY_SIZE (4 * 1024) @@ -64,7 +66,8 @@ extern "C" Active Image *******************************************************************************/ #define IMG_ACT_FACTORY 0 -#define IMG_ACT_UPDATE 1 +#define IMG_ACT_UPDATE1 1 +#define IMG_ACT_UPDATE2 2 #define IMG_STATUS_CHECK 0 #define IMG_STATUS_READY 1 @@ -72,13 +75,13 @@ extern "C" /****************************************************************************** Boot Info structure *******************************************************************************/ -typedef struct sBootInfo +typedef struct _sBootInfo_t { _u8 ActiveImg; _u8 Status; + _u8 PrevImg; _u8 : 8; - _u8 : 8; -}sBootInfo_t; +} sBootInfo_t; /****************************************************************************** diff --git a/cc3200/bootmgr/main.c b/cc3200/bootmgr/main.c index 166161dde7..95df47c297 100644 --- a/cc3200/bootmgr/main.c +++ b/cc3200/bootmgr/main.c @@ -64,11 +64,14 @@ #define BOOTMGR_HASH_SIZE 32 #define BOOTMGR_BUFF_SIZE 512 -#define BOOTMGR_WAIT_SAFE_MODE_MS 2400 -#define BOOTMGR_WAIT_SAFE_MODE_TOOGLE_MS 400 +#define BOOTMGR_WAIT_SAFE_MODE_0_MS 3000 +#define BOOTMGR_WAIT_SAFE_MODE_0_BLINK_MS 500 -#define BOOTMGR_SAFE_MODE_ENTER_MS 1600 -#define BOOTMGR_SAFE_MODE_ENTER_TOOGLE_MS 160 +#define BOOTMGR_WAIT_SAFE_MODE_1_MS 3000 +#define BOOTMGR_WAIT_SAFE_MODE_1_BLINK_MS 250 + +#define BOOTMGR_WAIT_SAFE_MODE_2_MS 1500 +#define BOOTMGR_WAIT_SAFE_MODE_2_BLINK_MS 100 //***************************************************************************** // Exported functions declarations @@ -79,9 +82,10 @@ extern void bootmgr_run_app (_u32 base); // Local functions declarations //***************************************************************************** static void bootmgr_board_init (void); -static bool bootmgr_verify (void); +static bool bootmgr_verify (_u8 *image); static void bootmgr_load_and_execute (_u8 *image); -static bool safe_mode_boot (void); +static bool wait_while_blinking (uint32_t wait_time, uint32_t period, bool force_wait); +static void wait_for_safe_boot (sBootInfo_t *psBootInfo); static void bootmgr_image_loader (sBootInfo_t *psBootInfo); //***************************************************************************** @@ -140,14 +144,14 @@ void SimpleLinkSockEventHandler(SlSockEvent_t *pSock) //! Board Initialization & Configuration //***************************************************************************** static void bootmgr_board_init(void) { - // Set vector table base + // set the vector table base MAP_IntVTableBaseSet((unsigned long)&g_pfnVectors[0]); - // Enable Processor Interrupts + // enable processor interrupts MAP_IntMasterEnable(); MAP_IntEnable(FAULT_SYSTICK); - // Mandatory MCU Initialization + // mandatory MCU initialization PRCMCC3200MCUInit(); mperror_bootloader_check_reset_cause(); @@ -157,10 +161,10 @@ static void bootmgr_board_init(void) { antenna_init0(); #endif - // Enable the Data Hashing Engine + // enable the data hashing engine CRYPTOHASH_Init(); - // Init the system led and the system switch + // init the system led and the system switch mperror_init0(); // clear the safe boot flag, since we can't trust its content after reset @@ -170,15 +174,15 @@ static void bootmgr_board_init(void) { //***************************************************************************** //! Verifies the integrity of the new application binary //***************************************************************************** -static bool bootmgr_verify (void) { +static bool bootmgr_verify (_u8 *image) { SlFsFileInfo_t FsFileInfo; _u32 reqlen, offset = 0; _i32 fHandle; // open the file for reading - if (0 == sl_FsOpen((_u8 *)IMG_UPDATE, FS_MODE_OPEN_READ, NULL, &fHandle)) { + if (0 == sl_FsOpen(image, FS_MODE_OPEN_READ, NULL, &fHandle)) { // get the file size - sl_FsGetInfo((_u8 *)IMG_UPDATE, 0, &FsFileInfo); + sl_FsGetInfo(image, 0, &FsFileInfo); if (FsFileInfo.FileLen > BOOTMGR_HASH_SIZE) { FsFileInfo.FileLen -= BOOTMGR_HASH_SIZE; @@ -242,47 +246,72 @@ static void bootmgr_load_and_execute (_u8 *image) { } //***************************************************************************** -//! Check for the safe mode pin +//! Wait while the safe mode pin is being held high and blink the system led +//! with the specified period //***************************************************************************** -static bool safe_mode_boot (void) { - _u32 count = 0; - while (MAP_GPIOPinRead(MICROPY_SAFE_BOOT_PORT, MICROPY_SAFE_BOOT_PORT_PIN) && - ((BOOTMGR_WAIT_SAFE_MODE_TOOGLE_MS * count++) < BOOTMGR_WAIT_SAFE_MODE_MS)) { +static bool wait_while_blinking (uint32_t wait_time, uint32_t period, bool force_wait) { + _u32 count; + for (count = 0; (force_wait || MAP_GPIOPinRead(MICROPY_SAFE_BOOT_PORT, MICROPY_SAFE_BOOT_PORT_PIN)) && + ((period * count) < wait_time); count++) { // toogle the led MAP_GPIOPinWrite(MICROPY_SYS_LED_PORT, MICROPY_SYS_LED_PORT_PIN, ~MAP_GPIOPinRead(MICROPY_SYS_LED_PORT, MICROPY_SYS_LED_PORT_PIN)); - UtilsDelay(UTILS_DELAY_US_TO_COUNT(BOOTMGR_WAIT_SAFE_MODE_TOOGLE_MS * 1000)); + UtilsDelay(UTILS_DELAY_US_TO_COUNT(period * 1000)); } - mperror_deinit_sfe_pin(); return MAP_GPIOPinRead(MICROPY_SAFE_BOOT_PORT, MICROPY_SAFE_BOOT_PORT_PIN) ? true : false; } //***************************************************************************** -//! Load the proper image based on information from boot info and executes it. +//! Check for the safe mode pin +//***************************************************************************** +static void wait_for_safe_boot (sBootInfo_t *psBootInfo) { + if (wait_while_blinking(BOOTMGR_WAIT_SAFE_MODE_0_MS, BOOTMGR_WAIT_SAFE_MODE_0_BLINK_MS, false)) { + // go back one step in time + psBootInfo->ActiveImg = psBootInfo->PrevImg; + if (wait_while_blinking(BOOTMGR_WAIT_SAFE_MODE_1_MS, BOOTMGR_WAIT_SAFE_MODE_1_BLINK_MS, false)) { + // go back directly to the factory image + psBootInfo->ActiveImg = IMG_ACT_FACTORY; + wait_while_blinking(BOOTMGR_WAIT_SAFE_MODE_2_MS, BOOTMGR_WAIT_SAFE_MODE_2_BLINK_MS, true); + } + // turn off the system led + MAP_GPIOPinWrite(MICROPY_SYS_LED_PORT, MICROPY_SYS_LED_PORT_PIN, 0); + // request a safe boot to the application + PRCMRequestSafeBoot(); + } + // uninit the safe boot pin + mperror_deinit_sfe_pin(); +} + +//***************************************************************************** +//! Load the proper image based on the information from the boot info +//! and launch it. //***************************************************************************** static void bootmgr_image_loader(sBootInfo_t *psBootInfo) { _i32 fhandle; - if (safe_mode_boot()) { - _u32 count = 0; - while ((BOOTMGR_SAFE_MODE_ENTER_TOOGLE_MS * count++) < BOOTMGR_SAFE_MODE_ENTER_MS) { - // toogle the led - MAP_GPIOPinWrite(MICROPY_SYS_LED_PORT, MICROPY_SYS_LED_PORT_PIN, ~MAP_GPIOPinRead(MICROPY_SYS_LED_PORT, MICROPY_SYS_LED_PORT_PIN)); - UtilsDelay(UTILS_DELAY_US_TO_COUNT(BOOTMGR_SAFE_MODE_ENTER_TOOGLE_MS * 1000)); - } - psBootInfo->ActiveImg = IMG_ACT_FACTORY; - // turn the led off - MAP_GPIOPinWrite(MICROPY_SYS_LED_PORT, MICROPY_SYS_LED_PORT_PIN, 0); - // request a safe boot to the application - PRCMRequestSafeBoot(); + _u8 *image; + + // search for the active image + switch (psBootInfo->ActiveImg) { + case IMG_ACT_UPDATE1: + image = (unsigned char *)IMG_UPDATE1; + break; + case IMG_ACT_UPDATE2: + image = (unsigned char *)IMG_UPDATE2; + break; + default: + image = (unsigned char *)IMG_FACTORY; + break; } - // do we have a new update image that needs to be verified? - else if ((psBootInfo->ActiveImg == IMG_ACT_UPDATE) && (psBootInfo->Status == IMG_STATUS_CHECK)) { - if (!bootmgr_verify()) { - // delete the corrupted file - sl_FsDel((_u8 *)IMG_UPDATE, 0); - // switch to the factory image - psBootInfo->ActiveImg = IMG_ACT_FACTORY; + + // do we have a new image that needs to be verified? + if ((psBootInfo->ActiveImg != IMG_ACT_FACTORY) && (psBootInfo->Status == IMG_STATUS_CHECK)) { + if (!bootmgr_verify(image)) { + // verification failed, delete the broken file + sl_FsDel(image, 0); + // switch to the previous image + psBootInfo->ActiveImg = psBootInfo->PrevImg; + psBootInfo->PrevImg = IMG_ACT_FACTORY; } - // in any case, set the status as "READY" + // in any case, change the status to "READY" psBootInfo->Status = IMG_STATUS_READY; // write the new boot info if (!sl_FsOpen((unsigned char *)IMG_BOOT_INFO, FS_MODE_OPEN_WRITE, NULL, &fhandle)) { @@ -292,24 +321,34 @@ static void bootmgr_image_loader(sBootInfo_t *psBootInfo) { } } - // now boot the active image - if (IMG_ACT_UPDATE == psBootInfo->ActiveImg) { - bootmgr_load_and_execute((unsigned char *)IMG_UPDATE); - } - else { - bootmgr_load_and_execute((unsigned char *)IMG_FACTORY); + // this one might modify the boot info hence it MUST be called after + // bootmgr_verify! (so that the changes are not saved to flash) + wait_for_safe_boot(psBootInfo); + + // select the active image again, since it might have changed + switch (psBootInfo->ActiveImg) { + case IMG_ACT_UPDATE1: + image = (unsigned char *)IMG_UPDATE1; + break; + case IMG_ACT_UPDATE2: + image = (unsigned char *)IMG_UPDATE2; + break; + default: + image = (unsigned char *)IMG_FACTORY; + break; } + bootmgr_load_and_execute(image); } //***************************************************************************** //! Main function //***************************************************************************** int main (void) { - sBootInfo_t sBootInfo = { .ActiveImg = IMG_ACT_FACTORY, .Status = IMG_STATUS_READY }; + sBootInfo_t sBootInfo = { .ActiveImg = IMG_ACT_FACTORY, .Status = IMG_STATUS_READY, .PrevImg = IMG_ACT_FACTORY }; bool bootapp = false; _i32 fhandle; - // Board Initialization + // board setup bootmgr_board_init(); // start simplelink since we need it to access the sflash @@ -322,12 +361,13 @@ int main (void) { } sl_FsClose(fhandle, 0, 0, 0); } + // boot info file not present (or read failed) if (!bootapp) { // create a new boot info file _u32 BootInfoCreateFlag = _FS_FILE_OPEN_FLAG_COMMIT | _FS_FILE_PUBLIC_WRITE | _FS_FILE_PUBLIC_READ; if (!sl_FsOpen ((unsigned char *)IMG_BOOT_INFO, FS_MODE_OPEN_CREATE((2 * sizeof(sBootInfo_t)), BootInfoCreateFlag), NULL, &fhandle)) { - // Write the default boot info. + // write the default boot info. if (sizeof(sBootInfo_t) == sl_FsWrite(fhandle, 0, (unsigned char *)&sBootInfo, sizeof(sBootInfo_t))) { bootapp = true; } @@ -343,14 +383,14 @@ int main (void) { // stop simplelink sl_Stop(SL_STOP_TIMEOUT); - // if we've reached this point, then it means a fatal error occurred and the application - // could not be loaded, so, loop forever and signal the crash to the user + // if we've reached this point, then it means that a fatal error has occurred and the + // application could not be loaded, so, loop forever and signal the crash to the user while (true) { // keep the bld on MAP_GPIOPinWrite(MICROPY_SYS_LED_PORT, MICROPY_SYS_LED_PORT_PIN, MICROPY_SYS_LED_PORT_PIN); - __asm volatile(" dsb \n" - " isb \n" - " wfi \n"); + __asm volatile(" dsb \n" + " isb \n" + " wfi \n"); } } diff --git a/cc3200/ftp/ftp.c b/cc3200/ftp/ftp.c index 2aba00d539..651cd1ef90 100644 --- a/cc3200/ftp/ftp.c +++ b/cc3200/ftp/ftp.c @@ -764,6 +764,8 @@ static void ftp_process_cmd (void) { ftp_send_reply(150, NULL); } else { + // to unlock the updater + updater_finnish(); ftp_data.state = E_FTP_STE_END_TRANSFER; ftp_send_reply(550, NULL); } diff --git a/cc3200/ftp/updater.c b/cc3200/ftp/updater.c index 974b2f72d7..2f983b16c6 100644 --- a/cc3200/ftp/updater.c +++ b/cc3200/ftp/updater.c @@ -37,6 +37,7 @@ #include "modnetwork.h" #include "modwlan.h" #include "debug.h" +#include "osi.h" /****************************************************************************** DEFINE PRIVATE CONSTANTS @@ -61,15 +62,37 @@ typedef struct { /****************************************************************************** DECLARE PRIVATE DATA ******************************************************************************/ -static updater_data_t updater_data; +static updater_data_t updater_data = { .path = NULL, .fhandle = -1, .fsize = 0, .foffset = 0 }; +static OsiLockObj_t updater_LockObj; +static sBootInfo_t sBootInfo; /****************************************************************************** DEFINE PUBLIC FUNCTIONS ******************************************************************************/ +__attribute__ ((section (".boot"))) +void updater_pre_init (void) { + // create the updater lock + ASSERT(OSI_OK == sl_LockObjCreate(&updater_LockObj, "UpdaterLock")); +} + bool updater_check_path (void *path) { + sl_LockObjLock (&updater_LockObj, SL_OS_WAIT_FOREVER); if (!strcmp(UPDATER_IMG_PATH, path)) { - updater_data.path = IMG_UPDATE; updater_data.fsize = IMG_SIZE; + updater_data.path = IMG_UPDATE1; +// the launchxl doesn't have enough flash space for 2 user update images +#ifdef WIPY + // check which one should be the next active image + _i32 fhandle; + if (!sl_FsOpen((unsigned char *)IMG_BOOT_INFO, FS_MODE_OPEN_READ, NULL, &fhandle)) { + ASSERT (sizeof(sBootInfo_t) == sl_FsRead(fhandle, 0, (unsigned char *)&sBootInfo, sizeof(sBootInfo_t))); + sl_FsClose(fhandle, 0, 0, 0); + if ((sBootInfo.Status == IMG_STATUS_CHECK && sBootInfo.ActiveImg == IMG_ACT_UPDATE2) || + sBootInfo.ActiveImg == IMG_ACT_UPDATE1) { + updater_data.path = IMG_UPDATE2; + } + } +#endif } else if (!strcmp(UPDATER_SRVPACK_PATH, path)) { updater_data.path = IMG_SRVPACK; updater_data.fsize = SRVPACK_SIZE; @@ -86,6 +109,7 @@ bool updater_check_path (void *path) { updater_data.path = KEY_FILE; updater_data.fsize = CA_KEY_SIZE; } else { + sl_LockObjUnlock (&updater_LockObj); return false; } return true; @@ -106,7 +130,6 @@ bool updater_start (void) { result = true; } sl_LockObjUnlock (&wlan_LockObj); - return result; } @@ -124,35 +147,56 @@ bool updater_write (uint8_t *buf, uint32_t len) { } void updater_finnish (void) { - sBootInfo_t sBootInfo; _i32 fhandle; if (updater_data.fhandle > 0) { sl_LockObjLock (&wlan_LockObj, SL_OS_WAIT_FOREVER); // close the file being updated sl_FsClose(updater_data.fhandle, NULL, NULL, 0); - - if (!strcmp (IMG_UPDATE, updater_data.path)) { - // open the boot info file for reading +#ifdef WIPY + // if we still have an image pending for verification, leave the boot info as it is + if (!strncmp(IMG_PREFIX, updater_data.path, strlen(IMG_PREFIX)) && sBootInfo.Status != IMG_STATUS_CHECK) { +#else + if (!strncmp(IMG_PREFIX, updater_data.path, strlen(IMG_PREFIX))) { +#endif +#ifdef DEBUG if (!sl_FsOpen((unsigned char *)IMG_BOOT_INFO, FS_MODE_OPEN_READ, NULL, &fhandle)) { + ASSERT (sizeof(sBootInfo_t) == sl_FsRead(fhandle, 0, (unsigned char *)&sBootInfo, sizeof(sBootInfo_t))); sl_FsClose(fhandle, 0, 0, 0); - // open the file for writing +#endif + // open the boot info file for writing ASSERT (sl_FsOpen((unsigned char *)IMG_BOOT_INFO, FS_MODE_OPEN_WRITE, NULL, &fhandle) == 0); +#ifdef DEBUG } else { + // the boot info file doesn't exist yet _u32 BootInfoCreateFlag = _FS_FILE_OPEN_FLAG_COMMIT | _FS_FILE_PUBLIC_WRITE | _FS_FILE_PUBLIC_READ; ASSERT (sl_FsOpen ((unsigned char *)IMG_BOOT_INFO, FS_MODE_OPEN_CREATE((2 * sizeof(sBootInfo_t)), BootInfoCreateFlag), NULL, &fhandle) == 0); } +#endif - // write the new boot info - sBootInfo.ActiveImg = IMG_ACT_UPDATE; + // save the new boot info +#ifdef WIPY + sBootInfo.PrevImg = sBootInfo.ActiveImg; + if (sBootInfo.ActiveImg == IMG_ACT_UPDATE1) { + sBootInfo.ActiveImg = IMG_ACT_UPDATE2; + } else { + sBootInfo.ActiveImg = IMG_ACT_UPDATE1; + } +// the launchxl doesn't have enough flash space for 2 user updates +#else + sBootInfo.PrevImg = IMG_ACT_FACTORY; + sBootInfo.ActiveImg = IMG_ACT_UPDATE1; +#endif sBootInfo.Status = IMG_STATUS_CHECK; ASSERT (sizeof(sBootInfo_t) == sl_FsWrite(fhandle, 0, (unsigned char *)&sBootInfo, sizeof(sBootInfo_t))); sl_FsClose(fhandle, 0, 0, 0); } sl_LockObjUnlock (&wlan_LockObj); + updater_data.fhandle = -1; } - updater_data.fhandle = -1; + sl_LockObjUnlock (&updater_LockObj); } + diff --git a/cc3200/ftp/updater.h b/cc3200/ftp/updater.h index 518d76c4c7..b581d0fc8a 100644 --- a/cc3200/ftp/updater.h +++ b/cc3200/ftp/updater.h @@ -28,10 +28,11 @@ #ifndef UPDATER_H_ #define UPDATER_H_ -bool updater_check_path (void *path); -bool updater_start (void); -bool updater_write (uint8_t *buf, uint32_t len); -void updater_finnish (void); -bool updater_verify (uint8_t *rbuff, uint8_t *hasbuff); +extern void updater_pre_init (void); +extern bool updater_check_path (void *path); +extern bool updater_start (void); +extern bool updater_write (uint8_t *buf, uint32_t len); +extern void updater_finnish (void); +extern bool updater_verify (uint8_t *rbuff, uint8_t *hasbuff); #endif /* UPDATER_H_ */ diff --git a/cc3200/mptask.c b/cc3200/mptask.c index 152637e158..bae0e56493 100644 --- a/cc3200/mptask.c +++ b/cc3200/mptask.c @@ -67,6 +67,7 @@ #include "pybtimer.h" #include "mpcallback.h" #include "cryptohash.h" +#include "updater.h" /****************************************************************************** DECLARE PRIVATE CONSTANTS @@ -279,6 +280,9 @@ STATIC void mptask_pre_init (void) { // this one allocates memory for the WLAN semaphore wlan_pre_init(); + // this one allocates memory for the updater semaphore + updater_pre_init(); + // this one allocates memory for the Socket semaphore modusocket_pre_init(); diff --git a/docs/wipy/general.rst b/docs/wipy/general.rst index 67f47f21e4..976fb6891f 100644 --- a/docs/wipy/general.rst +++ b/docs/wipy/general.rst @@ -32,15 +32,35 @@ Boot modes ---------- If you power up normally, or press the reset button, the WiPy will boot -into standard mode: the ``boot.py`` file will be executed first, then +into standard mode; the ``boot.py`` file will be executed first, then ``main.py`` will run. You can override this boot sequence by pulling ``GPIO28`` **up** (connect -it to the 3v3 output pin) during reset. The heart beat LED will flash slowly -3 times to signal that safe boot is being requested, and then 4 more times -quickly to let you know that safe boot is going to be performed. While safe -booting, the WiPy runs the factory firmware and skips the execution of -``boot.py`` and ``main.py``. This is useful to recover from any crash situation. +it to the 3v3 output pin) during reset. This procedure also allows going +back in time to old firmware versions. The WiPy can hold up to 3 different +firmware versions, which are: the factory firmware plus 2 user updates. + +After reset, if ``GPIO28`` is held high, the heart beat LED will start flashing +slowly, if after 3 seconds the pin is still being held high, the LED will start +blinking a bit faster and the WiPy will select the previous user update to boot. +If the previous user update is the desired firmware image, ``GPIO28`` must be +released before 3 more seconds elapse. If 3 seconds later the pin is still high, +the factory firmware will be selected, the LED will flash quickly for 1.5 seconds +and the WiPy will proceed to boot. The firmware selection mechanism is as follows: + + +**Safe Boot Pin** ``GPIO28`` **released during:** + ++-------------------------+-------------------------+----------------------------+ +| 1st 3 secs window | 2nd 3 secs window | Final 1.5 secs window | ++=========================+=========================+============================+ +| | Normal boot, *latest* | | Safe boot, *previous* | | Safe boot, the *factory* | +| | firmware is selected | | user update selected | | firmware is selected | ++-------------------------+-------------------------+----------------------------+ + +When selecting a previous firmware version, safe boot mode is entered, meaning +that the execution of both ``boot.py`` and ``main.py`` is skipped. This is +useful to recover from crash situations caused by the user scripts. The heart beat LED ------------------