Local preparation candidate. Original functional/timing diagrams and full code are included. See the dated validation/review records for acceptance scope. No publication or hardware result is claimed.

Pico · Humidity, contact temperature and uncertainty

Room Humidity and Window Temperature: Calculate a Condensation Margin

A room can have moderate humidity while one cold patch of glass approaches the dew point. Combine an air measurement with a separately contacted temperature sensor, then calculate a margin interval. The useful result is knowing when the assumptions leave the sign unresolved—not turning one positive number into a promise that the whole window will stay dry.

1. Two different temperatures answer different questions

Relative humidity (RH) compares water-vapour pressure with saturation at the same air temperature. Dew point is the temperature at which that vapour state reaches saturation over liquid water. A condensation margin here means contact-sensor temperature minus estimated air dew point. Positive and negative describe that numerical comparison; they are not building-control commands or proof of visible droplets.

The exact proposed hardware is an original RP2040 Raspberry Pi Pico with official RPI_PICO MicroPython 1.26.0, an Adafruit product identifier (PID) 5665 SHT45 carrier without a membrane, and an Analog Devices DS18B20+ in its three-lead TO-92 through-hole package. Sensirion SHT45-AD1B-R2 is the bare-sensor reference. The carrier's public schematic names the SHT4x family and does not prove a tape-and-reel suffix on an individual board; confirm the supplied carrier and sensor version rather than inferring one from its serial number.

Inter-Integrated Circuit (I2C) uses serial data (SDA) and serial clock (SCL). The DS18B20 uses a separate 1-Wire data line, DQ. Both are open-drain interfaces: a device pulls LOW and a resistor returns the line HIGH. General-purpose input/output (GPIO) names differ from physical Pico pin numbers. A cyclic redundancy check (CRC) detects many transfer errors; it does not establish thermal contact or calibrated accuracy.

Universal Serial Bus (USB) supplies the Pico. Ground (GND) is the common reference; VIN is the carrier's positive input, its 3V pad is an internal regulated output, and VDD/VSS label chip supply and return. JP2 is the carrier's header reference. Units below are volts (V), milliamperes (mA), microamperes (µA), kilohms (kΩ), nanofarads (nF), milliseconds (ms) and degrees Celsius (°C). No physical build, target firmware compilation, Pico execution or condensation performance is claimed.

2. One supply, two independent digital interfaces

Original RP2040 Pico 3V3_OUT physical 36 supplies Adafruit SHT45 JP2 VIN 1 and DS18B20 TO-92 VDD pin 3. Ground physical 38, carrier GND 3 and thermometer GND 1 share the same ground symbols. GP0 physical 1 reaches carrier SDA 5, GP1 physical 2 SCL 4, and GP16 physical 21 reaches DS data pin 2 with a 4.7 kilohm pull-up. A 100 nanofarad capacitor bypasses DS power. Two power crossings have explicit bridges. A separate mechanical panel shows one insulated contact point on glass and the humidity carrier in room air.
Logical pin-level circuit, not a plug-face or breadboard-layout instruction. All ground symbols identify one common net. The two red jump arcs cross data and clock without a junction. The placement panel adds no electrical wires. Open full-size circuit · Download editable Scalable Vector Graphics (SVG).
TerminalExact connectionPurpose
Pico 3V3_OUT, physical 36Carrier JP2 pin 1 VIN; DS18B20 pin 3 VDD; R1 top; C1 topOne nominal 3.3 V domain, powered by Pico USB only.
Pico GND, physical 38Carrier JP2 pin 3 GND; DS18B20 pin 1 GND; C1 bottomPower and signal return.
Pico GP0, physical 1Carrier JP2 pin 5 SDAI2C0 data; requested clock rate is 100 kilohertz (kHz).
Pico GP1, physical 2Carrier JP2 pin 4 SCLI2C0 clock.
Pico GP16, physical 21DS18B20 pin 2 DQ; R1 bottomExternally powered 1-Wire, with R1 = 4.7 kΩ ±1% to 3V3_OUT.
C1100 nF ceramic directly between DS pins 3 and 1Local nonpolar power bypass.
Carrier JP2 pin 2, 3VLeave unusedDo not join the regulator output to a second supply.

Adafruit's current carrier includes a regulator, level shifting, local bypass and 10 kΩ pull-ups on each side of each shifted bus line. Add no extra I2C pull-ups here. At assumed 3.3 V on both sides, two 10 kΩ branches draw about 0.66 mA per LOW line; actual voltage drops depend on the board. The separate 4.7 kΩ DQ resistor draws about 0.70 mA at nominal 3.3 V, within the thermometer's stated 4 mA sink test condition. These are loading estimates, not measured edge quality.

The bare SHT45 operates at 1.08–3.6 V, while the carrier accepts the microcontroller supply through VIN. The externally powered DS18B20 requires 3.0–5.5 V and its DQ pull-up must not exceed its local supply. Its local-power input-HIGH minimum is 2.2 V and specified LOW maximum is 0.8 V. Keep every external signal in the Pico's 3.3 V domain; the thermometer's 5.5 V capability does not permit a 5 V Pico input. Check actual startup, loaded rail, HIGH/LOW and rise times at the terminals. Short wiring and the stated resistor do not automatically qualify any cable length.

Use the manufacturer's TO-92 package view to identify pins before soldering. This lesson uses a bare DS18B20+, not a waterproof probe with undocumented wire colours. Cover each lead separately and strain-relieve the short cable. Only the plastic sensor body contacts the glass. Keep the Pico, USB connection and exposed conductors away from wet surfaces.

3. Contact temperature is not the whole window

Attach the thermometer's flat body to one small interior glass point with a repeatable, electrically insulating, removable attachment. Record its position and attachment method. A package partly exposed to warm room air can read warmer than the glass; the cable also transports heat. Thermal lag is the delay while the package approaches a changing surface temperature. The DS18B20's conversion time is not its contact-settling time.

Place the humidity carrier in representative nearby room air, apart from Pico heat, direct sun, exhaled breath, radiators and the wet boundary immediately against the pane. Air in a room is not perfectly mixed. A sensor at one location cannot establish vapour conditions behind a curtain or at every cold frame corner. Allow the mounted sensors to settle under the intended conditions and check stability; no universal waiting time establishes that contact error is bounded.

The supplied program never sends a SHT45 heater command. It performs a soft reset, but reset does not remove residual heat, contamination or previous exposure. A cold, unheated, settled sensor and known handling history are physical prerequisites. Sensirion's typical humidity response is on a seconds scale, so a new digital conversion can still describe a lagging sensor. Keep the humidity element open to air; do not tape over, touch or deliberately wet it. Follow current handling guidance for contamination and cleaning.

4. Estimate dew point, then widen the margin

The National Physical Laboratory (NPL) gives this Magnus approximation for saturation over liquid water. The natural logarithm, written ln, reverses an exponential relation. Its vapour-pressure approximation is described for −45 to +60 °C; this experiment deliberately accepts a narrower room domain and excludes dew-point intervals extending below 0 °C. It does not substitute a frost-point calculation. The application accepts indicated air temperature from 5 to 40 °C, RH from 20 to 90%, and contact temperature from 0 to 40 °C; it also requires the expanded dew-point interval to remain at or above 0 °C.

g = ln(RH / 100) + 17.62 × T_air / (243.12 + T_air)
T_dew = 243.12 × g / (17.62 - g)
central margin = T_contact - T_dew

Example: T_air = 22 °C; RH = 60%; T_contact = 15 °C
T_dew ≈ 13.875 °C; central margin ≈ +1.125 °C

A central estimate hides uncertainty. The code uses explicitly assumed bounds of ±1 °C for air temperature, ±5 percentage points for RH, ±1 °C for contact indication and ±0.3 °C for the dew-point model. Five percentage points means 60% becomes 55–65%, not a five-percent multiplier. These values are a teaching policy, not copied sensor tolerances, a validated error budget or a confidence interval. In particular, contact error and lag may exceed them. Replace them only with justified bounds and recheck the domain.

Dew point increases with air temperature and RH within this domain. Evaluate its low and high input corners, then subtract the opposite extremes from the contact interval:

dew_low  = dewpoint(T_air - 1, RH - 5) - 0.3
dew_high = dewpoint(T_air + 1, RH + 5) + 0.3
margin_low  = T_contact - 1 - dew_high
margin_high = T_contact + 1 - dew_low

At 22 °C / 60% RH / contact 15 °C:
dew interval ≈ [11.311, 16.364] °C
margin interval ≈ [-2.364, +4.689] °C → INDETERMINATE
Calculated margin for air 22 degrees Celsius and relative humidity 60 percent as the contact indication varies from 8 to 20 degrees. A central rising line and its assumed uncertainty band cross the zero-margin line at different temperatures. The contact indication 15 degrees has a positive central estimate but an interval crossing zero. All curves use the delivered corner calculation and are constructed, not measured.
Calculated using the actual supplied function and assumed bounds, not room measurements or a calibration certificate. A positive centre can coexist with an unresolved sign. Open full-size graph · Download editable SVG.

POSITIVE_MARGIN_ASSUMED means the whole chosen interval is above zero. NEGATIVE_MARGIN_ASSUMED means it is below zero. An interval including zero is INDETERMINATE. None is an all-window safety state: the classification is conditional on the assumed bounds, placement, timing and model. Droplet formation also depends on local conditions and surface behavior. The program supplies no heating, ventilation, mould-prevention or building-control action.

Datasheet accuracy and resolution are separate. The DS18B20 specifies ±0.5 °C error from −10 to +85 °C under its conditions, but a 12-bit step of 0.0625 °C does not make the contacted glass that accurate. SHT45 headline ±1% RH and ±0.1 °C are typical values in specified regions; consult the current maximum/range curves and handling conditions. NPL's vapour-pressure approximation uncertainty is not automatically a certified ±0.3 °C dew-point uncertainty. This lesson's extra allowance remains an explicit assumption.

5. New conversions still have different time windows

During preparation, the reader requires exactly one DS18B20 family address and checks its read-only memory (ROM) code with CRC. It rejects parasite power. It preserves the alarm bytes while setting 12-bit resolution in volatile scratchpad memory, then reads that configuration back. It does not copy settings to electrically erasable programmable read-only memory (EEPROM).

Each pair issues a new thermometer Convert T command, requires an observed busy LOW, waits at least 760 ms after the command returns, then requires completion HIGH and a valid nine-byte scratchpad. The datasheet's maximum 12-bit conversion time is 750 ms. A valid scratchpad alone could contain the +85 °C power-on value or an earlier result; the sequence checks address, resolution and conversion behavior instead of treating CRC alone as freshness. Returned delays and a stuck busy signal cause rejection. This is still protocol evidence, not proof of the physical die or thermal equilibrium.

About 600 ms into that conversion, the reader commands a fresh high-precision SHT45 measurement with byte 0xFD, waits 10 ms and reads temperature plus humidity with both CRCs. The current datasheet gives an 8.3 ms maximum under its characterization conditions. Raw RH can mathematically fall below 0 or above 100%; it is rejected by the application domain rather than cropped into a usable value.

The Pico records command/read brackets, not hardware measurement timestamps. The air bracket must be at most 40 ms, the contact bracket at most 850 ms, and their total span and oldest-command age at the decision must be at most 1000 ms. Conversion periods can overlap; the exact sensor sampling instants are unknown. These guards limit returned software observations, not physical response age or unobserved delays. No time-skew compensation or derivative estimate is applied.

The clock uses wrapping millisecond ticks. It rejects an observed backward step or gap above 2000 ms and a frozen clock has a finite wait budget. An entirely unobserved whole tick turn can alias a small difference; a hung low-level call is not interrupted by this logic. The short continuously serviced run is a required assumption. A completed row describes its historical decision snapshot, not the connection, temperature or margin at print time.

6. Complete finite reader and calculator

Save the entire program as pico_condensation_margin.py on the selected Pico firmware. The firmware's bundled 1-Wire module is used; no external cloud or network client is involved. Load the file once in the read-evaluate-print loop (REPL) and call main explicitly:

scope = {}
exec(open("pico_condensation_margin.py").read(), scope)
scope["main"]()
"""Finite heater-uncommanded SHT45 + externally powered DS18B20 experiment.
Host observation brackets and assumed error intervals are not physical guarantees.
Load once, then explicitly call main(); there is no autostart block.
"""
import math

POLICY = {'air_C': 1.0, 'rh_points': 5.0, 'surface_C': 1.0, 'model_C': 0.3}

def number(x):
    if isinstance(x, bool) or not isinstance(x, (int, float)) or not math.isfinite(x):
        raise ValueError('NONFINITE_NUMBER')
    return x

def crc_sht(data):
    c = 255
    for b in data:
        c ^= b
        for _ in range(8):
            c = ((c << 1) ^ 0x31) & 255 if c & 128 else (c << 1) & 255
    return c

def crc_dallas(data):
    c = 0
    for b in data:
        for _ in range(8):
            mix = (c ^ b) & 1
            c >>= 1
            if mix:
                c ^= 0x8C
            b >>= 1
    return c

def frame(data, n):
    if not isinstance(data, (bytes, bytearray)) or len(data) != n:
        raise ValueError('FRAME_LENGTH')
    return bytes(data)

def sht_words(data):
    b = frame(data, 6)
    if crc_sht(b[:2]) != b[2] or crc_sht(b[3:5]) != b[5]:
        raise ValueError('SHT_CRC')
    return (b[0] << 8 | b[1], b[3] << 8 | b[4])

def sht_values(data):
    t, r = sht_words(data)
    # Do not crop a nonphysical raw humidity into a usable endpoint.
    return -45 + 175 * t / 65535, -6 + 125 * r / 65535

def surface_value(data):
    b = frame(data, 9)
    if crc_dallas(b[:8]) != b[8]:
        raise ValueError('DS_CRC')
    if b[4] != 0x7F:
        raise ValueError('DS_RESOLUTION')
    raw = b[0] | b[1] << 8
    if raw & 0x8000:
        raw -= 65536
    return raw / 16

def dewpoint(t, rh):
    t, rh = number(t), number(rh)
    if not -45 <= t <= 60 or not 0 < rh <= 100:
        raise ValueError('MAGNUS_DOMAIN')
    g = math.log(rh / 100) + 17.62 * t / (243.12 + t)
    return 243.12 * g / (17.62 - g)

def margin(t, rh, surface, policy=None):
    t, rh, surface = number(t), number(rh), number(surface)
    p = dict(POLICY if policy is None else policy)
    if set(p) != set(POLICY):
        raise ValueError('POLICY_FIELDS')
    for k in p:
        if not 0 <= number(p[k]) <= (10 if k == 'rh_points' else 5):
            raise ValueError('POLICY_RANGE')
    if not 5 <= t <= 40 or not 20 <= rh <= 90 or not 0 <= surface <= 40:
        raise ValueError('ROOM_DOMAIN')
    lo = dewpoint(t - p['air_C'], rh - p['rh_points']) - p['model_C']
    hi = dewpoint(t + p['air_C'], rh + p['rh_points']) + p['model_C']
    if lo < 0:
        raise ValueError('LIQUID_WATER_SCOPE')
    low = surface - p['surface_C'] - hi
    high = surface + p['surface_C'] - lo
    label = 'INDETERMINATE'
    if low > 0:
        label = 'POSITIVE_MARGIN_ASSUMED'
    elif high < 0:
        label = 'NEGATIVE_MARGIN_ASSUMED'
    return {'dewpoint_C': dewpoint(t, rh), 'dewpoint_interval_C': [lo, hi],
            'margin_C': surface - dewpoint(t, rh), 'margin_interval_C': [low, high],
            'classification': label, 'assumed_policy': p,
            'accuracy_or_condensation_guaranteed': False}

class Clock:
    def __init__(self, ticks, diff):
        self.ticks, self.diff = ticks, diff
        self.raw = ticks()
        if type(self.raw) is not int:
            raise ValueError('CLOCK')
        self.elapsed, self.bad = 0, False
    def now(self):
        if self.bad:
            raise ValueError('CLOCK')
        raw = self.ticks()
        d = self.diff(raw, self.raw)
        if type(raw) is not int or type(d) is not int or not 0 <= d <= 2000:
            self.bad = True
            raise ValueError('CLOCK')
        self.raw, self.elapsed = raw, self.elapsed + d
        return self.elapsed

def wait(clock, sleep_ms, duration):
    start = clock.now()
    for _ in range(duration // 25 + 3):
        if clock.now() - start >= duration:
            return
        sleep_ms(min(25, duration - (clock.now() - start)))
        clock.now()
    raise ValueError('FROZEN_CLOCK')

def evaluate(air, contact, now):
    t, rh, ab, ae = air
    surface, sb, se = contact
    if any(type(v) is not int or v < 0 for v in (ab, ae, sb, se, now)):
        raise ValueError('TIME_TYPE')
    if not ab <= ae <= now or not sb <= se <= now:
        raise ValueError('TIME_ORDER')
    if ae - ab > 40 or se - sb > 850:
        raise ValueError('ACQUISITION_SPAN')
    oldest = min(ab, sb)
    offset = max(ae, se) - oldest
    if now - oldest > 1000 or offset > 1000:
        raise ValueError('PAIR_AGE')
    result = margin(t, rh, surface)
    result.update({'air_C': t, 'rh_percent': rh, 'surface_C': surface,
                   'air_host_window_ms': [ab, ae], 'surface_host_window_ms': [sb, se],
                   'decision_elapsed_ms': now, 'oldest_command_age_ms': now - oldest,
                   'pair_window_span_ms': offset, 'sensor_timestamp': None})
    return result

class Hardware:
    def __init__(self):
        self.i2c, self.ow, self.rom = None, None, None
        self.pins = []
    def open(self):
        from machine import Pin, I2C
        from onewire import OneWire
        self.Pin = Pin
        # Retain pin handles before changing modes, for partial-open cleanup.
        for n in (0, 1, 16):
            self.pins.append(Pin(n))
        sda, scl, dq = self.pins
        self.i2c = I2C(0, sda=sda, scl=scl, freq=100000, timeout=20000)
        sda.init(pull=None)
        scl.init(pull=None)
        self.ow = OneWire(dq)
        dq.init(pull=None)  # retain open-drain mode; external 4.7k is the pull-up
    def command(self, value):
        if self.i2c.writeto(0x44, bytes([value])) != 1:
            raise OSError('SHORT_WRITE')
    def select(self):
        self.ow.reset(True)
        self.ow.writebyte(0x55)
        self.ow.write(self.rom)
    def scratch(self):
        self.select()
        self.ow.writebyte(0xBE)
        b = bytearray(9)
        self.ow.readinto(b)
        # Checks CRC and volatile 12-bit configuration, not a fresh conversion.
        surface_value(b)
        return bytes(b)
    def prepare(self, clock, sleep_ms):
        wait(clock, sleep_ms, 10)
        self.command(0x94)
        wait(clock, sleep_ms, 2)
        self.command(0x89)
        wait(clock, sleep_ms, 2)
        a, b = sht_words(self.i2c.readfrom(0x44, 6))
        serial = a << 16 | b
        roms = self.ow.scan()
        if len(roms) != 1:
            raise ValueError('ONE_DS_REQUIRED')
        rom = frame(roms[0], 8)
        if rom[0] != 0x28 or crc_dallas(rom[:7]) != rom[7]:
            raise ValueError('DS_ROM')
        self.rom = rom
        self.select()
        self.ow.writebyte(0xB4)
        if self.ow.readbit() != 1:
            raise ValueError('NO_PARASITE_POWER')
        self.select()
        self.ow.writebyte(0xBE)
        raw = bytearray(9)
        self.ow.readinto(raw)
        if crc_dallas(raw[:8]) != raw[8]:
            raise ValueError('DS_CRC')
        # Preserve alarm bytes; set volatile resolution. Never COPY SCRATCHPAD.
        self.select()
        self.ow.writebyte(0x4E)
        self.ow.write(bytes([raw[2], raw[3], 0x7F]))
        self.scratch()
        return {'sht_serial': serial, 'ds_rom_hex': rom.hex()}
    def start_surface(self):
        self.select()
        self.ow.writebyte(0x44)
        if self.ow.readbit() != 0:
            raise ValueError('DS_BUSY_NOT_OBSERVED')
    def read_surface(self):
        if self.ow.readbit() != 1:
            raise ValueError('DS_NOT_COMPLETE')
        return surface_value(self.scratch())
    def air(self):
        return sht_values(self.i2c.readfrom(0x44, 6))
    def close(self):
        errors = []
        if self.i2c is not None:
            try:
                self.i2c.deinit()
            except BaseException as e:
                errors.append(e)
        for pin in self.pins:
            try:
                pin.init(self.Pin.IN, pull=None)
            except BaseException as e:
                errors.append(e)
        if errors:
            raise errors[0]


def run(io=None, ticks=None, diff=None, sleep_ms=None, count=8):
    if type(count) is not int or not 1 <= count <= 12:
        raise ValueError('COUNT')
    if ticks is None:
        from time import ticks_ms as ticks, ticks_diff as diff, sleep_ms
    io = Hardware() if io is None else io
    rows = []
    try:
        clock = Clock(ticks, diff)
        io.open()
        identity = io.prepare(clock, sleep_ms)
        clock.now()
        for index in range(count):
            if index:
                wait(clock, sleep_ms, 1000)
            sb = clock.now()
            io.start_surface()
            issued = clock.now()
            if issued - sb > 10:
                raise ValueError('DS_START_LATENCY')
            wait(clock, sleep_ms, 600)
            ab = clock.now()
            io.command(0xFD)  # high precision, never a heater command
            wait(clock, sleep_ms, 10)
            t, rh = io.air()
            ae = clock.now()
            wait(clock, sleep_ms, max(0, 760 - (clock.now() - issued)))
            surface = io.read_surface()
            se = clock.now()
            row = evaluate((t, rh, ab, ae), (surface, sb, se), clock.now())
            row['index'] = index
            rows.append(row)
    finally:
        io.close()  # no report if required cleanup raises; sensors remain powered
    return {'state': 'CLOSED_HISTORY', 'identity': identity, 'rows': rows,
            'current_margin_C': None, 'hardware_qualified': False,
            'scope': 'Assumed interval at each completed host snapshot; no current safety state'}


def main():
    import json
    report = run()
    print(json.dumps(report))
    return report

The default is eight pairs, with a one-second serviced pause between pairs—roughly thirteen seconds plus initialization and transfer overhead. It does not continually monitor the window. The program prints one CLOSED_HISTORY object after interface cleanup succeeds, with current_margin_C=null. No previous good reading substitutes for a failed pair. A transfer, CRC, range, clock, conversion or cleanup error prevents a successful report; KeyboardInterrupt and process-exit exceptions propagate through the same cleanup attempt.

Cleanup independently attempts to disable I2C and release each owned pin to an input without pulls. It does not switch off sensor power or undo a conversion already started. Partial construction and cleanup failures are exercised with host adapters; no claim is made that those tests execute Pico registers. Printing can also fail after a completed historical acquisition.

7. A repeatable small experiment

  1. With power removed, identify the carrier header and TO-92 leads. Check the five external signal/power nets and C1, common ground, the 4.7 kΩ pull-up and the two explicit non-junction crossings. Do not connect an additional supply or 5 V bus.
  2. Qualify loaded supply and bus levels with appropriate instruments, keeping conductors dry. Record exact boards, firmware, sensor identity, cable length and attachment method. The program does not measure its own rail or qualify the glass contact.
  3. Mount the contact sensor and place the air carrier as described. Let the arrangement stabilize and investigate drift, sunlight or room gradients. Document a justified error envelope before interpreting a physical interval.
  4. Run one eight-pair acquisition without moving the sensors. Save the complete returned object. Compare each pair's windows, old-command age, raw temperatures, RH and interval—not just its classification string.
  5. Repeat at a second separately documented contact point after remounting and settling. Do not treat both locations as simultaneous, or infer the rest of the pane from either. A quiet room condition can be more informative than artificially wetting a sensor.
  6. Use a wider assumed contact bound in a separate calculation to see whether a previously positive interval becomes indeterminate. That is sensitivity analysis, not a new measured result.
  7. Power off before disconnecting a sensor for a later failure check. A subsequent run with a missing thermometer or broken transfer must fail rather than reuse earlier values. Do not interrupt wet wiring during an acquisition.

The blank worksheet has no observations filled in. Keep original historical rows and write separate notes for contact changes, drift and rejected runs. A classification without its assumptions and position is incomplete.

8. Check the model and failure boundaries

The single experiment and test archive includes the complete program, host tests, blank log, instructions and original graphics. Run python test_margin.py on a host Python interpreter in that extracted directory. The suite exercises the actual supplied functions with deterministic bus/clock adapters, CRC vectors, signed temperatures, interval corners, timing boundaries, faults and cleanup. It is not a target firmware build, sensor calibration or physical thermal experiment.

Inspect a generated interval together with its assumptions. For instance, an apparently useful thermometer resolution can coexist with large contact error. Repeating the same indicated value reduces neither an unknown offset nor an unqualified thermal lag. A wider bound is honest only if its underlying physical limits are justified; merely choosing a large number is not qualification.

9. Challenge: is +1.125 °C enough?

The air is indicated as 22 °C and 60% RH, the contacted sensor reads 15 °C, and the central margin is +1.125 °C. The interval under the stated assumptions is −2.364 to +4.689 °C. May the report say that the whole window cannot condense?

Show the answer

No. The interval crosses zero, so the sign is indeterminate even under those assumptions. The physical contact and air placement may introduce errors beyond the selected bounds; two conversion windows are not an exact simultaneous field measurement. One small contact point is not the whole pane, and a historical row is not a current guarantee.

10. Primary references and complete downloads

Complete program · Blank worksheet · Experiment and tests ZIP. Both figures offer full-size Portable Network Graphics (PNG) and editable SVG files. Continue with the electronics course.