 // Main loop for OpenTRV radiator control.
 // Note: exiting and re-entering can take a little while, handling Arduino background tasks such as serial.
 void loop()
-  {
-    // 
-  // Force restart if SPAM/heap/stack likely corrupt.
-  OTV0P2BASE::MemoryChecks::forceResetIfStackOverflow();
-  // Complain and keep complaining when getting near stack overflow.
-  // TODO: make DEBUG-only when confident all configs OK.
-  const int16_t minsp = OTV0P2BASE::MemoryChecks::getMinSPSpaceBelowStackToEnd();
-  if(minsp < 64) { OTV0P2BASE::serialPrintlnAndFlush(F("!SH")); }
-  
-  // Use the zeroth second in each minute to force extra deep device sleeps/resets, etc.
-  const bool second0 = (0 == TIME_LSD);
-  // Sensor readings are taken late in each minute (where they are taken)
-  // and if possible noise and heat and light should be minimised in this part of each minute to improve readings.
-//  const bool sensorReading30s = (TIME_LSD >= 30);
-  // Sensor readings and (stats transmissions) are nominally on a 4-minute cycle.
-  const uint8_t minuteFrom4 = (minuteCount & 3);
-    
-  // Try if very near to end of cycle and thus causing an overrun.
-  // Conversely, if not true, should have time to safely log outputs, etc.
-  const uint8_t nearOverrunThreshold = OTV0P2BASE::GSCT_MAX - 8; // ~64ms/~32 serial TX chars of grace time...
-
-  // Sleep in low-power mode (waiting for interrupts) until seconds roll.
-  // NOTE: sleep at the top of the loop to minimise timing jitter/delay from Arduino background activity after loop() returns.
-  // DHD20130425: waking up from sleep and getting to start processing below this block may take >10ms.
-  // Ensure that serial I/O is off while sleeping.
-  OTV0P2BASE::powerDownSerial();
-  // Power down most stuff (except radio for hub RX).
-  OTV0P2BASE::minimisePowerWithoutSleep();
-  uint_fast8_t newTLSD;
-  while(TIME_LSD == (newTLSD = OTV0P2BASE::getSecondsLT()))
-    {
-    // Poll I/O and process message incrementally (in this otherwise idle time)
-    // before sleep and on wakeup in case some IO needs further processing now,
-    // eg work was accrued during the previous major slow/outer loop
-    // or the in a previous orbit of this loop sleep or nap was terminated by an I/O interrupt.
-    // May generate output to host on Serial.
-    // Come back and have another go immediately until no work remaining.
-    if(handleQueuedMessages(&Serial, true, &PrimaryRadio)) { continue; }
-
-    // Normal long minimal-power sleep until wake-up interrupt.
-    // Rely on interrupt to force quick loop round to I/O poll.
-    OTV0P2BASE::sleepUntilInt();
-    }
-  TIME_LSD = newTLSD;
-  // Reset and immediately re-prime the RTC-based watchdog.
-  OTV0P2BASE::resetRTCWatchDog();
-  OTV0P2BASE::enableRTCWatchdog(true);
-
-
-  // START LOOP BODY
-  // ===============
-
-  // High-priority UI handing, every other/even second.
-  // Show status if the user changed something significant.
-  // Must take ~300ms or less so as not to run over into next half second if two TXs are done.
-
-  // Handling the UI may have taken a little while, so process I/O a little.
-  handleQueuedMessages(&Serial, true, &PrimaryRadio); // Deal with any pending I/O.
-
-  // DO SCHEDULING
-
-  switch(TIME_LSD) // With V0P2BASE_TWO_S_TICK_RTC_SUPPORT only even seconds are available.
-    {
-    case 0:
-      {
-      // Tasks that must be run every minute.
-      ++minuteCount; // Note simple roll-over to 0 at max value.
-      break;
-      }
-
-    // Churn/reseed PRNG(s) a little to improve unpredictability in use: should be lightweight.
-    case 2: { if(minuteFrom4) { OTV0P2BASE::seedRNG8(minuteCount ^ OTV0P2BASE::getCPUCycleCount(), OTV0P2BASE::_getSubCycleTime(), 0xff); } break; }
-
-    // Periodic transmission of stats if NOT driving a local valve (else stats can be piggybacked onto that).
-    // Randomised somewhat between slots and also within the slot to help avoid collisions.
-    static uint8_t txTick;
-    case 6: { txTick = OTV0P2BASE::randRNG8() & 7; break; } // Pick which of the 8 slots to use.
-    case 56:
-      {
-      // Age errors/warnings.
-      OTV0P2BASE::ErrorReporter.read();
-      break;
-      }
-    }
-  // End-of-loop processing, that may be slow.
-  // Ensure progress on queued messages ahead of slow work.  (TODO-867)
-  handleQueuedMessages(&Serial, true, &PrimaryRadio); // Deal with any pending I/O.
-  }
-
+{
+    // CHECK STACK
+    // ===============
+    // Force restart if SPAM/heap/stack likely corrupt.
+    OTV0P2BASE::MemoryChecks::forceResetIfStackOverflow();
+    // Complain and keep complaining when getting near stack overflow.
+    // TODO: make DEBUG-only when confident all configs OK.
+    const int16_t minsp = OTV0P2BASE::MemoryChecks::getMinSPSpaceBelowStackToEnd();
+    if(minsp < 64) { OTV0P2BASE::serialPrintlnAndFlush(F("!SH")); }
+  
+    // SLEEP
+    // ===============
+    // Sleep in low-power mode (waiting for interrupts) until seconds roll.
+    // NOTE: sleep at the top of the loop to minimise timing jitter/delay from Arduino background activity after loop() returns.
+    // DHD20130425: waking up from sleep and getting to start processing below this block may take >10ms.
+    // Ensure that serial I/O is off while sleeping.
+    OTV0P2BASE::powerDownSerial();
+    // Power down most stuff (except radio for hub RX).
+    OTV0P2BASE::minimisePowerWithoutSleep();
+    uint_fast8_t newTLSD;
+    while(TIME_LSD == (newTLSD = OTV0P2BASE::getSecondsLT())) {
+        // Poll I/O and process message incrementally (in this otherwise idle time)
+        // before sleep and on wakeup in case some IO needs further processing now,
+        // eg work was accrued during the previous major slow/outer loop
+        // or the in a previous orbit of this loop sleep or nap was terminated by an I/O interrupt.
+        // May generate output to host on Serial.
+        // Come back and have another go immediately until no work remaining.
+        if(handleQueuedMessages(&Serial, &PrimaryRadio)) { continue; }
+    
+        // Normal long minimal-power sleep until wake-up interrupt.
+        // Rely on interrupt to force quick loop round to I/O poll.
+        OTV0P2BASE::sleepUntilInt();
+    }
+    TIME_LSD = newTLSD;
+    // Reset and immediately re-prime the RTC-based watchdog.
+    OTV0P2BASE::resetRTCWatchDog();
+    OTV0P2BASE::enableRTCWatchdog(true);
+  
+  
+    // START LOOP BODY
+    // ===============  
+    // Handling the UI may have taken a little while, so process I/O a little.
+    handleQueuedMessages(&Serial, &PrimaryRadio); // Deal with any pending I/O.
+    
+    // High-priority UI handing, every other/even second.
+    // Show status if the user changed something significant.
+    // Must take ~300ms or less so as not to run over into next half second if two TXs are done.
+    switch(TIME_LSD) { // With V0P2BASE_TWO_S_TICK_RTC_SUPPORT only even seconds are available.
+        // Tasks that must be run every minute.
+        // Note simple roll-over to 0 at max value.
+        case 0: { ++minuteCount; break; }
+        // Churn/reseed PRNG(s) a little to improve unpredictability in use: should be lightweight.
+        case 2: { if(minuteCount & 3) { OTV0P2BASE::seedRNG8(minuteCount ^ OTV0P2BASE::getCPUCycleCount(), OTV0P2BASE::_getSubCycleTime(), 0xff); } break; }
+    }
+    
+    // End-of-loop processing, that may be slow.
+    // Ensure progress on queued messages ahead of slow work.  (TODO-867)
+    handleQueuedMessages(&Serial, &PrimaryRadio); // Deal with any pending I/O.
+}
+


// Setup routine: runs once after reset.
void setup()
  {
  //----------- Low level setup
  // Set appropriate low-power states, interrupts, etc, ASAP.
  OTV0P2BASE::powerSetup();

  // IO setup for safety, and to avoid pins floating.
  OTV0P2BASE::IOSetup();
  
  OTV0P2BASE::serialPrintAndFlush(F("\r\nOpenTRV: ")); // Leading CRLF to clear leading junk, eg from bootloader.
    V0p2Base_serialPrintlnBuildVersion();
  
  // Count resets to detect unexpected crashes/restarts.
  const uint8_t oldResetCount = eeprom_read_byte((uint8_t *)V0P2BASE_EE_START_RESET_COUNT);
  eeprom_write_byte((uint8_t *)V0P2BASE_EE_START_RESET_COUNT, 1 + oldResetCount);
  
  // Have 32678Hz clock at least running before going any further.
  // Check that the slow clock is running reasonably OK, and tune the fast one to it.
  if(!::OTV0P2BASE::HWTEST::calibrateInternalOscWithExtOsc()) { panic(F("Xtal")); } // Async clock not running or can't tune.

  //----------- Flash UI
  // Signal that xtal is running AND give it time to settle.
  OTV0P2BASE::sleepLowPowerMs(1000);
  OTV0P2BASE::LED_HEATCALL_OFF();

  OTV0P2BASE::sleepLowPowerMs(PP_OFF_MS); // TODO: use this time to gather entropy.
  OTV0P2BASE::LED_HEATCALL_ON();
  OTV0P2BASE::sleepLowPowerMs(1000); // TODO: use this time to gather entropy.


  //----------- Init Radio
  // Initialise the radio, if configured, ASAP because it can suck a lot of power until properly initialised.
  PrimaryRadio.preinit(NULL);
  // Check that the radio is correctly connected; panic if not...
  if(!PrimaryRadio.configure(nPrimaryRadioChannels, RFM23BConfigs) || !PrimaryRadio.begin()) { panic(F("r1")); }

  // Turn power on for SIM900 with PFET for secondary power control.
  fastDigitalWrite(A3, 0); // todo move into sim900link
  pinMode(A3, OUTPUT);
  // Initialise the radio, if configured, ASAP because it can suck a lot of power until properly initialised.
  SecondaryRadio.preinit(NULL);
  // Check that the radio is correctly connected; panic if not...
  if(!SecondaryRadio.configure(1, &SecondaryRadioConfig) || !SecondaryRadio.begin()) { panic(F("r2")); }

  //----------- Init sensors
  // Collect full set of environmental values before entering loop() in normal mode.
  // This should also help ensure that sensors are properly initialised.
  const int heat = TemperatureC16.read();
  const uint16_t Vcc = Supply_cV.read();

  OTV0P2BASE::seedPRNGs();

  //----------- Ensure has ID
  // Ensure that the unique node ID is set up (mainly on first use).
  // Have one attempt (don't want to stress an already failing EEPROM) to force-reset if not good, then panic.
  // Needs to have had entropy gathered, etc.
  if(!OTV0P2BASE::ensureIDCreated())
    {
    if(!OTV0P2BASE::ensureIDCreated(true)) // Force reset.
      { panic(F("ID")); }
    }

  // Initialised: turn main/heatcall UI LED off.
  OTV0P2BASE::LED_HEATCALL_OFF();

  // Do OpenTRV-specific (late) setup.
  // Radio not listening to start with.
  // Ignore any initial spurious RX interrupts for example.
  PrimaryRadio.listen(false);

  // Set up async edge interrupts.
  ATOMIC_BLOCK (ATOMIC_RESTORESTATE)
    {
    //PCMSK0 = PB; PCINT  0--7    (LEARN1 and Radio)
    //PCMSK1 = PC; PCINT  8--15
    //PCMSK2 = PD; PCINT 16--24   (Serial RX and LEARN2 and MODE and Voice)
    PCICR = 1 ;

    PCMSK0 = RFM23B_INT_MASK;
    }

  PrimaryRadio.listen(true);  // XXX may be switched off and left that way somewhere
  
  // Start local counters in randomised positions to help avoid inter-unit collisions,
  // eg for mains-powered units starting up together after a power cut,
  // but without (eg) breaking any of the logic about what order things will be run first time through.
  // Uses some decent noise to try to start the units separated.
  const uint8_t b = OTV0P2BASE::getSecureRandomByte(); // randRNG8();
  // Start within bottom half of minute (or close to); sensor readings happen in second half.
  OTV0P2BASE::setSeconds(b >> 2);
  // Start anywhere in first 4 minute cycle.
  minuteCount = b & 3;
  // Set appropriate loop() values just before entering it.
  TIME_LSD = OTV0P2BASE::getSecondsLT();
  }

//========================================
// MAIN LOOP

// Tries to turn off most stuff safely that will benefit from doing so, but nothing too complex.
// Tries not to use lots of energy so as to keep distress beacon running for a while.
static void panic()
{
    // Reset radio and go into low-power mode.
    PrimaryRadio.panicShutdown();
    // Reset radio and go into low-power mode.
    SecondaryRadio.panicShutdown();
    // Power down almost everything else...
    OTV0P2BASE::minimisePowerWithoutSleep();
    pinMode(OTV0P2BASE::LED_HEATCALL_L, OUTPUT);
    for( ; ; ) {
        OTV0P2BASE::LED_HEATCALL_ON();
        OTV0P2BASE::nap(WDTO_15MS);
        OTV0P2BASE::LED_HEATCALL_OFF();
        OTV0P2BASE::nap(WDTO_120MS);
    }
}
// Panic with fixed message.
static void panic(const __FlashStringHelper *s)
{
    OTV0P2BASE::serialPrintlnAndFlush(); // Start new line to highlight error.  // May fail.
    OTV0P2BASE::serialPrintAndFlush('!'); // Indicate error with leading '!' // May fail.
    OTV0P2BASE::serialPrintlnAndFlush(s); // Print supplied detail text. // May fail.
    panic();
}

// Returns true on successful frame type match, false if no suitable frame was found/decoded and another parser should be tried.
static void decodeAndHandleOTSecureableFrame(const uint8_t * const msg)
{
    const uint8_t msglen = msg[-1];
    if(msglen < 2) { return; } // Too short to be useful, so ignore.
    const uint8_t firstByte = msg[0];
  
    // Validate structure of header/frame first.
    // This is quick and checks for insane/dangerous values throughout.
    OTRadioLink::SecurableFrameHeader sfh;
    const uint8_t l = sfh.checkAndDecodeSmallFrameHeader(msg-1, msglen+1);
    // If isOK flag is set false for any reason, frame is broken/unsafe/unauth.
    bool isOK = (l > 0);
    // If failed this early and this badly, let someone else try parsing the message buffer...
    if(!isOK) { return; }
  
    // Buffer for receiving secure frame body.
    // (Non-secure frame bodies should be read directly from the frame buffer.)
    uint8_t secBodyBuf[OTRadioLink::ENC_BODY_SMALL_FIXED_PTEXT_MAX_SIZE];
    uint8_t decryptedBodyOutSize = 0;
  
    // Validate integrity of frame (CRC for non-secure, auth for secure).
    const bool secureFrame = sfh.isSecure();
    // TODO: validate entire message, eg including auth, or CRC if insecure msg rcvd&allowed.
    // Validate (authenticate) and decrypt body of secure frames.
    uint8_t key[16];
    if(secureFrame && isOK) {
        // Get the 'building' key.
        if(!OTV0P2BASE::getPrimaryBuilding16ByteSecretKey(key)) {
            isOK = false;
            OTV0P2BASE::serialPrintlnAndFlush(F("!RX key"));
        }
    }
    uint8_t senderNodeID[OTV0P2BASE::OpenTRV_Node_ID_Bytes];
    if(secureFrame && isOK) {
        // Look up full ID in associations table,
        // validate RX message counter,
        // authenticate and decrypt,
        // update RX message counter.
        isOK = (0 != OTRadioLink::SimpleSecureFrame32or0BodyRXV0p2::getInstance().decodeSecureSmallFrameSafely(&sfh, msg-1, msglen+1,
                                                OTAESGCM::fixed32BTextSize12BNonce16BTagSimpleDec_DEFAULT_STATELESS,
                                                NULL, key,
                                                secBodyBuf, sizeof(secBodyBuf), decryptedBodyOutSize,
                                                senderNodeID,
                                                true));
        if(!isOK) {
            // Useful brief network diagnostics: a couple of bytes of the claimed ID of rejected frames.
            // Warnings rather than errors because there may legitimately be multiple disjoint networks.
            OTV0P2BASE::serialPrintAndFlush(F("?RX auth")); // Missing association or failed auth.
            if(sfh.getIl() > 0) { OTV0P2BASE::serialPrintAndFlush(' '); OTV0P2BASE::serialPrintAndFlush(sfh.id[0], HEX); }
            if(sfh.getIl() > 1) { OTV0P2BASE::serialPrintAndFlush(' '); OTV0P2BASE::serialPrintAndFlush(sfh.id[1], HEX); }
            OTV0P2BASE::serialPrintlnAndFlush();
        }
    }
    if(!isOK) { return; } // Stop if not OK.
    switch(firstByte) { // Switch on type. XXX
        case 'O' | 0x80: // Basic OpenTRV secure frame...
        {
            if(decryptedBodyOutSize < 2) { break; }
            // If the frame contains JSON stats
            // then forward entire secure frame as-is across the secondary radio relay link,
            // else print directly to console/Serial.
            if((0 != (secBodyBuf[1] & 0x10)) && (decryptedBodyOutSize > 3) && ('{' == secBodyBuf[2]))
                { SecondaryRadio.queueToSend(msg, msglen); }
        }
        // Reject unrecognised type, though fall through potentially to recognise other encodings.
        default: break;
    }
}

// Incrementally process I/O and queued messages, including from the radio link.
// This may mean printing them to Serial (which the passed Print object usually is),
