diff --git a/firmware/rust1/Cargo.toml b/firmware/rust1/Cargo.toml
index fefd5c0e667cb2a15986204f40697505e9f5a802..9936fa4a09a1f17f89799cdf2b1a15e7dc03010b 100644
--- a/firmware/rust1/Cargo.toml
+++ b/firmware/rust1/Cargo.toml
@@ -3,6 +3,8 @@ edition = "2021"
 name = "heizung"
 version = "0.1.0"
 license = "MIT OR Apache-2.0"
+homepage = "https://git.c3pb.de/c3pb/heizung/-/tree/main/firmware/rust1"
+description = "Heating control for Subraum"
 
 
 [features]
diff --git a/firmware/rust1/src/bin/heizung.rs b/firmware/rust1/src/bin/heizung.rs
index 371d6c9936fadde785e0d56f338c359541bd6558..0d15bdfe0caa22a9ac2f25448ed19b68304327c5 100644
--- a/firmware/rust1/src/bin/heizung.rs
+++ b/firmware/rust1/src/bin/heizung.rs
@@ -699,6 +699,8 @@ fn init_early() {
 
 
 // Add program info.
+// Show with: picotool info heizung-release.uf2 -a
+// or `picotool info` if actual hardware is connected in bootloader mode
 #[used]
 #[link_section = ".program_info"]
 pub static PROGRAM_INFO: [u8; 4096] = {
@@ -708,7 +710,23 @@ pub static PROGRAM_INFO: [u8; 4096] = {
     const PROGRAM_INFO_TABLE_SIZE: u32 = 128;  // one pointer (4 bytes) for each piece of data, all must be valid pointers
     const PROGRAM_INFO_COPY_TABLE_SIZE: u32 = 128;  // 3*u32 for each entry, picotool stops after 10 entries
 
+    #[cfg(debug_assertions)]
+    let build_type = "Debug";
+    #[cfg(not(debug_assertions))]
+    let build_type = "Release";
+
+    // We can fill in some values from info that is provided by Cargo.
+    // see https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
+
     ProgramInfoBuilder::new(PROGRAM_INFO_FLASH_OFFSET, PROGRAM_INFO_TABLE_SIZE, PROGRAM_INFO_COPY_TABLE_SIZE)
-        .program_name("Test 123")
+        .program_name("subraum-heizung")
+        .program_description(env!("CARGO_PKG_DESCRIPTION"))
+        .program_version_string(env!("CARGO_PKG_VERSION"))
+        .program_url(env!("CARGO_PKG_HOMEPAGE"))
+        .board("subraum-heizung base v1.0")
+        .sdk_version("Embassy")
+        .program_build_attribute(build_type)
+        //FIXME should be based on what is selected for embassy-rp crate
+        .boot2_name("boot2_w25q080")
         .build()
 };
diff --git a/firmware/rust1/src/program_info.rs b/firmware/rust1/src/program_info.rs
index de20db1427d422dc6d590747eb6e2847b1a0e6a2..503c5fe014398e20ba227a4cfd0f0d8ca649dd52 100644
--- a/firmware/rust1/src/program_info.rs
+++ b/firmware/rust1/src/program_info.rs
@@ -1,4 +1,3 @@
-
 pub struct ProgramInfoBuilder {
     addr: u32,
     buf: [u8; 4096],
@@ -8,6 +7,16 @@ pub struct ProgramInfoBuilder {
     data_end_offset: usize,
 }
 
+#[repr(u16)]
+#[allow(non_camel_case_types)]
+pub enum GroupFlags {
+    NONE            = 0x0000,
+    SHOW_IF_EMPTY   = 0x0001,  // default is to hide
+    SEPARATE_COMMAS = 0x0002,  // default is newlines
+    SORT_ALPHA      = 0x0004,  // default is no sort
+    ADVANCED        = 0x0008,  // if set, then only shown in say info -a
+}
+
 const fn copy(mut buf: [u8; 4096], offset: usize, source: &[u8], source_offset: usize) -> [u8; 4096] {
     if source_offset >= source.len() {
         buf
@@ -129,9 +138,9 @@ impl ProgramInfoBuilder {
             .push(((value >> 24) & 0xff) as u8)
     }
 
-    const fn push_raspberry_tag(self: Self) -> Self {
-        self.push('R' as u8)
-            .push('P' as u8)
+    const fn push_tag(self: Self, tag: [char; 2]) -> Self {
+        self.push(tag[0] as u8)
+            .push(tag[1] as u8)
     }
 
     const fn push_extra_data(mut self: Self, value: &[u8]) -> (Self, u32) {
@@ -149,7 +158,7 @@ impl ProgramInfoBuilder {
 
     //NOTE This will push the address and then add the string so this must be the last item of the data record.
     const fn push_string(mut self: Self, value: &str) -> Self {
-        if self.data_pos - self.info_pos < value.len() {
+        if self.data_pos - self.info_pos < value.len() + 1 {
             core::panic!("ProgramInfoBuilder: too much data")
         }
 
@@ -170,34 +179,179 @@ impl ProgramInfoBuilder {
         self
     }
 
-    const BINARY_INFO_TYPE_ID_AND_INT: u16 = 5;
-    const BINARY_INFO_TYPE_ID_AND_STRING: u16 = 6;
+    const fn append_to_last_string(mut self: Self, value: &str) -> Self {
+        if self.data_pos - self.info_pos < value.len() {
+            core::panic!("ProgramInfoBuilder: too much data")
+        }
+
+        self.data_pos -= 1;
+        (self, _) = self.push_extra_data(value.as_bytes());
+        (self, _) = self.push_extra_data(&[0]);
+        
+        self
+    }
 
-    pub const BINARY_INFO_ID_RP_PROGRAM_NAME: u32 = 0x02031c86;
-    pub const BINARY_INFO_ID_RP_PROGRAM_VERSION_STRING: u32 = 0x11a9bc3a;
-    pub const BINARY_INFO_ID_RP_PROGRAM_BUILD_DATE_STRING: u32 = 0x9da22254;
-    pub const BINARY_INFO_ID_RP_BINARY_END: u32 = 0x68f465de;
-    pub const BINARY_INFO_ID_RP_PROGRAM_URL: u32 = 0x1856239a;
-    pub const BINARY_INFO_ID_RP_PROGRAM_DESCRIPTION: u32 = 0xb6a07c19;
-    pub const BINARY_INFO_ID_RP_PROGRAM_FEATURE: u32 = 0xa1f4b453;
-    pub const BINARY_INFO_ID_RP_PROGRAM_BUILD_ATTRIBUTE: u32 = 0x4275f0d3;
-    pub const BINARY_INFO_ID_RP_SDK_VERSION: u32 = 0x5360b3ab;
-    pub const BINARY_INFO_ID_RP_PICO_BOARD: u32 = 0xb63cffbb;
-    pub const BINARY_INFO_ID_RP_BOOT2_NAME: u32 = 0x7f8882e1;
+    const fn bi_int(self: Self, tag: [char; 2], id: u32, value: u32) -> Self {
+        self.push_current_info_ptr()
+            .push_u16(Self::BINARY_INFO_TYPE_ID_AND_INT)
+            .push_tag(tag)
+            .push_u32(id)
+            .push_u32(value)
+    }
 
-    pub const fn program_name(self: Self, value: &str) -> Self {
+    const fn bi_string(self: Self, tag: [char; 2], id: u32, value: &str) -> Self {
         self.push_current_info_ptr()
             .push_u16(Self::BINARY_INFO_TYPE_ID_AND_STRING)
-            .push_raspberry_tag()
-            .push_u32(Self::BINARY_INFO_ID_RP_PROGRAM_NAME)
+            .push_tag(tag)
+            .push_u32(id)
             .push_string(value)
     }
 
-    pub const fn binary_end_address(self: Self, value: u32) -> Self {
+    const fn bi_named_group(self: Self, tag: [char; 2], id: u32, flags: u16, group_tag: u16, group_id: u32, label: &str) -> Self {
         self.push_current_info_ptr()
-            .push_u16(Self::BINARY_INFO_TYPE_ID_AND_INT)
-            .push_raspberry_tag()
-            .push_u32(Self::BINARY_INFO_ID_RP_BINARY_END)
+            .push_u16(Self::BINARY_INFO_TYPE_NAMED_GROUP)
+            .push_tag(tag)
+            .push_u32(id)
+            .push_u16(flags)
+            .push_u16(group_tag)
+            .push_u32(group_id)
+            .push_string(label)
+    }
+
+    const fn bi_encoded_pins_with_func(self: Self, value: u32) -> Self {
+        self.push_current_info_ptr()
+            .push_u16(Self::BINARY_INFO_TYPE_PINS_WITH_FUNC)
+            .push_tag(Self::BINARY_INFO_TAG_RASPBERRY_PI)
             .push_u32(value)
     }
+
+    const fn bi_pins_with_names(self: Self, pin_mask: u32, label: &str) -> Self {
+        self.push_current_info_ptr()
+            .push_u16(Self::BINARY_INFO_TYPE_PINS_WITH_NAME)
+            .push_tag(Self::BINARY_INFO_TAG_RASPBERRY_PI)
+            .push_u32(pin_mask)
+            .push_string(label)
+    }
+    
+    const BINARY_INFO_TAG_RASPBERRY_PI: [char; 2] = ['R', 'P'];
+
+    #[allow(unused)] const BINARY_INFO_TYPE_RAW_DATA: u16 = 1;
+    #[allow(unused)] const BINARY_INFO_TYPE_SIZED_DATA: u16 = 2;
+    #[allow(unused)] const BINARY_INFO_TYPE_BINARY_INFO_LIST_ZERO_TERMINATED: u16 = 3;
+    #[allow(unused)] const BINARY_INFO_TYPE_BSON: u16 = 4;
+    #[allow(unused)] const BINARY_INFO_TYPE_ID_AND_INT: u16 = 5;
+    #[allow(unused)] const BINARY_INFO_TYPE_ID_AND_STRING: u16 = 6;
+    #[allow(unused)] const BINARY_INFO_TYPE_BLOCK_DEVICE: u16 = 7;
+    #[allow(unused)] const BINARY_INFO_TYPE_PINS_WITH_FUNC: u16 = 8;
+    #[allow(unused)] const BINARY_INFO_TYPE_PINS_WITH_NAME: u16 = 9;
+    #[allow(unused)] const BINARY_INFO_TYPE_PINS_WITH_NAMES: u16 = 9;
+    #[allow(unused)] const BINARY_INFO_TYPE_NAMED_GROUP: u16 = 10;
+
+    #[allow(unused)] const BINARY_INFO_ID_RP_PROGRAM_NAME: u32 = 0x02031c86;
+    #[allow(unused)] const BINARY_INFO_ID_RP_PROGRAM_VERSION_STRING: u32 = 0x11a9bc3a;
+    #[allow(unused)] const BINARY_INFO_ID_RP_PROGRAM_BUILD_DATE_STRING: u32 = 0x9da22254;
+    #[allow(unused)] const BINARY_INFO_ID_RP_BINARY_END: u32 = 0x68f465de;
+    #[allow(unused)] const BINARY_INFO_ID_RP_PROGRAM_URL: u32 = 0x1856239a;
+    #[allow(unused)] const BINARY_INFO_ID_RP_PROGRAM_DESCRIPTION: u32 = 0xb6a07c19;
+    #[allow(unused)] const BINARY_INFO_ID_RP_PROGRAM_FEATURE: u32 = 0xa1f4b453;
+    #[allow(unused)] const BINARY_INFO_ID_RP_PROGRAM_BUILD_ATTRIBUTE: u32 = 0x4275f0d3;
+    #[allow(unused)] const BINARY_INFO_ID_RP_SDK_VERSION: u32 = 0x5360b3ab;
+    #[allow(unused)] const BINARY_INFO_ID_RP_PICO_BOARD: u32 = 0xb63cffbb;
+    #[allow(unused)] const BINARY_INFO_ID_RP_BOOT2_NAME: u32 = 0x7f8882e1;
+
+    pub const fn binary_end_address(self: Self, value: u32) -> Self {
+        self.bi_int(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_BINARY_END, value)
+    }
+
+    pub const fn program_name(self: Self, name: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PROGRAM_NAME, name)
+    }
+    pub const fn program_description(self: Self, description: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PROGRAM_DESCRIPTION, description)
+    }
+    pub const fn program_version_string(self: Self, version_string: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PROGRAM_VERSION_STRING, version_string)
+    }
+    pub const fn program_build_date_string(self: Self, date_string: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PROGRAM_BUILD_DATE_STRING, date_string)
+    }
+    pub const fn program_url(self: Self, url: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PROGRAM_URL, url)
+    }
+    // multiple of these may be added
+    pub const fn program_feature(self: Self, feature: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PROGRAM_FEATURE, feature)
+    }
+    pub const fn program_build_attribute(self: Self, attr: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PROGRAM_BUILD_ATTRIBUTE, attr)
+    }
+    pub const fn sdk_version(self: Self, attr: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_SDK_VERSION, attr)
+    }
+    pub const fn board(self: Self, attr: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PICO_BOARD, attr)
+    }
+    pub const fn boot2_name(self: Self, attr: &str) -> Self {
+        self.bi_string(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_BOOT2_NAME, attr)
+    }
+    pub const fn program_feature_group(self: Self, tag: u16, id: u32, label: &str) -> Self {
+        self.program_feature_group_with_flags(tag, id, label, GroupFlags::NONE)
+    }
+    pub const fn program_feature_group_with_flags(self: Self, tag: u16, id: u32, label: &str, flags: GroupFlags) -> Self {
+        self.bi_named_group(Self::BINARY_INFO_TAG_RASPBERRY_PI, Self::BINARY_INFO_ID_RP_PROGRAM_FEATURE,
+            flags as u16, tag, id, label)
+    }
+
+    pub const fn pins_with_func(self: Self, func: u8, pins: &[u8]) -> Self {
+        assert!(func < 16);
+        assert!(pins.len() > 0 && pins.len() < 6);
+
+        const BI_PINS_ENCODING_MULTI: u32 = 2;
+        let encoded = if pins.len() == 1 {
+            BI_PINS_ENCODING_MULTI | ((func as u32) << 3) | ((pins[0] as u32) << 7) | ((pins[0] as u32) << 12)
+        } else if pins.len() == 2 {
+            BI_PINS_ENCODING_MULTI | ((func as u32) << 3) | ((pins[0] as u32) << 7) | ((pins[1] as u32) << 12) | ((pins[1] as u32) << 17)
+        } else if pins.len() == 3 {
+            BI_PINS_ENCODING_MULTI | ((func as u32) << 3) | ((pins[0] as u32) << 7) | ((pins[1] as u32) << 12) | ((pins[2] as u32) << 17) | ((pins[2] as u32) << 22)
+        } else if pins.len() == 4 {
+            BI_PINS_ENCODING_MULTI | ((func as u32) << 3) | ((pins[0] as u32) << 7) | ((pins[1] as u32) << 12) | ((pins[2] as u32) << 17) | ((pins[3] as u32) << 22) | ((pins[3] as u32) << 27)
+        } else if pins.len() == 5 {
+            BI_PINS_ENCODING_MULTI | ((func as u32) << 3) | ((pins[0] as u32) << 7) | ((pins[1] as u32) << 12) | ((pins[2] as u32) << 17) | ((pins[3] as u32) << 22) | ((pins[4] as u32) << 27)
+        } else {
+            assert!(false);
+            0
+        };
+
+        self.bi_encoded_pins_with_func(encoded)
+    }
+
+    pub const fn pin_range_with_func(self: Self, func: u8, pin_range: (u8, u8)) -> Self {
+        assert!(func < 16);
+        let (plo, phi) = pin_range;
+
+        const BI_PINS_ENCODING_RANGE: u32 = 1;
+        let encoded = BI_PINS_ENCODING_RANGE | ((func as u32) << 3) | ((plo as u32) << 7) | ((phi as u32) << 12);
+
+        self.bi_encoded_pins_with_func(encoded)
+    }
+
+    pub const fn pins_with_names(mut self: Self, pins: &[(u8, &str)]) -> Self {
+        const fn collect_mask(mask: u32, pins: &[(u8, &str)], offset: usize) -> u32 {
+            if offset >= pins.len() {
+                mask
+            } else {
+                collect_mask(mask | (1 << pins[offset].0), pins, offset+1)
+            }
+        }
+        self = self.bi_pins_with_names(collect_mask(0, pins, 0), pins[0].1);
+
+        const fn append_names(acc: ProgramInfoBuilder, pins: &[(u8, &str)], offset: usize) -> ProgramInfoBuilder {
+            if offset >= pins.len() {
+                acc
+            } else {
+                append_names(acc.append_to_last_string("|").append_to_last_string(pins[offset].1), pins, offset+1)
+            }
+        }
+        append_names(self, pins, 1)
+    }
 }