Files
steveandClaude Opus 5 ba0c3b58b6 Initial commit: PT1000 probe emulation bridge
Feeds third-party BBQ probes into a pellet grill's wired probe jacks by
presenting the resistance a real PT1000 RTD would show at that temperature.
The grill sees an ordinary wired probe, so its display, app, target-temp
alarms and Keep Warm all work with no protocol reversing involved.

Pluggable probe sources: ThermoWorks RFX (via ThermoWorks Cloud), MEATER
(community-derived BLE decode), a synthetic simulator for hardware-free
development, and a Combustion stub.

Two safety invariants are load-bearing:
- Stale, missing or implausible readings drive the channel cold, never hot.
  A stuck-hot channel could convince the grill a cook finished early.
- Unimplemented sources raise rather than returning plausible numbers, since
  the grill acts on these values.

Cloud sources are dated by the cloud's own timestamp rather than by fetch
time, because ThermoWorks serves a dead probe's last value with a fresh 200.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-16 22:46:14 -04:00

6.7 KiB
Raw Permalink Blame History

Hardware

Target grill: Traeger Ironwood (2022, touchscreen WiFire) — the non-XL, non-850 model. It has two wired probe jacks on the controller, labelled 1 and 2, which take PT1000 RTD probes (1000 Ω at 0 °C, ~3.85 Ω/°C).

We present a resistance to those jacks. The grill has no idea it isn't a probe, so the display, the app, target-temp alarms and Keep Warm all work natively.


Step 0 — Prove the concept for $0 before buying anything

Do this first. It validates every assumption in this repo in about ten minutes, using a multimeter and one resistor.

0a. Ohm out a real Traeger probe

With the probe at room temperature, measure all three pairs on the 3.5 mm plug:

Pair Expected Meaning
One pair reads ~1086 Ω at 72 °F PT1000 element This is the sensor
Another pair reads ~0 Ω bonded lead 3-wire RTD, lead-compensated
Third pair reads ~1086 Ω element + bonded lead Consistent with 3-wire

If instead you see only two meaningful contacts and no near-zero pair, it's wired as a simple 2-wire RTD and the emulator gets simpler.

Confirm it really is PT1000 by measuring at two known temperatures:

Bath Temp PT1000 should read
Ice water 0 °C / 32 °F 1000 Ω
Boiling water (sea level) 100 °C / 212 °F 1385 Ω

Run smokescreen table for the full curve.

If these numbers don't match, stop — your grill isn't using the PT1000 curve this repo assumes, and pt1000.py needs new coefficients. Everything else in the design still holds.

0b. Fake a temperature with a plain resistor

Put a fixed resistor across the element contacts of a 3.5 mm TRS plug (mirror whatever the real probe does with the third contact) and plug it into jack 1:

Resistor Grill should display
1.0 kΩ ~32 °F
1.2 kΩ ~127 °F
1.5 kΩ ~264 °F

If the grill shows roughly these, the entire approach is proven — the rest is just making that resistor programmable. This experiment is worth more than any amount of protocol reversing.

While you're here, note what the grill does with an open jack and a shorted jack. Those are its fault thresholds, and knowing them tells you whether the fail-cold policy actually trips a visible probe error.


Bill of materials

Qty Part Notes
1 Raspberry Pi (any with I²C) Zero 2 W is plenty
2 AD5272BRMZ-1 digital rheostat 1 kΩ, 1024-step, I²C, ±1% end-to-end
2 1 kΩ 0.1% 25 ppm resistor Sets the base of the window
2 3.5 mm TRS male plug Or sacrifice a cheap replacement probe cable
1 ADuM1250 + isolated DC-DC Optional, strongly recommended — see Isolation
GeeekPi nRF52840 dongle For reading your BLE probes

Why the AD5272 specifically

The usual digipot fails here on two counts: 256 steps is ~4 Ω/step (≈1 °C, too coarse to hide), and typical end-to-end tolerance is ±20%, which is a ~50 °C error before you calibrate anything.

The AD5272-1 gives 1024 steps across 1 kΩ ≈ 1 Ω/step ≈ 0.26 °C, finer than the grill displays, with ±1% tolerance that calibration trims to nothing.

Sizing check, from smokescreen table:

0 °C   (32 °F)  → 1000 Ω
200 °C (392 °F) → 1759 Ω

A 759 Ω span fits inside the 1 kΩ rheostat with headroom to spare.


Wiring, per channel

              ┌─────────── 3.5mm TRS plug → grill jack ───────────┐
              │                                                    │
  tip  ───────┴──[ R_fixed 1kΩ 0.1% ]──[ AD5272 A ]               │
                                            │                      │
                                          (W) wiper ───────────────┴─── sleeve
                                            │
  ring ─────────────────────────────────────┘   (mirror the real probe:
                                                 bond to whichever contact
                                                 measured ~0 Ω in step 0a)

  AD5272: SDA/SCL → Pi I²C, VDD → 3.3 V, GND → Pi GND
          ADDR strapped differently per channel (0x2F and 0x2C)

Use the AD5272 as a rheostat (two-terminal): tie terminal B to the wiper W, or leave B open per the datasheet's rheostat-mode figure. Do not wire it as a three-terminal divider.

Both channels need distinct I²C addresses — the config loader rejects duplicates, because two rheostats answering to one address is a maddening bug to chase in the dark next to a hot grill.


Isolation

The grill controller is mains-powered and its probe jacks share a ground reference with its ADC. Tying the Pi's ground straight to it creates a ground loop that will, at best, skew your readings and, at worst, inject noise into the controller.

The clean fix is an ADuM1250 I²C isolator plus an isolated DC-DC so the rheostat side floats with the grill and the Pi side floats on its own.

At minimum, power the Pi from the same outlet as the grill, and check whether your step-0b resistor readings drift once the Pi's ground is connected.


Calibration

Per channel, once. This cancels resistor tolerance, wiper resistance, and contact resistance in one line fit.

  1. Wire up the channel, but plug it into your multimeter, not the grill.
  2. Command a low wiper code and measure across the plug:
    .venv/bin/python -c "from smokescreen.outputs.rheostat import AD5272Rheostat; AD5272Rheostat(0x2f).set_code(0)"
    
  3. Repeat at a high code (e.g. 1000).
  4. Solve the fit:
    smokescreen calibrate --code-a 0 --ohms-a 1012.4 --code-b 1000 --ohms-b 2015.6
    
  5. Paste the emitted calibration: block into config.yaml.

Sanity-check the reported temperature range covers 32400 °F. If it starts above ~40 °F, your fixed resistor is too large.


Failure behaviour

When a probe goes stale, disconnects, or reports something implausible, the emulator drives the channel to its minimum resistance — an obviously-cold reading.

This direction is deliberate. A stuck-hot channel could convince the grill the food hit its target and trigger Keep Warm or a shutdown in the middle of a cook. A stuck-cold channel can only ever look wrong. Failures should be visible, never mistaken for success.

The same fail-cold state is applied on clean shutdown.

This does not make the system safe to leave unattended. It is a hobby project driving a fire with reverse-engineered numbers. Keep a real probe in the cook until you have several successful runs behind you.