prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use super::identity::BleAddress;

pub const MAX_ADVERTISEMENT_LEN: usize = 31;

const fn ble_reticulum_uuid(last: u8) -> [u8; 16] {
    [
        0x37, 0x14, 0x5b, 0x00, 0x44, 0x2d, 0x4a, 0x94, 0x91, 0x7f, 0x8f, 0x42, 0xc5, 0xda, 0x28,
        last,
    ]
}

pub const BLE_SERVICE_UUID_BYTES: [u8; 16] = ble_reticulum_uuid(0xe3);
pub const BLE_SERVICE_UUID: BleUuid = BleUuid::Bit128(BLE_SERVICE_UUID_BYTES);
pub const COLUMBA_RX_UUID: BleUuid = BleUuid::Bit128(ble_reticulum_uuid(0xe5));
pub const COLUMBA_TX_UUID: BleUuid = BleUuid::Bit128(ble_reticulum_uuid(0xe4));
pub const COLUMBA_IDENTITY_UUID: BleUuid = BleUuid::Bit128(ble_reticulum_uuid(0xe6));
pub const NATIVE_CONTROL_UUID: BleUuid = BleUuid::Bit128(ble_reticulum_uuid(0xe7));
pub const NATIVE_DATA_UUID: BleUuid = BleUuid::Bit128(ble_reticulum_uuid(0xe8));

const AD_FLAGS: u8 = 0x01;
const AD_INCOMPLETE_SERVICE_UUID128: u8 = 0x06;
const AD_SERVICE_UUID128: u8 = 0x07;
pub(super) const AD_MANUFACTURER_SPECIFIC: u8 = 0xff;
const FLAGS_LE_GENERAL_DISCOVERABLE: u8 = 0x06;
const EXPERIMENTAL_ROLE_COMPANY_ID: [u8; 2] = [0xff, 0xff];
pub(super) const EXPERIMENTAL_ROLE_VERSION: u8 = 0x03;
pub(super) const EXPERIMENTAL_ROLE_PERIPHERAL_ONLY: u8 = 0x01;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BleUuid {
    Bit16(u16),
    Bit128([u8; 16]),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BleRoleCapabilities {
    DualRole,
    PeripheralOnly,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumbaConnectionRole {
    Dial,
    Accept,
    Unavailable,
}

pub fn encode_advertisement(
    out: &mut [u8],
    role_capabilities: BleRoleCapabilities,
) -> Option<usize> {
    let mut writer = AdWriter::new(out);
    writer.put(AD_FLAGS, &[FLAGS_LE_GENERAL_DISCOVERABLE])?;
    let mut little_endian = BLE_SERVICE_UUID_BYTES;
    little_endian.reverse();
    writer.put(AD_SERVICE_UUID128, &little_endian)?;
    let flags = match role_capabilities {
        BleRoleCapabilities::DualRole => 0,
        BleRoleCapabilities::PeripheralOnly => EXPERIMENTAL_ROLE_PERIPHERAL_ONLY,
    };
    writer.put(
        AD_MANUFACTURER_SPECIFIC,
        &[
            EXPERIMENTAL_ROLE_COMPANY_ID[0],
            EXPERIMENTAL_ROLE_COMPANY_ID[1],
            EXPERIMENTAL_ROLE_VERSION,
            flags,
        ],
    )?;
    Some(writer.len())
}

pub fn contains_service(adv: &[u8]) -> bool {
    let mut little_endian = BLE_SERVICE_UUID_BYTES;
    little_endian.reverse();
    AdReader::new(adv).any(|(ad_type, body)| {
        (ad_type == AD_SERVICE_UUID128 || ad_type == AD_INCOMPLETE_SERVICE_UUID128)
            && body == little_endian
    })
}

pub fn columba_role_capabilities(adv: &[u8]) -> Option<BleRoleCapabilities> {
    AdReader::new(adv).find_map(|(ad_type, body)| {
        if ad_type != AD_MANUFACTURER_SPECIFIC {
            return None;
        }
        let company_id: [u8; 2] = body.get(..2)?.try_into().ok()?;
        columba_role_capabilities_from_manufacturer(u16::from_le_bytes(company_id), body.get(2..)?)
    })
}

pub fn columba_role_capabilities_from_manufacturer(
    company_id: u16,
    data: &[u8],
) -> Option<BleRoleCapabilities> {
    if company_id != u16::from_le_bytes(EXPERIMENTAL_ROLE_COMPANY_ID)
        || *data.first()? < EXPERIMENTAL_ROLE_VERSION
    {
        return None;
    }
    if data.get(1)? & EXPERIMENTAL_ROLE_PERIPHERAL_ONLY == 0 {
        Some(BleRoleCapabilities::DualRole)
    } else {
        Some(BleRoleCapabilities::PeripheralOnly)
    }
}

pub fn columba_connection_role(
    local_address: BleAddress,
    local_capabilities: BleRoleCapabilities,
    peer_address: BleAddress,
    peer_capabilities: BleRoleCapabilities,
) -> ColumbaConnectionRole {
    match (local_capabilities, peer_capabilities) {
        (BleRoleCapabilities::DualRole, BleRoleCapabilities::PeripheralOnly) => {
            ColumbaConnectionRole::Dial
        }
        (BleRoleCapabilities::PeripheralOnly, BleRoleCapabilities::DualRole) => {
            ColumbaConnectionRole::Accept
        }
        (BleRoleCapabilities::PeripheralOnly, BleRoleCapabilities::PeripheralOnly) => {
            ColumbaConnectionRole::Unavailable
        }
        (BleRoleCapabilities::DualRole, BleRoleCapabilities::DualRole) => {
            match local_address.cmp(&peer_address) {
                core::cmp::Ordering::Less => ColumbaConnectionRole::Dial,
                core::cmp::Ordering::Greater => ColumbaConnectionRole::Accept,
                core::cmp::Ordering::Equal => ColumbaConnectionRole::Unavailable,
            }
        }
    }
}

struct AdWriter<'a> {
    buf: &'a mut [u8],
    pos: usize,
}

impl<'a> AdWriter<'a> {
    fn new(buf: &'a mut [u8]) -> Self {
        Self { buf, pos: 0 }
    }

    fn put(&mut self, ad_type: u8, body: &[u8]) -> Option<()> {
        let field_len = 1 + body.len();
        let end = self.pos + 1 + field_len;
        let slot = self.buf.get_mut(self.pos..end)?;
        slot[0] = u8::try_from(field_len).ok()?;
        slot[1] = ad_type;
        slot[2..].copy_from_slice(body);
        self.pos = end;
        Some(())
    }

    fn len(&self) -> usize {
        self.pos
    }
}

struct AdReader<'a> {
    buf: &'a [u8],
    pos: usize,
}

impl<'a> AdReader<'a> {
    fn new(buf: &'a [u8]) -> Self {
        Self { buf, pos: 0 }
    }
}

impl<'a> Iterator for AdReader<'a> {
    type Item = (u8, &'a [u8]);

    fn next(&mut self) -> Option<Self::Item> {
        let field_len = *self.buf.get(self.pos)? as usize;
        if field_len == 0 {
            return None;
        }
        let ad_type = *self.buf.get(self.pos + 1)?;
        let body = self.buf.get(self.pos + 2..self.pos + 1 + field_len)?;
        self.pos += 1 + field_len;
        Some((ad_type, body))
    }
}