diff --git a/firmware/rust1/src/bin/heizung.rs b/firmware/rust1/src/bin/heizung.rs
index e0d30a4065dd908c4994cd9098541831cf8a854d..17156398ca1a8525485408f9cf953d04d7b41fa1 100644
--- a/firmware/rust1/src/bin/heizung.rs
+++ b/firmware/rust1/src/bin/heizung.rs
@@ -13,215 +13,30 @@ use embassy_rp::i2c;
 use embassy_rp::interrupt;
 //use embedded_hal_async::i2c::I2c;
 use embassy_embedded_hal::SetConfig;
-use embassy_rp::peripherals::{UART0};
-use embassy_rp::uart::{self, UartRx, Parity};
+use embassy_rp::uart::{self, Parity};
 use embassy_rp::pwm::{self, Pwm};
-use embassy_rp::pio::{Config, Pio, ShiftConfig, ShiftDirection, FifoJoin};
-use embassy_rp::relocate::RelocatedProgram;
-use embassy_rp::Peripheral;
 use {defmt_rtt as _, panic_probe as _};
 use heapless::String;
-use fixed::traits::ToFixed;
-use fixed_macro::types::U56F8;
 
-fn append_hex1<const N : usize>(s : &mut String<N>, num : u8) -> Result<(), ()> {
-    let ch = if num < 10 {
-        ('0' as u8 + num) as char
-    } else {
-        ('a' as u8 + num - 10) as char
-    };
-    s.push(ch)
-}
-
-fn append_hex2<const N : usize>(s : &mut String<N>, num : u8) -> Result<(), ()> {
-    append_hex1(s, num >> 4)?;
-    append_hex1(s, num & 0xf)
-}
-
-#[derive(PartialEq, Eq, Copy, Clone, Debug)]
-#[cfg_attr(feature = "defmt", derive(defmt::Format))]
-enum ScanResult {
-    NAck,
-    Ack,
-    Reserved,
-    Error
-}
-
-fn format_scan_result<const N : usize>(msg : &mut String<N>, scan_result : [ScanResult; 128]) -> Result<(), ()> {
-    const MAX_ADDRESS : u8 = 128;
-    for i in 0..MAX_ADDRESS/16 {
-        let base = i*16;
-        append_hex2(msg, base)?;
-        msg.push_str(":")?;
-        for j in 0..16 {
-            msg.push_str(" ")?;
-            match scan_result[(base+j) as usize] {
-                ScanResult::Ack => append_hex2(msg, base+j)?,
-                ScanResult::NAck => msg.push_str("--")?,
-                ScanResult::Reserved => msg.push_str("rr")?,
-                ScanResult::Error => msg.push_str("!!")?,
-            }
-        }
-        msg.push_str("\n")?;
-    }
-    Ok(())
-}
-
-
-// adapted from arduino-pico:
-// https://github.com/earlephilhower/arduino-pico/blob/6e52b72523b11470b2ad2b0578ca1500be238108/libraries/Wire/src/Wire.cpp#L257
-// This seems to be the only way to do zero-length writes:
-// see https://github.com/raspberrypi/pico-sdk/issues/238
-async fn handle_clock_stretching<'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 {
-        Either::First(_) => Ok(()),
-        Either::Second(_) => Err(Abort(ArbitrationLoss)),
-    }
-}
-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();
-    handle_clock_stretching(scl).await?;
-    sda.set_as_output();
-    Timer::after(delay).await;
-    scl.set_as_output();
-    Timer::after(delay).await;
-    for _ 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;
-        handle_clock_stretching(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();
-    handle_clock_stretching(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>(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 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;
-
-    // abort if SCL is stuck low
-    handle_clock_stretching(&mut scl).await?;
-
-    // handle SDA being stuck low
-    // (This can happen for very simple I2C devices that only use SCL and don't have any other clocks that
-    // they can use to handle timeouts, e.g. simple port expanders.)
-    if sda.is_low() {
-        // I think I have read that DW_apb_i2c does nine clocks. This seems resonable so let's do the same.
-        for _ in 0..9 {
-            scl.set_as_output();
-            Timer::after(delay).await;
-            scl.set_as_input();
-            Timer::after(delay).await;
-
-            if sda.is_high() {
-                break;
-            }
-        }
-
-        if sda.is_low() || scl.is_low() {
-            // There is isn't much that we can do here. A start condition would reset the state but we cannot
-            // generate it while one of the lines is stuck low.
-            return Err(Abort(ArbitrationLoss))
-        }
-
-        // SDA is high, again. Good. We will generate a start condition soon. This should abort any
-        // partial transfers that some devices might think are ongoing. We have already waited for half
-        // a clock so we are good to go.
-    }
-
-    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;
-}
+use heizung::i2c_scan::{self, ScanResultForBus};
+use heizung::rs485::RS485;
 
 #[embassy_executor::task]
 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::*;
-
-    const MAX_ADDRESS : u8 = 128;
-    let mut prev_scan_result = [ScanResult::Reserved; MAX_ADDRESS as usize];
+    let mut prev_scan_result = ScanResultForBus::new();
     loop {
         if true {
             let _i2c = i2c::I2c::new_async(&mut i2c_peri, &mut scl, &mut sda, &mut i2c_irq, i2c_config);
         }
 
-        let mut scan_result = [ScanResult::Error; MAX_ADDRESS as usize];
-        for i in 0..MAX_ADDRESS {
-            scan_result[i as usize] = match i2c_write_zero_bytes::<_, _, 400_000>(i as u16, &mut scl, &mut sda).await {
-                Result::Ok(()) => ScanResult::Ack,
-                Result::Err(Abort(NoAcknowledge)) => ScanResult::NAck,
-                Result::Err(AddressReserved(_)) => ScanResult::Reserved,
-                Result::Err(e) => {
-                    warn!("I2C problem: {:02x}: {:?}", i, e);
-                    ScanResult::Error
-                }
-            }
-        }
-
-        if prev_scan_result.iter().zip(scan_result.iter()).any(|(x,y)| x != y) {
+        let scan_result = ScanResultForBus::scan(&mut scl, &mut sda).await;
+        if prev_scan_result != scan_result {
             prev_scan_result = scan_result;
-            const MSG_LEN : usize = (MAX_ADDRESS as usize)*3 + (MAX_ADDRESS as usize)/16*6;
+            const MSG_LEN : usize = i2c_scan::SCAN_RESULT_STRING_LENGTH;
             let mut msg = String::<MSG_LEN>::new();
-            match format_scan_result(&mut msg, scan_result) {
+            match scan_result.format(&mut msg) {
                 Ok(()) => info!("I2C scan result:\n{}", msg),
                 Err(()) => info!("I2C scan result: too long for our buffer!"),
             }
@@ -255,318 +70,10 @@ async fn beeper_task(pwm_channel: embassy_rp::peripherals::PWM_CH3, beeper_pin:
     }
 }
 
-
-fn pin_io<P: embassy_rp::gpio::Pin>(pin: &P) -> embassy_rp::pac::io::Gpio {
-    use embassy_rp::gpio::Bank;
-    use embassy_rp::pac;
-
-    let block = match pin.bank() {
-        Bank::Bank0 => pac::IO_BANK0,
-        Bank::Qspi => pac::IO_QSPI,
-    };
-    block.gpio(pin.pin() as _)
-}
-
-async fn debug_print_pio_addr(sm: embassy_rp::pac::pio::StateMachine) {
-    let mut prev = 42u8;
-    loop {
-        let addr = unsafe { sm.addr().read().addr() };
-        if prev != addr {
-            prev = addr;
-            info!("SM addr: {}", addr);
-        }
-        Timer::after(Duration::from_millis(200)).await;
-    }
-}
-
-mod dont_abort {
-    use core::future::*;
-    use core::pin::Pin;
-    use core::task::*;
-
-    pub struct DontAbort<A> {
-        a: A,
-        done: bool
-    }
-
-    pub struct DontAbortFuture<'a, A> {
-        inner: &'a mut DontAbort<A>
-    }
-
-    impl<A> DontAbort<A> {
-        pub fn new(a: A) -> DontAbort<A> {
-            DontAbort { a, done: false }
-        }
-
-        #[allow(dead_code)]
-        pub fn done(self: &Self) -> bool {
-            return self.done;
-        }
-
-        pub fn continue_wait<'a>(self: &'a mut Self) -> DontAbortFuture<'a, A> {
-            DontAbortFuture { inner: self }
-        }
-    }
-
-    impl<A: Unpin> Unpin for DontAbort<A> {}
-    impl<'a, A: Unpin> Unpin for DontAbortFuture<'a, A> {}
-
-    impl<'a, A> Future for DontAbortFuture<'a, A>
-    where
-        A: Future,
-    {
-        type Output = A::Output;
-
-        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
-            let this = unsafe { self.get_unchecked_mut() };
-            if this.inner.done {
-                panic!("Ready future is called again!");
-            }
-            let a = unsafe { Pin::new_unchecked(&mut this.inner.a) };
-            if let Poll::Ready(x) = a.poll(cx) {
-                this.inner.done = true;
-                return Poll::Ready(x);
-            }
-            Poll::Pending
-        }
-    }
-}
-
 #[embassy_executor::task]
-async fn uart_task(pio0: embassy_rp::peripherals::PIO0, rx_pin: embassy_rp::peripherals::PIN_17,
-        dma_channel: embassy_rp::peripherals::DMA_CH0,
-        mut rx: UartRx<'static, UART0, uart::Async>) {
-    let Pio {
-        mut common,
-        sm0: mut sm,
-        mut irq0,
-        ..
-    } = Pio::new(pio0);
-
-    sm.set_enable(false);
-
-    let prg_set_osr = pio_proc::pio_asm!(
-        ".origin 0",
-        "pull",
-        "hang:",
-        "jmp hang",
-    );
-
-    let prg = pio_proc::pio_asm!(
-        ".origin 0",
-
-        // Send zero to RX FIFO to signal start of next measurement.
-        ".wrap_target",
-        "timeout:",
-        "in null, 32",
-
-        // Wait for pin to be high for a longer time (UART is idle), copy waiting time to X
-        // and decrement until done, start again when pin changes.
-        "wait_idle:",
-        "wait 1 pin 0",
-        "mov x, osr",
-        "continue_wait_idle:",
-        "jmp pin still_one",
-        "jmp wait_idle",  // we got a zero, start again
-        "still_one:",
-        "jmp x--, continue_wait_idle",
-
-        // We have a long 1, i.e. UART is idle. Wait for start bit.
-        "have_long_high:",
-        "wait 0 pin 0",
-        "nop [4]",
-        "jmp pin wait_idle",  // abort if zero was just a glitch
-
-        // We want to measure 9 bits (start bit plus eight (or nine) data bits). However, we need
-        // different code for high and low so it's easier to measure 4x2 bits. Weuse Y as the loop counter.
-        "set y, 3",
-
-        // Start counting. We count down from the value in OSR (which is our timeout).
-        "measure_next:",
-        "mov x, osr",
-        "measure_low:",
-        "jmp pin changed_to_one",
-        //"nop",
-        "jmp x-- measure_low",
-        "jmp timeout",
-        "changed_to_one:",
-        "in x, 32"
-
-        // Start counting, again, but for a high pulse.
-        "mov x, osr",
-        "measure_high:",
-        "jmp pin still_high2",
-        "jmp changed_to_low",
-        "still_high2:",
-        "jmp x-- measure_high",
-        "jmp timeout",
-        "changed_to_low:",
-        "in x, 32",
-        "jmp y-- measure_next",
-
-        // We are done.
-        "irq set 0",
-        ".wrap",
-    );
-
-    let relocated = RelocatedProgram::new(&prg_set_osr.program);
-    let mut cfg = Config::default();
-    let loaded_program = common.load_program(&relocated);
-    cfg.use_program(&loaded_program, &[]);
-    const SM_FREQ: u32 = 125_000_000;
-    cfg.clock_divider = (U56F8!(125_000_000) / U56F8!(125_000_000 /* SM_FREQ */)).to_fixed();
-    cfg.shift_in = ShiftConfig {
-        auto_fill: true,
-        threshold: 32,
-        direction: ShiftDirection::Left,
-    };
-    cfg.shift_out = ShiftConfig {
-        auto_fill: false,
-        threshold: 32,
-        direction: ShiftDirection::Right,
-    };
-    let rx_pin_pio = common.make_pio_pin(unsafe { rx_pin.clone_unchecked() });
-    cfg.set_in_pins(&[&rx_pin_pio]);
-    cfg.set_jmp_pin(&rx_pin_pio);
-
-    sm.set_config(&cfg);
-    sm.set_enable(true);
-
-    // Timeout: Modbus wants 1.5 ms between frames so we make this a bit smaller. SM runs at 25 MHz
-    // but we need two clocks per loop.
-    let timeout_start_value = (SM_FREQ as f32 * 1.2e-3 / 2.) as u32;
-    sm.tx().push(timeout_start_value);
-    info!("timeout_start_value: {} = 0x{:08x}", timeout_start_value, timeout_start_value);
-
-    // switch to the real program and join FIFOs
-    unsafe { common.free_instr(loaded_program.used_memory); };
-    sm.set_enable(false);
-    let relocated = RelocatedProgram::new(&prg.program);
-    let loaded_program = common.load_program(&relocated);
-    cfg.use_program(&loaded_program, &[]);
-    cfg.fifo_join = FifoJoin::RxOnly;
-    sm.set_config(&cfg);
-    sm.set_enable(true);
-
-    // set rx pin function back to UART
-    // (PIO can always read and we don't want to write but embassy_rp cannot know that so it "helpfully" configured the pin for PIO function.)
-    unsafe {
-        pin_io(&rx_pin).ctrl().write(|w| w.set_funcsel(embassy_rp::pac::io::vals::Gpio17ctrlFuncsel::UART0_RX.0));
-    }
-
-    info!("Program size for auto-baud: {}", prg.program.code.len());
-
-    let mut dma_in_ref = dma_channel.into_ref();
-    let mut din = [42u32; 9];
-    let mut bit_index = 0;
-    let mut rx_buf = [0; 1];
-    let mut rx_future = dont_abort::DontAbort::new(rx.read(&mut rx_buf));
-    loop {
-        let x = select4(
-            irq0.wait(),
-            sm.rx().dma_pull(dma_in_ref.reborrow(), &mut din),
-            debug_print_pio_addr(embassy_rp::pac::PIO0.sm(0)),
-            rx_future.continue_wait(),
-        ).await;
-        match x {
-            Either4::First(_) => {
-                let mut sum = 0;
-                let mut first = 0;
-                let mut ok = false;
-                let mut got_to_eight = false;
-                for i in 0..din.len() {
-                    if din[i] == 0 {
-                        bit_index = 0;
-                        info!("SM in {}: {:08x} -> start", i, din[i]);
-                    } else {
-                        let mut delay = timeout_start_value - din[i];
-                        if bit_index == 0 {
-                            delay += 7;
-                        } else if bit_index%2 == 1 {
-                            //delay += 4;
-                            delay += 3;
-                        } else {
-                            delay += 3;
-                        }
-                        let millis = (delay) as f32 / SM_FREQ as f32 * 1000. * 2.;
-                        let baud = 1000. / millis;
-                        info!("SM in {} ({}): {:08x} -> {} -> {} ms -> {}", i, bit_index, din[i], delay, millis, baud);
-                        if bit_index == 0 {
-                            sum = 0;
-                            first = delay;
-                            ok = true;
-                        } else if delay < first-first/4 || delay > first+first/4 {
-                            ok = false;
-                        }
-                        sum += delay;
-                        bit_index += 1;
-                        got_to_eight = bit_index == 8;
-                    }
-                }
-
-                for _ in bit_index..8 {
-                    if let Some(x) = sm.rx().try_pull() {
-                        let mut delay = timeout_start_value - x;
-                        if bit_index == 0 {
-                            delay += 7;
-                        } else if bit_index%2 == 1 {
-                            //delay += 4;
-                            delay += 3;
-                        } else {
-                            delay += 3;
-                        }
-                        let millis = (delay) as f32 / SM_FREQ as f32 * 1000. * 2.;
-                        let baud = 1000. / millis;
-                        info!("SM in ({}): {:08x} -> {} -> {} ms -> {}", bit_index, x, delay, millis, baud);
-                        if bit_index == 0 {
-                            sum = 0;
-                            first = delay;
-                            ok = true;
-                        } else if delay < first-first/4 || delay > first+first/4 {
-                            ok = false;
-                        }
-                        sum += delay;
-                        bit_index += 1;
-                        got_to_eight = bit_index == 8;
-                    } else {
-                        break
-                    }
-                }
-
-                if got_to_eight && ok {
-                    let millis = sum as f32 / SM_FREQ as f32 * 1000. * 2. / 8.;
-                    let baud = 1000. / millis;
-                    info!("Guessed baud rate: {}", baud);
-                }
-            },
-            Either4::Second(_) => {
-                if false {
-                    for i in 0..din.len() {
-                        if din[i] == 0 {
-                            bit_index = 0
-                        } else {
-                            bit_index += 1
-                        }
-                        info!("SM in {}: {:08x}", i, din[i]);
-                    }
-                }
-            },
-            Either4::Fourth(x) => {
-                drop(rx_future);
-                match x {
-                    Result::Ok(()) => {
-                        info!("RX {:?}", rx_buf);
-                    },
-                    Result::Err(e) => {
-                        info!("RX error {:?}", e);
-                    },
-                }
-                rx_future = dont_abort::DontAbort::new(rx.read(&mut rx_buf));
-            },
-            _ => {
-            },
-        }
-    }
+async fn uart_task(this: RS485) {
+    info!("abc");
+    this.run_task().await;
 }
 
 #[embassy_executor::main]
@@ -595,7 +102,6 @@ async fn main(spawner: Spawner) {
     let _tx_en = p.PIN_15;
     let _tx = p.PIN_16;
     let rx = p.PIN_17;
-    let rx_for_autobaud = unsafe { rx.clone_unchecked() };  //FIXME move UART and auto-baud into same task
     let _matrix_in2 = Input::new(p.PIN_18, Pull::Up);
     let _reed3 = Input::new(p.PIN_19, Pull::Up);
     let _reed4 = Input::new(p.PIN_20, Pull::Up);
@@ -618,14 +124,8 @@ async fn main(spawner: Spawner) {
     let mut uart_config = uart::Config::default();
     uart_config.baudrate = 19200;
     uart_config.parity = Parity::ParityEven;
-    let uart_rx = UartRx::new(
-        p.UART0,
-        rx,
-        interrupt::take!(UART0_IRQ),
-        p.DMA_CH1,
-        uart_config,
-    );
-    unwrap!(spawner.spawn(uart_task(p.PIO0, rx_for_autobaud, p.DMA_CH0, uart_rx)));
+    let rs485 = RS485::new(p.UART0, rx, interrupt::take!(UART0_IRQ), p.DMA_CH1, uart_config, p.PIO0, p.DMA_CH0);
+    unwrap!(spawner.spawn(uart_task(rs485)));
 
     if false {
         unwrap!(spawner.spawn(beeper_task(p.PWM_CH3, led_r)));
diff --git a/firmware/rust1/src/dont_abort.rs b/firmware/rust1/src/dont_abort.rs
new file mode 100644
index 0000000000000000000000000000000000000000..29544a119a37cc0d7a4b51f80031335ceb554166
--- /dev/null
+++ b/firmware/rust1/src/dont_abort.rs
@@ -0,0 +1,50 @@
+use core::future::*;
+use core::pin::Pin;
+use core::task::*;
+
+pub struct DontAbort<A> {
+    a: A,
+    done: bool
+}
+
+pub struct DontAbortFuture<'a, A> {
+    inner: &'a mut DontAbort<A>
+}
+
+impl<A> DontAbort<A> {
+    pub fn new(a: A) -> DontAbort<A> {
+        DontAbort { a, done: false }
+    }
+
+    #[allow(dead_code)]
+    pub fn done(self: &Self) -> bool {
+        return self.done;
+    }
+
+    pub fn continue_wait<'a>(self: &'a mut Self) -> DontAbortFuture<'a, A> {
+        DontAbortFuture { inner: self }
+    }
+}
+
+impl<A: Unpin> Unpin for DontAbort<A> {}
+impl<'a, A: Unpin> Unpin for DontAbortFuture<'a, A> {}
+
+impl<'a, A> Future for DontAbortFuture<'a, A>
+where
+    A: Future,
+{
+    type Output = A::Output;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let this = unsafe { self.get_unchecked_mut() };
+        if this.inner.done {
+            panic!("Ready future is called again!");
+        }
+        let a = unsafe { Pin::new_unchecked(&mut this.inner.a) };
+        if let Poll::Ready(x) = a.poll(cx) {
+            this.inner.done = true;
+            return Poll::Ready(x);
+        }
+        Poll::Pending
+    }
+}
diff --git a/firmware/rust1/src/i2c_scan.rs b/firmware/rust1/src/i2c_scan.rs
new file mode 100644
index 0000000000000000000000000000000000000000..78bb44d445438730683326cb58130d7a396cc43c
--- /dev/null
+++ b/firmware/rust1/src/i2c_scan.rs
@@ -0,0 +1,217 @@
+use defmt::*;
+use {defmt_rtt as _, panic_probe as _};
+use heapless::String;
+
+use embassy_futures::select::*;
+use embassy_time::{Duration, Timer};
+use embassy_rp::gpio::{self, Pull, Flex};
+use embassy_rp::i2c;
+
+fn append_hex1<const N : usize>(s : &mut String<N>, num : u8) -> Result<(), ()> {
+    let ch = if num < 10 {
+        ('0' as u8 + num) as char
+    } else {
+        ('a' as u8 + num - 10) as char
+    };
+    s.push(ch)
+}
+
+fn append_hex2<const N : usize>(s : &mut String<N>, num : u8) -> Result<(), ()> {
+    append_hex1(s, num >> 4)?;
+    append_hex1(s, num & 0xf)
+}
+
+#[derive(PartialEq, Eq, Copy, Clone, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub enum ScanResult {
+    NAck,
+    Ack,
+    Error
+}
+
+// adapted from arduino-pico:
+// https://github.com/earlephilhower/arduino-pico/blob/6e52b72523b11470b2ad2b0578ca1500be238108/libraries/Wire/src/Wire.cpp#L257
+// This seems to be the only way to do zero-length writes:
+// see https://github.com/raspberrypi/pico-sdk/issues/238
+async fn handle_clock_stretching<'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 {
+        Either::First(_) => Ok(()),
+        Either::Second(_) => Err(Abort(ArbitrationLoss)),
+    }
+}
+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();
+    handle_clock_stretching(scl).await?;
+    sda.set_as_output();
+    Timer::after(delay).await;
+    scl.set_as_output();
+    Timer::after(delay).await;
+    for _ 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;
+        handle_clock_stretching(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();
+    handle_clock_stretching(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>(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 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;
+
+    // abort if SCL is stuck low
+    handle_clock_stretching(&mut scl).await?;
+
+    // handle SDA being stuck low
+    // (This can happen for very simple I2C devices that only use SCL and don't have any other clocks that
+    // they can use to handle timeouts, e.g. simple port expanders.)
+    if sda.is_low() {
+        // I think I have read that DW_apb_i2c does nine clocks. This seems resonable so let's do the same.
+        for _ in 0..9 {
+            scl.set_as_output();
+            Timer::after(delay).await;
+            scl.set_as_input();
+            Timer::after(delay).await;
+
+            if sda.is_high() {
+                break;
+            }
+        }
+
+        if sda.is_low() || scl.is_low() {
+            // There is isn't much that we can do here. A start condition would reset the state but we cannot
+            // generate it while one of the lines is stuck low.
+            return Err(Abort(ArbitrationLoss))
+        }
+
+        // SDA is high, again. Good. We will generate a start condition soon. This should abort any
+        // partial transfers that some devices might think are ongoing. We have already waited for half
+        // a clock so we are good to go.
+    }
+
+    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;
+}
+
+#[derive(PartialEq, Eq, Copy, Clone, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct ScanResultForBus([ScanResult; 128]);
+
+pub const MAX_ADDRESS : u8 = 128;
+pub const SCAN_RESULT_STRING_LENGTH : usize = (MAX_ADDRESS as usize)*3 + (MAX_ADDRESS as usize)/16*6;
+
+impl ScanResultForBus {
+    pub fn new() -> ScanResultForBus {
+        ScanResultForBus([ScanResult::Error; MAX_ADDRESS as usize])
+    }
+
+    pub async fn scan<SCL: gpio::Pin, SDA: gpio::Pin>(scl: &mut SCL, sda: &mut SDA) -> ScanResultForBus {
+        let mut results = Self::new();
+        results.scan_i2c(scl, sda).await;
+        return results;
+    }
+
+    pub async fn scan_i2c<SCL: gpio::Pin, SDA: gpio::Pin>(self: &mut Self, scl: &mut SCL, sda: &mut SDA) {
+        use i2c::Error::*;
+        use i2c::AbortReason::*;
+
+        for i in 0..MAX_ADDRESS {
+            self.0[i as usize] = match i2c_write_zero_bytes::<_, _, 400_000>(i as u16, scl, sda).await {
+                Result::Ok(()) => ScanResult::Ack,
+                Result::Err(Abort(NoAcknowledge)) => ScanResult::NAck,
+                Result::Err(e) => {
+                    warn!("I2C problem: {:02x}: {:?}", i, e);
+                    ScanResult::Error
+                }
+            }
+        }
+    }    
+
+    pub fn format<const N : usize>(self: &Self, msg : &mut String<N>) -> Result<(), ()> {
+        self::assert!(N >= SCAN_RESULT_STRING_LENGTH);
+
+        for i in 0..MAX_ADDRESS/16 {
+            let base = i*16;
+            append_hex2(msg, base)?;
+            msg.push_str(":")?;
+            for j in 0..16 {
+                msg.push_str(" ")?;
+                match self.0[(base+j) as usize] {
+                    ScanResult::Ack => append_hex2(msg, base+j)?,
+                    ScanResult::NAck => msg.push_str("--")?,
+                    ScanResult::Error => msg.push_str("!!")?,
+                }
+            }
+            msg.push_str("\n")?;
+        }
+        Ok(())
+    }
+}
+
+/*
+impl PartialEq for ScanResultForBus {
+    fn eq(self: &Self, other: &Self) -> bool {
+        self.0.iter().zip(other.0.iter()).all(|(x,y)| x == y)
+    }
+}
+
+impl Eq for ScanResultForBus { }
+ */
diff --git a/firmware/rust1/src/lib.rs b/firmware/rust1/src/lib.rs
index 26e5f5c2c222b0247e1981eb41c010e1c068ce8a..c969570ef887d1bea8b906fa4264985119554e79 100644
--- a/firmware/rust1/src/lib.rs
+++ b/firmware/rust1/src/lib.rs
@@ -1,2 +1,5 @@
 #![no_std]
 pub mod rp;
+pub mod dont_abort;
+pub mod i2c_scan;
+pub mod rs485;
diff --git a/firmware/rust1/src/rs485.rs b/firmware/rust1/src/rs485.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e8b9d8a9dd4980129e486a75821b28437c0b2fea
--- /dev/null
+++ b/firmware/rust1/src/rs485.rs
@@ -0,0 +1,299 @@
+use defmt::*;
+use embassy_futures::select::*;
+use embassy_rp::gpio;
+use embassy_rp::pac;
+use embassy_rp::interrupt::UART0_IRQ;
+use embassy_time::{Duration, Timer};
+use embassy_embedded_hal::SetConfig;
+use embassy_rp::peripherals::{self, UART0};
+use embassy_rp::uart::{self, UartRx};
+use embassy_rp::pio::{Config, Pio, ShiftConfig, ShiftDirection, FifoJoin};
+use embassy_rp::relocate::RelocatedProgram;
+use embassy_rp::Peripheral;
+use {defmt_rtt as _, panic_probe as _};
+use fixed::traits::ToFixed;
+use fixed_macro::types::U56F8;
+
+use crate::dont_abort::DontAbort;
+
+pub struct RS485 {
+    pio: peripherals::PIO0,
+    rx_pin: peripherals::PIN_17,
+    dma_channel: peripherals::DMA_CH0,
+    rx: UartRx<'static, UART0, uart::Async>
+}
+
+fn pin_io<P: gpio::Pin>(pin: &P) -> pac::io::Gpio {
+    use gpio::Bank;
+
+    let block = match pin.bank() {
+        Bank::Bank0 => pac::IO_BANK0,
+        Bank::Qspi => pac::IO_QSPI,
+    };
+    block.gpio(pin.pin() as _)
+}
+
+async fn debug_print_pio_addr(sm: pac::pio::StateMachine) {
+    let mut prev = 42u8;
+    loop {
+        let addr = unsafe { sm.addr().read().addr() };
+        if prev != addr {
+            prev = addr;
+            info!("SM addr: {}", addr);
+        }
+        Timer::after(Duration::from_millis(200)).await;
+    }
+}
+
+impl RS485 {
+    pub fn new(
+            uart: UART0, rx: peripherals::PIN_17, rx_irq: UART0_IRQ, rx_dma_channel: peripherals::DMA_CH1, uart_config: uart::Config,
+            pio: peripherals::PIO0, dma_channel: peripherals::DMA_CH0
+        ) -> RS485 {
+        // SAFETY: The auto-baud will only read from this pin and we will set it back to UART mode after initialising the PIO.
+        let rx_pin_for_autobaud = unsafe { rx.clone_unchecked() };
+
+        let uart_rx = UartRx::new(
+            uart,
+            rx,
+            rx_irq,
+            rx_dma_channel,
+            uart_config,
+        );
+        RS485 { pio, rx_pin: rx_pin_for_autobaud, dma_channel, rx: uart_rx }
+    }
+
+    pub async fn run_task(mut self: Self) {
+        let Pio {
+            mut common,
+            sm0: mut sm,
+            mut irq0,
+            ..
+        } = Pio::new(self.pio);
+
+        sm.set_enable(false);
+
+        let prg_set_osr = pio_proc::pio_asm!(
+            ".origin 0",
+            "pull",
+            "hang:",
+            "jmp hang",
+        );
+
+        let prg = pio_proc::pio_asm!(
+            ".origin 0",
+
+            // Send zero to RX FIFO to signal start of next measurement.
+            ".wrap_target",
+            "timeout:",
+            "in null, 32",
+
+            // Wait for pin to be high for a longer time (UART is idle), copy waiting time to X
+            // and decrement until done, start again when pin changes.
+            "wait_idle:",
+            "wait 1 pin 0",
+            "mov x, osr",
+            "continue_wait_idle:",
+            "jmp pin still_one",
+            "jmp wait_idle",  // we got a zero, start again
+            "still_one:",
+            "jmp x--, continue_wait_idle",
+
+            // We have a long 1, i.e. UART is idle. Wait for start bit.
+            "have_long_high:",
+            "wait 0 pin 0",
+            "nop [4]",
+            "jmp pin wait_idle",  // abort if zero was just a glitch
+
+            // We want to measure 9 bits (start bit plus eight (or nine) data bits). However, we need
+            // different code for high and low so it's easier to measure 4x2 bits. Weuse Y as the loop counter.
+            "set y, 3",
+
+            // Start counting. We count down from the value in OSR (which is our timeout).
+            "measure_next:",
+            "mov x, osr",
+            "measure_low:",
+            "jmp pin changed_to_one",
+            //"nop",
+            "jmp x-- measure_low",
+            "jmp timeout",
+            "changed_to_one:",
+            "in x, 32"
+
+            // Start counting, again, but for a high pulse.
+            "mov x, osr",
+            "measure_high:",
+            "jmp pin still_high2",
+            "jmp changed_to_low",
+            "still_high2:",
+            "jmp x-- measure_high",
+            "jmp timeout",
+            "changed_to_low:",
+            "in x, 32",
+            "jmp y-- measure_next",
+
+            // We are done.
+            "irq set 0",
+            ".wrap",
+        );
+
+        let relocated = RelocatedProgram::new(&prg_set_osr.program);
+        let mut cfg = Config::default();
+        let loaded_program = common.load_program(&relocated);
+        cfg.use_program(&loaded_program, &[]);
+        const SM_FREQ: u32 = 125_000_000;
+        cfg.clock_divider = (U56F8!(125_000_000) / U56F8!(125_000_000 /* SM_FREQ */)).to_fixed();
+        cfg.shift_in = ShiftConfig {
+            auto_fill: true,
+            threshold: 32,
+            direction: ShiftDirection::Left,
+        };
+        cfg.shift_out = ShiftConfig {
+            auto_fill: false,
+            threshold: 32,
+            direction: ShiftDirection::Right,
+        };
+        let rx_pin_pio = common.make_pio_pin(unsafe { self.rx_pin.clone_unchecked() });
+        cfg.set_in_pins(&[&rx_pin_pio]);
+        cfg.set_jmp_pin(&rx_pin_pio);
+
+        sm.set_config(&cfg);
+        sm.set_enable(true);
+
+        // Timeout: Modbus wants 1.5 ms between frames so we make this a bit smaller. SM runs at 25 MHz
+        // but we need two clocks per loop.
+        let timeout_start_value = (SM_FREQ as f32 * 1.2e-3 / 2.) as u32;
+        sm.tx().push(timeout_start_value);
+        info!("timeout_start_value: {} = 0x{:08x}", timeout_start_value, timeout_start_value);
+
+        // switch to the real program and join FIFOs
+        unsafe { common.free_instr(loaded_program.used_memory); };
+        sm.set_enable(false);
+        let relocated = RelocatedProgram::new(&prg.program);
+        let loaded_program = common.load_program(&relocated);
+        cfg.use_program(&loaded_program, &[]);
+        cfg.fifo_join = FifoJoin::RxOnly;
+        sm.set_config(&cfg);
+        sm.set_enable(true);
+
+        // set rx pin function back to UART
+        // (PIO can always read and we don't want to write but embassy_rp cannot know that so it "helpfully" configured the pin for PIO function.)
+        unsafe {
+            pin_io(&self.rx_pin).ctrl().write(|w| w.set_funcsel(embassy_rp::pac::io::vals::Gpio17ctrlFuncsel::UART0_RX.0));
+        }
+
+        info!("Program size for auto-baud: {}", prg.program.code.len());
+
+        let mut dma_in_ref = self.dma_channel.into_ref();
+        let mut din = [42u32; 9];
+        let mut bit_index = 0;
+        let mut rx_buf = [0; 1];
+        let mut rx_future = DontAbort::new(self.rx.read(&mut rx_buf));
+        loop {
+            let x = select4(
+                irq0.wait(),
+                sm.rx().dma_pull(dma_in_ref.reborrow(), &mut din),
+                debug_print_pio_addr(embassy_rp::pac::PIO0.sm(0)),
+                rx_future.continue_wait(),
+            ).await;
+            match x {
+                Either4::First(_) => {
+                    let mut sum = 0;
+                    let mut first = 0;
+                    let mut ok = false;
+                    let mut got_to_eight = false;
+                    for i in 0..din.len() {
+                        if din[i] == 0 {
+                            bit_index = 0;
+                            info!("SM in {}: {:08x} -> start", i, din[i]);
+                        } else {
+                            let mut delay = timeout_start_value - din[i];
+                            if bit_index == 0 {
+                                delay += 7;
+                            } else if bit_index%2 == 1 {
+                                //delay += 4;
+                                delay += 3;
+                            } else {
+                                delay += 3;
+                            }
+                            let millis = (delay) as f32 / SM_FREQ as f32 * 1000. * 2.;
+                            let baud = 1000. / millis;
+                            info!("SM in {} ({}): {:08x} -> {} -> {} ms -> {}", i, bit_index, din[i], delay, millis, baud);
+                            if bit_index == 0 {
+                                sum = 0;
+                                first = delay;
+                                ok = true;
+                            } else if delay < first-first/4 || delay > first+first/4 {
+                                ok = false;
+                            }
+                            sum += delay;
+                            bit_index += 1;
+                            got_to_eight = bit_index == 8;
+                        }
+                    }
+
+                    for _ in bit_index..8 {
+                        if let Some(x) = sm.rx().try_pull() {
+                            let mut delay = timeout_start_value - x;
+                            if bit_index == 0 {
+                                delay += 7;
+                            } else if bit_index%2 == 1 {
+                                //delay += 4;
+                                delay += 3;
+                            } else {
+                                delay += 3;
+                            }
+                            let millis = (delay) as f32 / SM_FREQ as f32 * 1000. * 2.;
+                            let baud = 1000. / millis;
+                            info!("SM in ({}): {:08x} -> {} -> {} ms -> {}", bit_index, x, delay, millis, baud);
+                            if bit_index == 0 {
+                                sum = 0;
+                                first = delay;
+                                ok = true;
+                            } else if delay < first-first/4 || delay > first+first/4 {
+                                ok = false;
+                            }
+                            sum += delay;
+                            bit_index += 1;
+                            got_to_eight = bit_index == 8;
+                        } else {
+                            break
+                        }
+                    }
+
+                    if got_to_eight && ok {
+                        let millis = sum as f32 / SM_FREQ as f32 * 1000. * 2. / 8.;
+                        let baud = 1000. / millis;
+                        info!("Guessed baud rate: {}", baud);
+                    }
+                },
+                Either4::Second(_) => {
+                    if false {
+                        for i in 0..din.len() {
+                            if din[i] == 0 {
+                                bit_index = 0
+                            } else {
+                                bit_index += 1
+                            }
+                            info!("SM in {}: {:08x}", i, din[i]);
+                        }
+                    }
+                },
+                Either4::Fourth(x) => {
+                    drop(rx_future);
+                    match x {
+                        Result::Ok(()) => {
+                            info!("RX {:?}", rx_buf);
+                        },
+                        Result::Err(e) => {
+                            info!("RX error {:?}", e);
+                        },
+                    }
+                    rx_future = DontAbort::new(self.rx.read(&mut rx_buf));
+                },
+                _ => {
+                },
+            }
+        }
+    }
+}