/**
 * @brief	If debug enabled, prints a string to serial
 * @todo	Work out what to do about debug config
 * @param	buf		Pointer to a buffer
 */
bool OTNullRadioLink::sendRaw(const uint8_t *buf, uint8_t buflen, int8_t , TXpower , bool )
{
	const char *pBuf = (const char *) buf;
	// print if in debug mode
	V0P2BASE_DEBUG_SERIAL_PRINT_FLASHSTRING("Radio: ");
	for (uint8_t i = 0; i < buflen; i++) {
		V0P2BASE_DEBUG_SERIAL_PRINT(*pBuf);
		pBuf++;
	}
	V0P2BASE_DEBUG_SERIAL_PRINTLN();
	return true;
}
// Computes a new valve position given supplied input state including the current valve position; [0,100].
// Uses no state other than that passed as the arguments (thus unit testable).
// Does not alter any of the input state.
// Uses hysteresis and a proportional control and some other cleverness.
// Is always willing to turn off quickly, but on slowly (AKA "slow start" algorithm),
// and tries to eliminate unnecessary 'hunting' which makes noise and uses actuator energy.
// Nominally called at a regular rate, once per minute.
// All inputState values should be set to sensible values before starting.
// Usually called by tick() which does required state updates afterwards.
uint8_t ModelledRadValveState::computeRequiredTRVPercentOpen(const uint8_t valvePCOpen, const ModelledRadValveInputState &inputState) const
  {
#if 0 && defined(V0P2BASE_DEBUG)
V0P2BASE_DEBUG_SERIAL_PRINT_FLASHSTRING("targ=");
V0P2BASE_DEBUG_SERIAL_PRINT(inputState.targetTempC);
V0P2BASE_DEBUG_SERIAL_PRINT_FLASHSTRING(" room=");
V0P2BASE_DEBUG_SERIAL_PRINT(inputState.refTempC);
V0P2BASE_DEBUG_SERIAL_PRINTLN();
#endif

  // Possibly-adjusted and/or smoothed temperature to use for targeting.
  const int adjustedTempC16 = isFiltering ? (getSmoothedRecent() + refTempOffsetC16) : inputState.refTempC16;
  const int8_t adjustedTempC = (adjustedTempC16 >> 4);

  // (Well) under temp target: open valve up.
  if(adjustedTempC < inputState.targetTempC)
    {
//V0P2BASE_DEBUG_SERIAL_PRINTLN_FLASHSTRING("under temp");
    // Force to fully open in BAKE mode.
    // Need debounced bake mode value to avoid spurious slamming open of the valve as the user cycles through modes.
    if(inputState.inBakeMode) { return(inputState.maxPCOpen); }

    // Avoid trying to heat the outside world when a window or door is opened (TODO-621).
    // This is a short-term tactical response to a persistent cold draught,
    // eg from a window being opened to ventilate a room manually,
    // or a door being left open.
    //
    // BECAUSE not currently very close to target
    // (possibly because of sudden temperature drop already from near target)
    // AND IF system has 'eco' bias (so tries harder to save energy)
    // and the temperature above a minimum frost safety threshold
    // and the temperature is currently falling
    // and the temperature fall over the last few minutes is large
    // THEN attempt to stop calling for heat immediately and continue to turn down
    // (if not inhibited from turning down, in which case avoid opening any further).
    // Turning the valve down should also inhibit reopening it for a little while,
    // even once the temperature has stopped falling.
    //
    // It seems sensible to stop calling for heat immediately if one of these events seems to be happening,
    // though that (a) may not stop the boiler and heat delivery if other rooms are still calling for heat
    // and (b) may prevent the boiler being started again for a while even if this was a false alarm,
    // so may annoy users and make heating control seem erratic,
    // so only do this in 'eco' mode where permission has been given to try harder to save energy.
    if(inputState.hasEcoBias &&
       (adjustedTempC > MIN_VALVE_TARGET_C) &&
       (getRawDelta() < 0) &&
       (getRawDelta(MIN_WINDOW_OPEN_TEMP_FALL_M) <= -(int)MIN_WINDOW_OPEN_TEMP_FALL_C16))
        {
        if(!dontTurndown())
          {
          // Try to turn down far enough to stop calling for heat immediately.
          if(valvePCOpen >= OTRadValve::DEFAULT_VALVE_PC_SAFER_OPEN)
            { return(OTRadValve::DEFAULT_VALVE_PC_SAFER_OPEN - 1); }
          // Else continue to close at a reasonable pace.
          if(valvePCOpen > TRV_MAX_SLEW_PC_PER_MIN)
            { return(valvePCOpen - TRV_MAX_SLEW_PC_PER_MIN); }
          // Else close it.
          return(0);
          }
        // Else at least avoid opening the valve.
        return(valvePCOpen);
        }

    // Limit valve open slew to help minimise overshoot and actuator noise.
    // This should also reduce nugatory setting changes when occupancy (etc) is fluctuating.
    // Thus it may take several minutes to turn the radiator fully on,
    // though probably opening the first third or so will allow near-maximum heat output in practice.
    if(valvePCOpen < inputState.maxPCOpen)
      {
      // Reduce valve hunting: defer re-opening if recently closed.
      if(dontTurnup()) { return(valvePCOpen); }

      // True if a long way below target (more than 1C below target).
      const bool vBelowTarget = (adjustedTempC < inputState.targetTempC-1);

      // Open glacially if explicitly requested or if temperature overshoot has happened or is a danger,
      // or if there's likely no one going to care about getting on target particularly quickly (or would prefer reduced noise).
      //
      // If already at least at the expected minimum % open for significant flow,
      // AND a wide deadband has been allowed by the caller (eg room dark or filtering is on or doing pre-warm)
      //   if not way below target to avoid over-eager pre-warm / anticipation for example (TODO-467)
      //     OR
      //   if filtering is on indicating rapid recent changes or jitter, and the last raw change was upwards,
      // THEN force glacial mode to try to damp oscillations and avoid overshoot and excessive valve movement (TODO-453).
      const bool beGlacial = inputState.glacial ||
          ((valvePCOpen >= inputState.minPCOpen) && inputState.widenDeadband && !inputState.fastResponseRequired &&
              (
#if defined(GLACIAL_ON_WITH_WIDE_DEADBAND)
               // Don't rush to open the valve
               // if neither in comfort mode nor massively below (possibly already setback) target temp.
               (inputState.hasEcoBias && !vBelowTarget) ||
#endif
               // Don't rush to open the valve
               // if temperature is jittery but is moving in the right direction.
               (isFiltering && (getRawDelta() > 0)))); // FIXME: maybe redundant w/ GLACIAL_ON_WITH_WIDE_DEADBAND and widenDeadband set when isFiltering is true
      if(beGlacial) { return(valvePCOpen + 1); }

      // If well below target (and without a wide deadband),
      // or needing a fast response to manual input to be responsive (TODO-593),
      // then jump straight to (just over*) 'moderately open' if less open currently,
      // which should allow flow and turn the boiler on ASAP,
      // a little like a mini-BAKE.
      // For this to work, don't set a wide deadband when, eg, user has just touched the controls.
      // *Jump to just over moderately-open threshold to defeat any small rounding errors in the data path, etc,
      // since boiler is likely to regard this threshold as a trigger to immediate action.
      const uint8_t cappedModeratelyOpen = min(inputState.maxPCOpen, min(99, OTRadValve::DEFAULT_VALVE_PC_MODERATELY_OPEN+TRV_SLEW_PC_PER_MIN_FAST));
      if((valvePCOpen < cappedModeratelyOpen) &&
         (inputState.fastResponseRequired || (vBelowTarget && !inputState.widenDeadband)))
          { return(cappedModeratelyOpen); }

      // Ensure that the valve opens quickly from cold for acceptable response (TODO-593)
      // both locally in terms of valve position and also in terms of the boiler responding.
      // Less fast if already moderately open or with a wide deadband.
      const uint8_t slewRate =
          ((valvePCOpen > OTRadValve::DEFAULT_VALVE_PC_MODERATELY_OPEN) || !inputState.widenDeadband) ?
              TRV_MAX_SLEW_PC_PER_MIN : TRV_SLEW_PC_PER_MIN_VFAST;
      const uint8_t minOpenFromCold = max(slewRate, inputState.minPCOpen);
      // Open to 'minimum' likely open state immediately if less open currently.
      if(valvePCOpen < minOpenFromCold) { return(minOpenFromCold); }
      // Slew open relatively gently...
      return(min((uint8_t)(valvePCOpen + slewRate), inputState.maxPCOpen)); // Capped at maximum.
      }
    // Keep open at maximum allowed.
    return(inputState.maxPCOpen);
    }

  // (Well) over temp target: close valve down.
  if(adjustedTempC > inputState.targetTempC)
    {
//V0P2BASE_DEBUG_SERIAL_PRINTLN_FLASHSTRING("over temp");

    if(0 != valvePCOpen)
      {
      // Reduce valve hunting: defer re-closing if recently opened.
      if(dontTurndown()) { return(valvePCOpen); }

      // True if just above the the proportional range.
      const bool justOverTemp = (adjustedTempC == inputState.targetTempC+1);

      // TODO-453: avoid closing the valve at all when the temperature error is small and falling, and there is a widened deadband.
      if(justOverTemp && inputState.widenDeadband && (getRawDelta() < 0)) { return(valvePCOpen); }

      // TODO-482: glacial close if temperature is jittery and not too far above target.
      if(justOverTemp && isFiltering) { return(valvePCOpen - 1); }

      // Continue shutting valve slowly as not yet fully closed.
      // TODO-117: allow very slow final turn off to help systems with poor bypass, ~1% per minute.
      // Special slow-turn-off rules for final part of travel at/below "min % really open" floor.
      const uint8_t minReallyOpen = inputState.minPCOpen;
      const uint8_t lingerThreshold = (minReallyOpen > 0) ? (minReallyOpen-1) : 0;
      if(valvePCOpen < minReallyOpen)
        {
        // If lingered long enough then do final chunk in one burst to help avoid valve hiss and temperature overshoot.
        if((DEFAULT_MAX_RUN_ON_TIME_M < minReallyOpen) && (valvePCOpen < minReallyOpen - DEFAULT_MAX_RUN_ON_TIME_M))
          { return(0); } // Shut valve completely.
        return(valvePCOpen - 1); // Turn down as slowly as reasonably possible to help boiler cool.
        }

      // TODO-109: with comfort bias close relatively slowly to reduce wasted effort from minor overshoots.
      // TODO-453: close relatively slowly when temperature error is small (<1C) to reduce wasted effort from minor overshoots.
      // TODO-593: if user is manually adjusting device then attempt to respond quickly.
      if(((!inputState.hasEcoBias) || justOverTemp || isFiltering) &&
         (!inputState.fastResponseRequired) &&
         (valvePCOpen > constrain(((int)lingerThreshold) + TRV_SLEW_PC_PER_MIN_FAST, TRV_SLEW_PC_PER_MIN_FAST, inputState.maxPCOpen)))
        { return(valvePCOpen - TRV_SLEW_PC_PER_MIN_FAST); }

      // Else (by default) force to (nearly) off immediately when requested, ie eagerly stop heating to conserve energy.
      // In any case percentage open should now be low enough to stop calling for heat immediately.
      return(lingerThreshold);
      }

    // Ensure that the valve is/remains fully shut.
    return(0);
    }

  // Close to (or at) temp target: set valve partly open to try to tightly regulate.
  //
  // Use currentTempC16 lsbits to set valve percentage for proportional feedback
  // to provide more efficient and quieter TRV drive and probably more stable room temperature.
  // Bigger lsbits value means closer to target from below, so closer to valve off.
  const uint8_t lsbits = (uint8_t) (adjustedTempC16 & 0xf); // LSbits of temperature above base of proportional adjustment range.
//    uint8_t tmp = (uint8_t) (refTempC16 & 0xf); // Only interested in lsbits.
  const uint8_t tmp = 16 - lsbits; // Now in range 1 (at warmest end of 'correct' temperature) to 16 (coolest).
  const uint8_t ulpStep = 6;
  // Get to nominal range 6 to 96, eg valve nearly shut just below top of 'correct' temperature window.
  const uint8_t targetPORaw = tmp * ulpStep;
  // Constrain from below to likely minimum-open value, in part to deal with TODO-117 'linger open' in lieu of boiler bypass.
  // Constrain from above by maximum percentage open allowed, eg for pay-by-volume systems.
  const uint8_t targetPO = constrain(targetPORaw, inputState.minPCOpen, inputState.maxPCOpen);

  // Reduce spurious valve/boiler adjustment by avoiding movement at all unless current temperature error is significant.
  if(targetPO != valvePCOpen)
    {
    // True iff valve needs to be closed somewhat.
    const bool tooOpen = (targetPO < valvePCOpen);
    // Compute the minimum/epsilon slew adjustment allowed (the deadband).
    // Also increase effective deadband if temperature resolution is lower than 1/16th, eg 8ths => 1+2*ulpStep minimum.
// FIXME //    const uint8_t realMinUlp = 1 + (inputState.isLowPrecision ? 2*ulpStep : ulpStep); // Assume precision no coarser than 1/8C.
    const uint8_t realMinUlp = 1 + ulpStep;
    const uint8_t _minAbsSlew = (uint8_t)(inputState.widenDeadband ? max(min(OTRadValve::DEFAULT_VALVE_PC_MODERATELY_OPEN/2,max(TRV_MAX_SLEW_PC_PER_MIN,2*TRV_MIN_SLEW_PC)), 2+TRV_MIN_SLEW_PC) : TRV_MIN_SLEW_PC);
    const uint8_t minAbsSlew = max(realMinUlp, _minAbsSlew);
    if(tooOpen) // Currently open more than required.  Still below target at top of proportional range.
      {
//V0P2BASE_DEBUG_SERIAL_PRINTLN_FLASHSTRING("slightly too open");
      const uint8_t slew = valvePCOpen - targetPO;
      // Ensure no hunting for ~1ulp temperature wobble.
      if(slew < minAbsSlew) { return(valvePCOpen); }

      // Reduce valve hunting: defer re-closing if recently opened.
      if(dontTurndown()) { return(valvePCOpen); }

      // TODO-453: avoid closing the valve at all when the (raw) temperature is not rising, so as to minimise valve movement.
      // Since the target is the top of the proportional range than nothing within it requires the temperature to be *forced* down.
      // Possibly don't apply this rule at the very top of the range in case filtering is on and the filtered value moves differently to the raw.
      if(getRawDelta() <= 0) { return(valvePCOpen); }

      // Close glacially if explicitly requested or if temperature undershoot has happened or is a danger.
      // Also be glacial if in soft setback which aims to allow temperatures to drift passively down a little.
      //   (TODO-451, TODO-467: have darkness only immediately trigger a 'soft setback' using wide deadband)
      // This assumes that most valves more than about 1/3rd open can deliver significant power, esp if not statically balanced.
      // TODO-482: try to deal better with jittery temperature readings.
      const bool beGlacial = inputState.glacial ||
#if defined(GLACIAL_ON_WITH_WIDE_DEADBAND)
          ((inputState.widenDeadband || isFiltering) && (valvePCOpen <= OTRadValve::DEFAULT_VALVE_PC_MODERATELY_OPEN)) ||
#endif
          (lsbits < 8);
      if(beGlacial) { return(valvePCOpen - 1); }

      if(slew > TRV_SLEW_PC_PER_MIN_FAST)
          { return(valvePCOpen - TRV_SLEW_PC_PER_MIN_FAST); } // Cap slew rate.
      // Adjust directly to target.
      return(targetPO);
      }

    // if(targetPO > TRVPercentOpen) // Currently open less than required.  Still below target at top of proportional range.
//V0P2BASE_DEBUG_SERIAL_PRINTLN_FLASHSTRING("slightly too closed");
    // If room is well below target and in BAKE mode then immediately open to maximum.
    // Needs debounced bake mode value to avoid spuriously slamming open the valve as the user cycles through modes.
    if(inputState.inBakeMode) { return(inputState.maxPCOpen); }

    const uint8_t slew = targetPO - valvePCOpen;
    // To to avoid hunting around boundaries of a ~1ulp temperature step.
    if(slew < minAbsSlew) { return(valvePCOpen); }

    // Reduce valve hunting: defer re-opening if recently closed.
    if(dontTurnup()) { return(valvePCOpen); }

    // TODO-453: minimise valve movement (and thus noise and battery use).
    // Keeping the temperature steady anywhere in the target proportional range
    // while minimising valve movement/noise/etc is a good goal,
    // so if raw temperatures are rising at the moment then leave the valve as-is.
    // If fairly near the final target then also leave the valve as-is (TODO-453 & TODO-451).
    const int rise = getRawDelta();
    if(rise > 0) { return(valvePCOpen); }
    if( /* (0 == rise) && */ (lsbits >= (inputState.widenDeadband ? 8 : 12))) { return(valvePCOpen); }

    // Open glacially if explicitly requested or if temperature overshoot has happened or is a danger.
    // Also be glacial if in soft setback which aims to allow temperatures to drift passively down a little.
    //   (TODO-451, TODO-467: have darkness only immediately trigger a 'soft setback' using wide deadband)
    // This assumes that most valves more than about 1/3rd open can deliver significant power, esp if not statically balanced.
    const bool beGlacial = inputState.glacial ||
#if defined(GLACIAL_ON_WITH_WIDE_DEADBAND)
        inputState.widenDeadband ||
#endif
        (lsbits >= 8) || ((lsbits >= 4) && (valvePCOpen > OTRadValve::DEFAULT_VALVE_PC_MODERATELY_OPEN));
    if(beGlacial) { return(valvePCOpen + 1); }

    // Slew open faster with comfort bias.  (Or with explicit request? inputState.fastResponseRequired TODO-593)
    const uint8_t maxSlew = (!inputState.hasEcoBias) ? TRV_SLEW_PC_PER_MIN_FAST : TRV_MAX_SLEW_PC_PER_MIN;
    if(slew > maxSlew)
        { return(valvePCOpen + maxSlew); } // Cap slew rate open.
    // Adjust directly to target.
    return(targetPO);
    }

  // Leave value position as was...
  return(valvePCOpen);
  }
Example #3
0
uint8_t noisyADCRead(const bool /*powerUpIO*/)
  {
  const bool neededEnable = powerUpADCIfDisabled();
#ifndef IGNORE_POWERUPIO
  const bool poweredUpIO = powerUpIO;
  if(powerUpIO) { power_intermittent_peripherals_enable(false); }
#endif
  // Sample supply voltage.
  ADMUX = _BV(REFS0) | 14; // Bandgap vs Vcc.
  ADCSRB = 0; // Enable free-running mode.
  bitWrite(ADCSRA, ADATE, 0); // Multiple samples NOT required.
  ADC_complete = false;
  bitSet(ADCSRA, ADIE); // Turn on ADC interrupt.
  bitSet(ADCSRA, ADSC); // Start conversion.
  uint8_t count = 0;
  while(!ADC_complete) { ++count; } // Busy wait while 'timing' the ADC conversion.
  const uint8_t l1 = ADCL; // Capture the low byte and latch the high byte.
  const uint8_t h1 = ADCH; // Capture the high byte.
#if 0 && defined(V0P2BASE_DEBUG)
  V0P2BASE_DEBUG_SERIAL_PRINT_FLASHSTRING("NAR V: ");
  V0P2BASE_DEBUG_SERIAL_PRINTFMT(h1, HEX);
  V0P2BASE_DEBUG_SERIAL_PRINT(' ');
  V0P2BASE_DEBUG_SERIAL_PRINTFMT(l1, HEX);
  V0P2BASE_DEBUG_SERIAL_PRINTLN();
#endif
  // Sample internal temperature.
  ADMUX = _BV(REFS1) | _BV(REFS0) | _BV(MUX3); // Temp vs bandgap.
  ADC_complete = false;
  bitSet(ADCSRA, ADSC); // Start conversion.
  while(!ADC_complete) { ++count; } // Busy wait while 'timing' the ADC conversion.
  const uint8_t l2 = ADCL; // Capture the low byte and latch the high byte.
  const uint8_t h2 = ADCH; // Capture the high byte.
#if 0 && defined(V0P2BASE_DEBUG)
  V0P2BASE_DEBUG_SERIAL_PRINT_FLASHSTRING("NAR T: ");
  V0P2BASE_DEBUG_SERIAL_PRINTFMT(h2, HEX);
  V0P2BASE_DEBUG_SERIAL_PRINT(' ');
  V0P2BASE_DEBUG_SERIAL_PRINTFMT(l2, HEX);
  V0P2BASE_DEBUG_SERIAL_PRINTLN();
#endif
  uint8_t result = (h1 << 5) ^ (l2) ^ (h2 << 3) ^ count;
#if defined(CATCH_OTHER_NOISE_DURING_NAR)
  result = _crc_ibutton_update(_adcNoise++, result);
#endif
  // Sample all possible ADC inputs relative to Vcc, whatever the inputs may be connected to.
  // Assumed never to do any harm, eg physical damage, nor to disturb I/O setup.
  for(uint8_t i = 0; i < 8; ++i)
    {
    ADMUX = (i & 7) | (DEFAULT << 6); // Switching MUX after sample has started may add further noise.
    ADC_complete = false;
    bitSet(ADCSRA, ADSC); // Start conversion.
    while(!ADC_complete) { ++count; }
    const uint8_t l = ADCL; // Capture the low byte and latch the high byte.
    const uint8_t h = ADCH; // Capture the high byte.
#if 0 && defined(V0P2BASE_DEBUG)
    V0P2BASE_DEBUG_SERIAL_PRINT_FLASHSTRING("NAR M: ");
    V0P2BASE_DEBUG_SERIAL_PRINTFMT(h, HEX);
    V0P2BASE_DEBUG_SERIAL_PRINT(' ');
    V0P2BASE_DEBUG_SERIAL_PRINTFMT(l, HEX);
    V0P2BASE_DEBUG_SERIAL_PRINT(' ');
    V0P2BASE_DEBUG_SERIAL_PRINT(count);
    V0P2BASE_DEBUG_SERIAL_PRINTLN();
#endif
    result = _crc_ibutton_update(result ^ h, l ^ count); // A thorough hash.
#if 0 && defined(V0P2BASE_DEBUG)
    V0P2BASE_DEBUG_SERIAL_PRINT_FLASHSTRING("NAR R: ");
    V0P2BASE_DEBUG_SERIAL_PRINTFMT(result, HEX);
    V0P2BASE_DEBUG_SERIAL_PRINTLN();
#endif
    }
  bitClear(ADCSRA, ADIE); // Turn off ADC interrupt.
  bitClear(ADCSRA, ADATE); // Turn off ADC auto-trigger.
#ifndef IGNORE_POWERUPIO
  if(poweredUpIO) { power_intermittent_peripherals_disable(); }
#endif
  if(neededEnable) { powerDownADC(); }
  result ^= l1; // Ensure that the Vcc raw lsbs get directly folded in to the final result.
#if 0 && defined(V0P2BASE_DEBUG)
  V0P2BASE_DEBUG_SERIAL_PRINT_FLASHSTRING("NAR: ");
  V0P2BASE_DEBUG_SERIAL_PRINTFMT(result, HEX);
  V0P2BASE_DEBUG_SERIAL_PRINTLN();
#endif
  return(result); // Use all the bits collected.
  }