 // Ambient/room temperature sensor, usually on main board.
 OTV0P2BASE::RoomTemperatureC16_SHT21 TemperatureC16; // SHT21 impl.
 
-////////////////////////// Actuators
+
+////// ACTUATORS
 
 // DORM1/REV7 direct drive actuator.
 // Singleton implementation/instance.

  OTV0P2BASE::LED_HEATCALL_OFF,
  OTV0P2BASE::LED_HEATCALL_ON_ISR_SAFE);


// Controller's notion/cache of seconds within major cycle.
static uint_fast8_t TIME_LSD;

// 'Elapsed minutes' count of minute/major cycles; cheaper than accessing RTC and not tied to real time.
// Starts at or just above zero (within the first 4-minute cycle) to help avoid collisions between units after mass power-up.
// Wraps at its maximum (0xff) value.
static uint8_t minuteCount;

// Managed JSON stats.
OTV0P2BASE::SimpleStatsRotation<12> ss1; // Configured for maximum different stats.  // FIXME REDUCE AS MUCH AS POSSIBLE


// Previous state of port D pins to help detect changes.
static volatile uint8_t prevStatePD;

    if((changes & SERIALRX_INT_MASK) && !(pins & SERIALRX_INT_MASK)) { OTV0P2BASE::CLI::resetCLIActiveTimer(); }
}


/******************************************************************************
 * LOCAL FUNCTIONS
 *****************************************************************************/

////// PANIC

// Indicate that the system is broken in an obvious way (distress flashing the main LED).
// DOES NOT RETURN.
// 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.
void panic() {
    // Reset radio and go into low-power mode.
    PrimaryRadio.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.
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();
}


////// DIAGNOSTICS

#if 1
// More detailed stack usage output
inline void stackCheck()
{
    // Force restart if SPAM/heap/stack likely corrupt.
    OTV0P2BASE::MemoryChecks::forceResetIfStackOverflow();

    // Print max stack usage each cycle
    const int16_t minsp = OTV0P2BASE::MemoryChecks::getMinSPSpaceBelowStackToEnd();
    const uint8_t location = OTV0P2BASE::MemoryChecks::getLocation();
    OTV0P2BASE::serialPrintAndFlush(F("minsp: "));
    OTV0P2BASE::serialPrintAndFlush(minsp, HEX);
    OTV0P2BASE::serialPrintAndFlush(F(" loc:"));
    OTV0P2BASE::serialPrintAndFlush(location);
    OTV0P2BASE::serialPrintlnAndFlush();

    OTV0P2BASE::MemoryChecks::resetMinSP();
}
#endif

////// SENSORS

// Update sensors with historic/trailing statistics information where needed.
// Should be called at least hourly after all stats have been updated,
// but can also be called whenever the user adjusts settings for example.
void updateSensorsFromStats() {
    // Update with rolling stats to adapt to sensors and local environment...
    // ...and prevailing bias, so may take a while to adjust.
    AmbLight.setTypMinMax(
          eeStats.getByHourStatRTC(OTV0P2BASE::NVByHourByteStatsBase::STATS_SET_AMBLIGHT_BY_HOUR_SMOOTHED),
          eeStats.getMinByHourStat(OTV0P2BASE::NVByHourByteStatsBase::STATS_SET_AMBLIGHT_BY_HOUR_SMOOTHED),
          eeStats.getMaxByHourStat(OTV0P2BASE::NVByHourByteStatsBase::STATS_SET_AMBLIGHT_BY_HOUR_SMOOTHED),
          !tempControl.hasEcoBias());
}


////// CONTROL

// Call this to do an I/O poll if needed; returns true if something useful definitely happened.
// This call should typically take << 1ms at 1MHz CPU.
// Does not change CPU clock speeds, mess with interrupts (other than possible brief blocking), or sleep.
// Should also do nothing that interacts with Serial.
// Limits actual poll rate to something like once every 8ms, unless force is true.
//   * force if true then force full poll on every call (ie do not internally rate-limit)
// Note that radio poll() can be for TX as well as RX activity.
// Not thread-safe, eg not to be called from within an ISR.
// FIXME trying to move into utils (for the time being.)
bool pollIO(const bool force) {
    static volatile uint8_t _pO_lastPoll;
    // Poll RX at most about every ~8ms.
    const uint8_t sct = OTV0P2BASE::getSubCycleTime();
    if(force || (sct != _pO_lastPoll)) {
        _pO_lastPoll = sct;
        // Poll for inbound frames.
        // If RX is not interrupt-driven then
        // there will usually be little time to do this
        // before getting an RX overrun or dropped frame.
        PrimaryRadio.poll();
    }
    return(false);
}

// Run tasks needed at the end of each day (nominal midnight).
// Should be run once at a fixed slot in the last minute of the last hour of each day.
// Will be run after all stats for the current hour have been updated.
static void endOfDayTasks() {
    // Count down the setback lockout if not finished...  (TODO-786, TODO-906)
    OTRadValve::countDownSetbackLockout();
}


////// MESSAGING

// Do bare stats transmission.
// Output should be filtered for items appropriate
