freitag
This commit is contained in:
@@ -99,7 +99,7 @@ Verify access: `test -w /dev/ttyUSB0 && echo ok`
|
|||||||
autotune 50
|
autotune 50
|
||||||
```
|
```
|
||||||
|
|
||||||
Emergency cutoff is fixed at **70°C** — you can autotune at 50–55°C with ABS in the chamber while hot corners stay below that.
|
Cutoff follows target (`target + 12°C`, max 95°C) — e.g. ABS at 55°C trips at 67°C corner, nylon at 80°C at 92°C.
|
||||||
|
|
||||||
5. Dry at your target — fan runs at stir PWM (default **178**) whenever target > 0, including heat-up. Override with `fan <pwm>`; `fan auto` returns to default. Fan auto-off below 40°C applies only in idle (`target 0`).
|
5. Dry at your target — fan runs at stir PWM (default **178**) whenever target > 0, including heat-up. Override with `fan <pwm>`; `fan auto` returns to default. Fan auto-off below 40°C applies only in idle (`target 0`).
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ static const uint8_t SENSOR_COUNT = sizeof(SENSOR_CHANNELS) / sizeof(SENSOR_CHAN
|
|||||||
// SHT31 I2C address (ADDR pin low → 0x44, high → 0x45)
|
// SHT31 I2C address (ADDR pin low → 0x44, high → 0x45)
|
||||||
static const uint8_t SHT31_ADDRESS = 0x44;
|
static const uint8_t SHT31_ADDRESS = 0x44;
|
||||||
|
|
||||||
|
// Bus timeout (Wire.setWireTimeout) — bounds a stuck I2C transaction so a
|
||||||
|
// glitch resets the TWI hardware instead of hanging the whole sketch.
|
||||||
|
static const uint32_t I2C_TIMEOUT_US = 25000UL;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Outputs — D5 has hardware PWM; heater on A2 uses burst control (SSR-friendly)
|
// Outputs — D5 has hardware PWM; heater on A2 uses burst control (SSR-friendly)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -25,9 +29,22 @@ static const float TARGET_TEMP_C = 0.0f;
|
|||||||
static const float AUTOTUNE_DEFAULT_TEMP_C = 40.0f;
|
static const float AUTOTUNE_DEFAULT_TEMP_C = 40.0f;
|
||||||
static const float TARGET_MIN_C = 0.0f;
|
static const float TARGET_MIN_C = 0.0f;
|
||||||
static const float TARGET_MAX_C = 80.0f;
|
static const float TARGET_MAX_C = 80.0f;
|
||||||
// Absolute max corner — heater off + full fan (decoupled from PID target).
|
// Hard ceiling (sensor / enclosure limit). Cutoff when regulating = target + CUTOFF_ABOVE_TARGET_C.
|
||||||
static const float EMERGENCY_MAX_TEMP_C = 70.0f;
|
static const float EMERGENCY_ABSOLUTE_MAX_C = 95.0f;
|
||||||
static const float CORNER_STOP_MARGIN_C = 3.0f;
|
static const float CUTOFF_ABOVE_TARGET_C = 12.0f;
|
||||||
|
static const float CUTOFF_RECOVERY_BAND_C = 5.0f;
|
||||||
|
static const float CORNER_STOP_MARGIN_C = 5.0f;
|
||||||
|
|
||||||
|
inline float emergencyCutoffForTarget(float targetC) {
|
||||||
|
if (targetC <= 0.0f) {
|
||||||
|
return EMERGENCY_ABSOLUTE_MAX_C;
|
||||||
|
}
|
||||||
|
float cutoff = targetC + CUTOFF_ABOVE_TARGET_C;
|
||||||
|
if (cutoff > EMERGENCY_ABSOLUTE_MAX_C) {
|
||||||
|
cutoff = EMERGENCY_ABSOLUTE_MAX_C;
|
||||||
|
}
|
||||||
|
return cutoff;
|
||||||
|
}
|
||||||
|
|
||||||
// Heat PI on average temp (no D term)
|
// Heat PI on average temp (no D term)
|
||||||
static const float HEAT_PI_KP = 4.0f;
|
static const float HEAT_PI_KP = 4.0f;
|
||||||
@@ -42,15 +59,8 @@ static const float PID_KI = HEAT_PI_KI;
|
|||||||
static const float PID_KD = 0.0f;
|
static const float PID_KD = 0.0f;
|
||||||
static const float GOOD_SPREAD_C = 5.0f;
|
static const float GOOD_SPREAD_C = 5.0f;
|
||||||
|
|
||||||
// Tiered heater cap during heat-up (100% until near target)
|
// Corner taper when avg is near target — keeps hottest sensor below emergency
|
||||||
static const float HEATER_MAX_DUTY_COLD = 100.0f;
|
|
||||||
static const float HEATER_MAX_DUTY_MID = 75.0f;
|
|
||||||
static const float HEATER_MAX_DUTY_NEAR = 45.0f;
|
|
||||||
static const float HEATER_COLD_BELOW_C = 10.0f;
|
|
||||||
static const float HEATER_WARM_BELOW_C = 3.0f;
|
|
||||||
// Corner taper only within this band below target (was 10°C — blocked heat-up in uneven chambers)
|
|
||||||
static const float CORNER_LIMIT_BAND_C = 2.0f;
|
static const float CORNER_LIMIT_BAND_C = 2.0f;
|
||||||
static const float HEATER_SLEW_UP_PER_S = 18.0f;
|
|
||||||
static const float MAX_TEMP_HEADROOM_C = 15.0f;
|
static const float MAX_TEMP_HEADROOM_C = 15.0f;
|
||||||
|
|
||||||
static const uint16_t HEATER_CYCLE_MS = 3000;
|
static const uint16_t HEATER_CYCLE_MS = 3000;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ public:
|
|||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTempC >= EMERGENCY_MAX_TEMP_C) {
|
if (maxTempC >= emergencyCutoffForTarget(setpointC_)) {
|
||||||
fail(F("autotune: abort — max sensor at emergency limit"));
|
fail(F("autotune: abort — max sensor at emergency limit"));
|
||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,11 +299,13 @@ public:
|
|||||||
|
|
||||||
bool isIdle() const { return targetTempC_ <= 0.0f; }
|
bool isIdle() const { return targetTempC_ <= 0.0f; }
|
||||||
|
|
||||||
|
float emergencyCutoffC() const { return emergencyCutoffForTarget(targetTempC_); }
|
||||||
|
|
||||||
float cutoffThreshold() const {
|
float cutoffThreshold() const {
|
||||||
if (isIdle()) {
|
if (isIdle()) {
|
||||||
return INFINITY;
|
return INFINITY;
|
||||||
}
|
}
|
||||||
return EMERGENCY_MAX_TEMP_C;
|
return emergencyCutoffC();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCutoffActive() const { return cutoffActive_; }
|
bool isCutoffActive() const { return cutoffActive_; }
|
||||||
@@ -470,7 +472,8 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateRegulating(float avgTempC, float maxTempC, uint32_t nowMs) {
|
void updateRegulating(float avgTempC, float maxTempC, uint32_t nowMs) {
|
||||||
if (maxTempC >= EMERGENCY_MAX_TEMP_C) {
|
const float cutoffC = emergencyCutoffC();
|
||||||
|
if (maxTempC >= cutoffC) {
|
||||||
cutoffActive_ = true;
|
cutoffActive_ = true;
|
||||||
heaterDutyPercent_ = 0.0f;
|
heaterDutyPercent_ = 0.0f;
|
||||||
heaterAllowancePercent_ = 0.0f;
|
heaterAllowancePercent_ = 0.0f;
|
||||||
@@ -480,7 +483,7 @@ private:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cutoffActive_ && maxTempC < EMERGENCY_MAX_TEMP_C - 5.0f) {
|
if (cutoffActive_ && maxTempC < cutoffC - CUTOFF_RECOVERY_BAND_C) {
|
||||||
cutoffActive_ = false;
|
cutoffActive_ = false;
|
||||||
heatPi_.reset();
|
heatPi_.reset();
|
||||||
}
|
}
|
||||||
@@ -494,24 +497,14 @@ private:
|
|||||||
heaterAllowancePercent_ = allowanceFromMaxCorner(maxTempC, avgTempC);
|
heaterAllowancePercent_ = allowanceFromMaxCorner(maxTempC, avgTempC);
|
||||||
|
|
||||||
float duty = heatPi_.compute(avgTempC, nowMs);
|
float duty = heatPi_.compute(avgTempC, nowMs);
|
||||||
const float below = targetTempC_ - avgTempC;
|
duty = clampPercent(duty);
|
||||||
if (below > 8.0f) {
|
|
||||||
const float floor = below > 15.0f ? 75.0f : 60.0f;
|
|
||||||
if (duty < floor) {
|
|
||||||
duty = floor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const float maxDuty = heaterMaxDuty(avgTempC);
|
|
||||||
if (duty > maxDuty) {
|
|
||||||
duty = maxDuty;
|
|
||||||
}
|
|
||||||
if (duty > heaterAllowancePercent_) {
|
if (duty > heaterAllowancePercent_) {
|
||||||
duty = heaterAllowancePercent_;
|
duty = heaterAllowancePercent_;
|
||||||
if (heaterAllowancePercent_ < 100.0f) {
|
if (heaterAllowancePercent_ < 100.0f) {
|
||||||
heaterBlock_ = HeaterBlock::Corner;
|
heaterBlock_ = HeaterBlock::Corner;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
heaterDutyPercent_ = applyHeaterRamp(duty, avgTempC, nowMs);
|
heaterDutyPercent_ = duty;
|
||||||
|
|
||||||
regulatingFanPwm_ = fanManualActive_ ? fanManualPwm_ : stirFanPwm_;
|
regulatingFanPwm_ = fanManualActive_ ? fanManualPwm_ : stirFanPwm_;
|
||||||
}
|
}
|
||||||
@@ -532,16 +525,17 @@ private:
|
|||||||
|
|
||||||
float maxHeatStopTemp(float avgTempC) const {
|
float maxHeatStopTemp(float avgTempC) const {
|
||||||
if (!shouldLimitMaxCorner(avgTempC)) {
|
if (!shouldLimitMaxCorner(avgTempC)) {
|
||||||
return EMERGENCY_MAX_TEMP_C;
|
return emergencyCutoffC();
|
||||||
}
|
}
|
||||||
return EMERGENCY_MAX_TEMP_C - CORNER_STOP_MARGIN_C;
|
return emergencyCutoffC() - CORNER_STOP_MARGIN_C;
|
||||||
}
|
}
|
||||||
|
|
||||||
float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
|
float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
|
||||||
|
const float cutoffC = emergencyCutoffC();
|
||||||
if (!shouldLimitMaxCorner(avgTempC)) {
|
if (!shouldLimitMaxCorner(avgTempC)) {
|
||||||
// Heat-up: only taper when a hot corner nears the emergency ceiling
|
// Heat-up: only taper when a hot corner nears the dynamic cutoff
|
||||||
if (maxTempC >= EMERGENCY_MAX_TEMP_C - 3.0f) {
|
if (maxTempC >= cutoffC - 3.0f) {
|
||||||
const float headroom = EMERGENCY_MAX_TEMP_C - maxTempC;
|
const float headroom = cutoffC - maxTempC;
|
||||||
return clampPercent((headroom / 3.0f) * 100.0f);
|
return clampPercent((headroom / 3.0f) * 100.0f);
|
||||||
}
|
}
|
||||||
return 100.0f;
|
return 100.0f;
|
||||||
@@ -560,38 +554,6 @@ private:
|
|||||||
return clampPercent((headroom / MAX_TEMP_HEADROOM_C) * 100.0f);
|
return clampPercent((headroom / MAX_TEMP_HEADROOM_C) * 100.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
float heaterMaxDuty(float avgTempC) const {
|
|
||||||
if (avgTempC >= targetTempC_) {
|
|
||||||
return HEATER_MAX_DUTY_NEAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float below = targetTempC_ - avgTempC;
|
|
||||||
if (below >= HEATER_COLD_BELOW_C) {
|
|
||||||
return HEATER_MAX_DUTY_COLD;
|
|
||||||
}
|
|
||||||
if (below >= HEATER_WARM_BELOW_C) {
|
|
||||||
return HEATER_MAX_DUTY_MID;
|
|
||||||
}
|
|
||||||
return HEATER_MAX_DUTY_NEAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
float applyHeaterRamp(float requestedDuty, float avgTempC, uint32_t nowMs) {
|
|
||||||
const float maxDuty = heaterMaxDuty(avgTempC);
|
|
||||||
if (requestedDuty > maxDuty) {
|
|
||||||
requestedDuty = maxDuty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastHeaterUpdateMs_ > 0 && requestedDuty > heaterDutyPercent_) {
|
|
||||||
const float dt = static_cast<float>(nowMs - lastHeaterUpdateMs_) / 1000.0f;
|
|
||||||
const float maxUp = heaterDutyPercent_ + HEATER_SLEW_UP_PER_S * dt;
|
|
||||||
if (requestedDuty > maxUp) {
|
|
||||||
requestedDuty = maxUp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestedDuty;
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyHeaterBurst(uint32_t nowMs) {
|
void applyHeaterBurst(uint32_t nowMs) {
|
||||||
if (heaterDutyPercent_ <= 0.0f) {
|
if (heaterDutyPercent_ <= 0.0f) {
|
||||||
forceHeaterOff();
|
forceHeaterOff();
|
||||||
|
|||||||
75127
live/dryer_20260801_181152.csv
Normal file
75127
live/dryer_20260801_181152.csv
Normal file
File diff suppressed because it is too large
Load Diff
7
live/dryer_20260806_190101.csv
Normal file
7
live/dryer_20260806_190101.csv
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
wall_time,ms,target_c,avg_c,min_c,max_c,spread_c,heatlim_pct,heater_pct,fan_pct,cutoff,failsafe,ch2_t,ch2_h,ch3_t,ch3_h,ch4_t,ch4_h,ch5_t,ch5_h
|
||||||
|
2026-08-06T17:01:04+00:00,1786035664603,75.0,35.97,31.92,41.43,8.59,100,100.0,69,0,0,41.4,18,38.4,21,32.1,28,31.9,28
|
||||||
|
2026-08-06T17:01:06+00:00,1786035666605,75.0,37.64,32.08,41.97,9.72,100,100.0,69,0,0,42.0,18,38.9,21,,,32.1,28
|
||||||
|
2026-08-06T17:01:08+00:00,1786035668605,75.0,36.70,32.37,42.46,10.05,100,100.0,69,0,0,42.5,18,39.3,21,32.7,27,32.4,27
|
||||||
|
2026-08-06T17:01:10+00:00,1786035670605,75.0,37.02,32.50,43.01,10.29,100,100.0,69,0,0,43.0,17,39.7,20,32.8,27,32.5,27
|
||||||
|
2026-08-06T17:01:12+00:00,1786035672605,75.0,41.96,40.24,43.68,6.58,100,100.0,69,0,0,43.7,17,40.2,20,,,,
|
||||||
|
2026-08-06T17:01:14+00:00,1786035674607,75.0,37.74,32.97,44.18,10.56,100,100.0,69,0,0,44.2,17,40.6,20,33.2,27,33.0,27
|
||||||
|
@@ -22,6 +22,10 @@ FALLBACK_HEADER = (
|
|||||||
|
|
||||||
SENSOR_CHANNELS = [2, 3, 4, 5]
|
SENSOR_CHANNELS = [2, 3, 4, 5]
|
||||||
|
|
||||||
|
# If no bytes at all arrive for this long, treat the device as hung (e.g. an
|
||||||
|
# I2C bus lockup freezing the Arduino) rather than looping forever in silence.
|
||||||
|
DEFAULT_STALL_TIMEOUT_S = 20.0
|
||||||
|
|
||||||
_FAN_PCT_RE = re.compile(r"\((\d+)%\)")
|
_FAN_PCT_RE = re.compile(r"\((\d+)%\)")
|
||||||
_FAN_PWM_RE = re.compile(r"^(\d+)/")
|
_FAN_PWM_RE = re.compile(r"^(\d+)/")
|
||||||
|
|
||||||
@@ -206,6 +210,11 @@ def enable_dryer_logging(ser, retries: int = 3) -> None:
|
|||||||
print("WARN: did not see 'OK csv logging on' — continuing anyway", file=sys.stderr)
|
print("WARN: did not see 'OK csv logging on' — continuing anyway", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def log_notice(message: str) -> None:
|
||||||
|
stamp = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||||
|
print(f"{stamp} {message}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def cmd_log(args: argparse.Namespace) -> int:
|
def cmd_log(args: argparse.Namespace) -> int:
|
||||||
from dryer_tui import parse_status
|
from dryer_tui import parse_status
|
||||||
|
|
||||||
@@ -214,6 +223,7 @@ def cmd_log(args: argparse.Namespace) -> int:
|
|||||||
if out is None:
|
if out is None:
|
||||||
out = args.log_dir / f"dryer_{datetime.now():%Y%m%d_%H%M%S}.csv"
|
out = args.log_dir / f"dryer_{datetime.now():%Y%m%d_%H%M%S}.csv"
|
||||||
|
|
||||||
|
stall_timeout = args.stall_timeout
|
||||||
print(f"Logging {port} -> {out}", file=sys.stderr)
|
print(f"Logging {port} -> {out}", file=sys.stderr)
|
||||||
if args.auto_log_on:
|
if args.auto_log_on:
|
||||||
print("Will send 'log on' after connect", file=sys.stderr)
|
print("Will send 'log on' after connect", file=sys.stderr)
|
||||||
@@ -223,6 +233,7 @@ def cmd_log(args: argparse.Namespace) -> int:
|
|||||||
if args.auto_log_on:
|
if args.auto_log_on:
|
||||||
enable_dryer_logging(ser)
|
enable_dryer_logging(ser)
|
||||||
|
|
||||||
|
last_activity = time.monotonic()
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
raw = ser.readline()
|
raw = ser.readline()
|
||||||
@@ -230,10 +241,26 @@ def cmd_log(args: argparse.Namespace) -> int:
|
|||||||
print(f"\nStopped ({session.row_count} rows).", file=sys.stderr)
|
print(f"\nStopped ({session.row_count} rows).", file=sys.stderr)
|
||||||
session.close()
|
session.close()
|
||||||
return 0
|
return 0
|
||||||
|
except Exception as exc:
|
||||||
|
log_notice(
|
||||||
|
f"ERROR: serial read failed ({exc}) — closing after "
|
||||||
|
f"{session.row_count} rows"
|
||||||
|
)
|
||||||
|
session.close()
|
||||||
|
return 1
|
||||||
|
|
||||||
if not raw:
|
if not raw:
|
||||||
|
if time.monotonic() - last_activity >= stall_timeout:
|
||||||
|
log_notice(
|
||||||
|
f"WARN: no data from {port} for {stall_timeout:.0f}s — "
|
||||||
|
f"device likely hung (e.g. I2C bus lockup on the Arduino) "
|
||||||
|
f"— closing after {session.row_count} rows"
|
||||||
|
)
|
||||||
|
session.close()
|
||||||
|
return 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
last_activity = time.monotonic()
|
||||||
line = decode_line(raw)
|
line = decode_line(raw)
|
||||||
parsed = parse_status(line)
|
parsed = parse_status(line)
|
||||||
if parsed:
|
if parsed:
|
||||||
@@ -277,6 +304,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
default=True,
|
default=True,
|
||||||
help="Send 'log on' after connect in log mode (default: on)",
|
help="Send 'log on' after connect in log mode (default: on)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--stall-timeout",
|
||||||
|
type=float,
|
||||||
|
default=DEFAULT_STALL_TIMEOUT_S,
|
||||||
|
help=(
|
||||||
|
"log mode: seconds without any data before treating the device as "
|
||||||
|
f"hung and exiting (default: {DEFAULT_STALL_TIMEOUT_S:.0f})"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="action")
|
subparsers = parser.add_subparsers(dest="action")
|
||||||
log_p = subparsers.add_parser("log", help="Headless CSV capture", add_help=False)
|
log_p = subparsers.add_parser("log", help="Headless CSV capture", add_help=False)
|
||||||
@@ -285,6 +321,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
log_p.add_argument("-o", "--output", type=Path)
|
log_p.add_argument("-o", "--output", type=Path)
|
||||||
log_p.add_argument("--log-dir", type=Path, default=Path("logs"))
|
log_p.add_argument("--log-dir", type=Path, default=Path("logs"))
|
||||||
log_p.add_argument("--auto-log-on", action=argparse.BooleanOptionalAction, default=True)
|
log_p.add_argument("--auto-log-on", action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
log_p.add_argument("--stall-timeout", type=float, default=DEFAULT_STALL_TIMEOUT_S)
|
||||||
|
|
||||||
tui_p = subparsers.add_parser("tui", help="Interactive curses dashboard")
|
tui_p = subparsers.add_parser("tui", help="Interactive curses dashboard")
|
||||||
tui_p.add_argument("-p", "--port")
|
tui_p.add_argument("-p", "--port")
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const char *FanCharacterize::phaseName() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool FanCharacterize::start(float maxCornerC, float avgTempC) {
|
bool FanCharacterize::start(float maxCornerC, float avgTempC) {
|
||||||
if (maxCornerC < 45.0f || maxCornerC > EMERGENCY_MAX_TEMP_C - 5.0f) {
|
if (maxCornerC < 45.0f || maxCornerC > EMERGENCY_ABSOLUTE_MAX_C - 5.0f) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ bool FanCharacterize::update(float avgTempC, float maxTempC, float spreadC, uint
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTempC >= EMERGENCY_MAX_TEMP_C) {
|
if (maxTempC >= EMERGENCY_ABSOLUTE_MAX_C) {
|
||||||
fail(F("fanchars: abort limit"));
|
fail(F("fanchars: abort limit"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/main.cpp
10
src/main.cpp
@@ -51,6 +51,9 @@ void readAllSensors() {
|
|||||||
readSensorOnChannel(SENSOR_CHANNELS[i], sensors[i]);
|
readSensorOnChannel(SENSOR_CHANNELS[i], sensors[i]);
|
||||||
}
|
}
|
||||||
mux.disableAll();
|
mux.disableAll();
|
||||||
|
if (Wire.getWireTimeoutFlag()) {
|
||||||
|
Wire.clearWireTimeoutFlag();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float averageValidTemperature() {
|
float averageValidTemperature() {
|
||||||
@@ -437,7 +440,7 @@ void processSerialLine(const char *line) {
|
|||||||
maxC = atof(line + 9);
|
maxC = atof(line + 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxC < 45.0f || maxC > EMERGENCY_MAX_TEMP_C - 5.0f) {
|
if (maxC < 45.0f || maxC > EMERGENCY_ABSOLUTE_MAX_C - 5.0f) {
|
||||||
Serial.println(F("ERR fanchars max 45-65 C"));
|
Serial.println(F("ERR fanchars max 45-65 C"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -521,6 +524,11 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Wire.begin();
|
Wire.begin();
|
||||||
|
// Without a timeout, a glitched I2C transaction (electrical noise, a
|
||||||
|
// momentary bad connection) can hang the AVR's Wire library forever,
|
||||||
|
// freezing the whole sketch. This bounds any transaction and resets the
|
||||||
|
// TWI hardware so the loop keeps running instead of locking up silently.
|
||||||
|
Wire.setWireTimeout(I2C_TIMEOUT_US, true);
|
||||||
|
|
||||||
if (!mux.begin()) {
|
if (!mux.begin()) {
|
||||||
Serial.println(F("ERROR: TCA9548A not found on I2C bus"));
|
Serial.println(F("ERROR: TCA9548A not found on I2C bus"));
|
||||||
|
|||||||
Reference in New Issue
Block a user