← Back to Kevin's newslettersPublished: 2023 July 16

Negative croissant results

It is with great disappointment that I must report continued failure to break through the sub 20 minute frozen croissant barrier.

Through more than a dozen bakes, I’ve established that my 1 kW air fryer can deliver more than enough power. The trouble has been sufficiently baking the inside before burning the outside — over the last 3 months I’ve scorched many croissant tops.

This is likely because the heating element is suspended just a few centimeters above the croissant, which rests in a 2 liter basket:

pain au chocolat and thermocouple

To test whether the burnt tops are from airflow (the top being the first surface the hot air hits, and thus where it transfers most of its energy) or from infrared radiation (direct line-of-sight to the heating element), I baked several croissants underneath a tin-foil “shade”.

Unfortunately, these shaded croissants turned out under-cooked (even at the full 20 minute cooking time) — my best guess is that, given the small basket size, any tin foil sufficient to fully shade a croissant ends up also significantly impeding the flow of warm air around it.

I also tested raising the humidity of the baking chamber by opening it every few minutes to spritz in water. However, this merely yielded soggy croissants with burnt tops.

All-together, these results leave me discouraged: Perhaps modifying air fryer geometry could get from 20 minutes to 15 minutes, but I don’t see a route to the truly life-changing sub 5 minute bake times using just convective airflow.

After the last newsletter, a reader informed me about metal baking rods which conduct heat to the center of large cakes. The same principle could be employed, though piercing frozen dough poses a more difficult physical interface than liquid batter — a croissant-shrike mechanism would need to overcome that.

Microwaves, even putting aside the safety and complexity reservations discussed last time, are poorly suited to defrosting — frozen water molecules are fixed in a lattice, so won’t readily absorb microwave energy, which finds its way to any available liquid water. This feedback loop results in the painfully-familiar “my burrito is still mostly frozen but now has pockets of lava” effect.

After pondering all this for a while, I stepped back to consider my overall goal: Conjure a deliciously, warm, coffee-complementing snack in the same time it takes to brew a cup.

Then it occurred to me: Rather than optimize a baking mechanism for a fixed croissant, what if I optimized a croissant for a fixed baking mechanism? That is, design a pastry which can be baked from frozen in under 5 minutes.

But that’ll have to wait until later. (In the mean time, please send in any ideas or favorite recipes!)

Time series visualization

While my research failed to uncover a superior baking mechanism, it did prove to be a fertile problem domain for exploring time series visualization tools.

Here’s a graphic of the air fryer basket temperature:

Note that when it heats an empty basket it overshoots the setpoint temperature by about 20°C, but after adding the ~60g pain au chocolat it’s pretty accurate, with the temperature oscillating by only about 15°C or 10% of the setpoint — seems pretty good to me for an all-analog, cheapest-on-Amazon special.

My measurement apparatus consists of a Raspberry Pi running a Rust program (enclosed at end of newsletter) which reads the oven air temperature via a MAX6675 Module + K Type Thermocouple and stores to a SQLite database, which is then visualized in a browser using Grafana.

For my use case of live time series visualization, I found Grafana just OK:

I didn’t need or want an extra running service or to maintain a “database” of temperatures. Really all I wanted was something that felt as lightweight as a println statement which I could add to my small measurement program, which would spin up a webpage with a live-updating time series plot.

When I mentioned this last time, readers suggested:

However, since I wanted to stick with the language I had the most embedded experience with, Rust, I ended up making splot (“streaming plot”), a lil’ Rust library to do exactly what I want =D

On the browser-side, it relies on the wonderfully lightweight and fast uPlot JavaScript plotting library for visualization. On the server-side, while it’s not entirely allocation-free (the underlying axum web framework will allocate when new browsers connect), I did my best to keep things lightweight:

In addition to numbers, it also allows streaming lines of text — imagine that, being able to view time series and logs in a browser. With that kind of advanced 1990’s technology, will you even need a terminal window anymore!?!

Misc. stuff

Appendix: Raspberry Pi temperature measurement code

// Build for raspberry pi via:
// cross build --release --features pi --bin croissantron --target aarch64-unknown-linux-gnu

use log::*;

fn now_millis() -> u64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
}

fn main() -> anyhow::Result<()> {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
        .format_timestamp(None)
        .init();

    let db = rusqlite::Connection::open("observations.db")?;

    db.execute(
        "CREATE TABLE IF NOT EXISTS observation (
            t     REAL NOT NULL,
            temp  REAL NOT NULL
        )",
        (),
    )?;

    let insert = |temp| {
        db.execute(
            "INSERT INTO observation (t, temp) VALUES (?1, ?2)",
            // grafana struggles with human readable string (even after sqlite datetime fn); so just store unix timestamp integer
            (now_millis(), temp),
        )
        .unwrap();
    };

    #[cfg(feature = "pi")]
    let mut read_temp = {
        // Wire to spi0 pins as described on https://pinout.xyz/pinout/spi
        let mut spi = {
            use rppal::spi::{Bus, Mode, SlaveSelect};
            rppal::spi::Spi::new(Bus::Spi0, SlaveSelect::Ss0, 8_000_000, Mode::Mode0)?
        };

        move || {
            let mut buf = [0u8; 2];
            spi.read(&mut buf).unwrap();
            debug!("{:04x?}", buf);

            // https://www.analog.com/media/en/technical-documentation/data-sheets/MAX6675.pdf
            0.25 * (u16::from_be_bytes(buf) >> 3) as f32
        }
    };

    #[cfg(not(feature = "pi"))]
    let read_temp = || 100.0;

    loop {
        let temp = read_temp();
        info!("{} degC", temp);
        insert(temp);
        std::thread::sleep(std::time::Duration::from_millis(500));
    }

    //Ok(())
}