diff --git a/firmware/rust1/src/bin/heizung.rs b/firmware/rust1/src/bin/heizung.rs
index 42947b74ccb2401ba07e207926a395dacfaa3b27..020332467454b578401c7176f5f0d9e02a99e88b 100644
--- a/firmware/rust1/src/bin/heizung.rs
+++ b/firmware/rust1/src/bin/heizung.rs
@@ -4,11 +4,12 @@
 
 use defmt::*;
 use embassy_executor::Spawner;
+use embassy_futures::select::select;
+use embassy_futures::select::Either::*;
 use embassy_rp::gpio;
 use embassy_time::{Duration, Timer};
-use embassy_rp::gpio::{Input, Level, Output, Pull};
-//use embassy_rp::i2c;
-use heizung::rp::i2c;
+use embassy_rp::gpio::{Input, Level, Output, Pull, Flex};
+use embassy_rp::i2c;
 use embassy_rp::interrupt;
 use embedded_hal_async::i2c::I2c;
 use embassy_rp::peripherals::{UART0, I2C0};
@@ -71,21 +72,116 @@ fn format_scan_result<const N : usize>(msg : &mut String<N>, scan_result : [Scan
     Ok(())
 }
 
+async fn _clockStretch<'d, SCL: gpio::Pin>(scl: &mut Flex<'d, SCL>) -> Result<(), i2c::Error> {
+    use i2c::Error::*;
+    use i2c::AbortReason::*;
+
+    if scl.is_high() {
+        return Ok(())
+    }
+
+    let is_high = scl.wait_for_high();
+    let timeout = Timer::after(Duration::from_micros(100));
+
+    return match select(is_high, timeout).await {
+        First(_) => Ok(()),
+        Second(_) => Err(Abort(ArbitrationLoss)),
+    }
+}
+
+// adapted from https://github.com/earlephilhower/arduino-pico/blob/6e52b72523b11470b2ad2b0578ca1500be238108/libraries/Wire/src/Wire.cpp#L257
+async fn i2c_write_zero_bytes_middle_part<'d, SCL: gpio::Pin, SDA: gpio::Pin, const FREQ : u64>(
+        mut addr: u16, delay: Duration, scl: &mut Flex<'d, SCL>, sda: &mut Flex<'d, SDA>)
+        -> Result<(), i2c::Error> {
+    sda.set_as_input();
+    Timer::after(delay).await;
+    scl.set_as_input();
+    _clockStretch(scl).await?;
+    sda.set_as_output();
+    Timer::after(delay).await;
+    scl.set_as_output();
+    Timer::after(delay).await;
+    for i in 0..8 {
+        addr <<= 1;
+        sda.set_level(if (addr & (1 << 7)) != 0 { gpio::Level::High } else { gpio::Level::Low });
+        Timer::after(delay).await;
+        scl.set_as_input();
+        Timer::after(delay).await;
+        _clockStretch(scl).await?;
+        scl.set_as_output();
+        Timer::after(Duration::from_micros(5)).await; // Ensure we don't change too close to clock edge
+    }
+
+    sda.set_as_input();
+    Timer::after(delay).await;
+    scl.set_as_input();
+    _clockStretch(scl).await?;
+
+    let result = if sda.is_low() {
+        Ok(())
+    } else {
+        use i2c::Error::*;
+        use i2c::AbortReason::*;
+        Err(Abort(NoAcknowledge))
+    };
+    Timer::after(delay).await;
+    scl.set_as_output();
+
+    result
+}
+async fn i2c_write_zero_bytes<SCL: gpio::Pin, SDA: gpio::Pin, const FREQ : u64>(mut addr: u16, scl: &mut SCL, sda: &mut SDA)
+        -> Result<(), i2c::Error> {
+    use i2c::Error::*;
+    use i2c::AbortReason::*;
+
+    let delay = Duration::from_micros((1000000 / FREQ) / 2);
+    let ack = false;
+
+    let mut sda = Flex::new(sda);
+    let mut scl = Flex::new(scl);
+    sda.set_as_input();
+    sda.set_pull(Pull::Up);
+    scl.set_as_input();
+    scl.set_pull(Pull::Up);
+    // We only need low or pullup so we can set the value to low and only toggle direction.
+    // This is how OutputOpenDrain does it (and the only reason we use Flex is because we want the pullup).
+    sda.set_low();
+    scl.set_low();
+
+    Timer::after(delay).await;
+    if sda.is_low() || scl.is_low() {
+        return Err(Abort(ArbitrationLoss))
+    }
+
+    let result = i2c_write_zero_bytes_middle_part::<_, _, FREQ>(addr, delay, &mut scl, &mut sda).await;
+
+    Timer::after(delay).await;
+    sda.set_as_output();
+    Timer::after(delay).await;
+    scl.set_as_input();
+    Timer::after(delay).await;
+    sda.set_as_input();
+    Timer::after(delay).await;
+
+    return result;
+}
+
 #[embassy_executor::task]
-async fn i2c_task(mut i2c: i2c::I2c<'static, I2C0, i2c::Async>) {
+async fn i2c_task(mut i2c_peri: embassy_rp::peripherals::I2C0,
+        mut scl: embassy_rp::peripherals::PIN_13, mut sda: embassy_rp::peripherals::PIN_12,
+        mut i2c_irq: embassy_rp::interrupt::I2C0_IRQ, i2c_config: i2c::Config) {
     use i2c::Error::*;
     use i2c::AbortReason::*;
 
     loop {
+        if true {
+            let mut i2c = i2c::I2c::new_async(&mut i2c_peri, &mut scl, &mut sda, &mut i2c_irq, i2c_config);
+        }
+
         const MAX_ADDRESS : u8 = 128;
         let mut scan_result = [ScanResult::Error; MAX_ADDRESS as usize];
         for i in 0..MAX_ADDRESS {
-            //NOTE It looks like the blocking implementation doesn't accept an empty buffer
-            //     but the async one does. Lucky for us because we need that here!
-            // -> Actually, it doesn't. It just hangs.
-            info!("I2C testing: {}", i);
-            //FIXME Don't test with buffer size 1 because that will actually write, which can cause all kinds of issues!
-            match i2c.write_async(i.into(), [0; 0]).await {
+            match i2c_write_zero_bytes::<_, _, 400_000>(i as u16, &mut scl, &mut sda).await {
                 Result::Ok(()) => {
                     //info!("I2C ok: {}", i);
                     scan_result[i as usize] = ScanResult::Ack
@@ -155,8 +251,7 @@ async fn main(spawner: Spawner) {
     let i2c_irq = interrupt::take!(I2C0_IRQ);
     let mut i2c_config = i2c::Config::default();
     i2c_config.frequency = 400_000;
-    let i2c = i2c::I2c::new_async(p.I2C0, scl, sda, i2c_irq, i2c_config);
-    unwrap!(spawner.spawn(i2c_task(i2c)));
+    unwrap!(spawner.spawn(i2c_task(p.I2C0, scl, sda, i2c_irq, i2c_config)));
 
     // use 19200 baud in 8E1 mode - not great but Modbus default
     let mut uart_config = uart::Config::default();