#include "ScanI2CTwoWire.h"
#include "configuration.h"
#include "detect/ScanI2C.h"

#if !MESHTASTIC_EXCLUDE_I2C

#include "concurrency/LockGuard.h"
#if defined(ARCH_PORTDUINO)
#include "linux/LinuxHardwareI2C.h"
#endif
#if defined(SENSECAP_INDICATOR)
#include "mesh/comms/I2CProxy.h"
#endif
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32)
#include "meshUtils.h" // vformat

#endif
#if defined(HAS_QMA6100P) && (defined(ARCH_NRF52) || defined(NRF52_SERIES) || defined(NRF52))
#include "platform/nrf52/Nrf52Twim.h"
#include <QMA6100P.h>

namespace
{
bool probeQMA6100P(uint8_t address)
{
    uint8_t chipID = 0;

    Nrf52Twim::restoreBus();
    const bool readOk = Nrf52Twim::readRegister(address, SFE_QMA6100P_CHIP_ID, chipID);
    const bool found = readOk && chipID == QMA6100P_CHIP_ID;
    Nrf52Twim::restoreBus();

    return found;
}
} // namespace
#endif

bool in_array(uint8_t *array, int size, uint8_t lookfor)
{
    int i;
    for (i = 0; i < size; i++)
        if (lookfor == array[i])
            return true;
    return false;
}

ScanI2C::FoundDevice ScanI2CTwoWire::find(ScanI2C::DeviceType type) const
{
    concurrency::LockGuard guard((concurrency::Lock *)&lock);

    return exists(type) ? ScanI2C::FoundDevice(type, deviceAddresses.at(type)) : DEVICE_NONE;
}

bool ScanI2CTwoWire::exists(ScanI2C::DeviceType type) const
{
    return deviceAddresses.find(type) != deviceAddresses.end();
}

ScanI2C::FoundDevice ScanI2CTwoWire::firstOfOrNONE(size_t count, DeviceType types[]) const
{
    concurrency::LockGuard guard((concurrency::Lock *)&lock);

    for (size_t k = 0; k < count; k++) {
        ScanI2C::DeviceType current = types[k];

        if (exists(current)) {
            return ScanI2C::FoundDevice(current, deviceAddresses.at(current));
        }
    }

    return DEVICE_NONE;
}

ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const
{
    TwoWire *i2cBus = fetchI2CBus(addr);

    uint8_t r = 0;
    uint8_t r_prev = 0;
    uint8_t c = 0;
    ScanI2C::DeviceType o_probe = ScanI2C::DeviceType::SCREEN_UNKNOWN;
    do {
        r_prev = r;
        i2cBus->beginTransmission(addr.address);
        i2cBus->write((uint8_t)0x00);
        i2cBus->endTransmission();
        i2cBus->requestFrom((int)addr.address, 1);
        if (i2cBus->available()) {
            r = i2cBus->read();
        }
        if (r == 0x80) {
            LOG_INFO("QMC6310N found at address 0x%02X", addr.address);
            return ScanI2C::DeviceType::QMC6310N;
        }
        r &= 0x0f;

        if (r == 0x08 || r == 0x00) {
            logFoundDevice("SH1106", (uint8_t)addr.address);
            o_probe = SCREEN_SH1106; // SH1106
        } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07 || r == 0x05) {
            logFoundDevice("SSD1306", (uint8_t)addr.address);
            o_probe = SCREEN_SSD1306; // SSD1306
        }
        c++;
    } while ((r != r_prev) && (c < 4));
    LOG_DEBUG("0x%x subtype probed in %i tries ", r, c);

    return o_probe;
}
uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation &registerLocation,
                                          ScanI2CTwoWire::ResponseWidth responseWidth, bool zeropad = false) const
{
    uint16_t value = 0x00;
    TwoWire *i2cBus = fetchI2CBus(registerLocation.i2cAddress);

    i2cBus->beginTransmission(registerLocation.i2cAddress.address);
    i2cBus->write(registerLocation.registerAddress);
    if (zeropad) {
        // Lark Commands need the argument list length in 2 bytes.
        i2cBus->write((int)0);
        i2cBus->write((int)0);
    }
    i2cBus->endTransmission();
    delay(20);
    i2cBus->requestFrom(registerLocation.i2cAddress.address, responseWidth);
    if (i2cBus->available() > 1) {
        // Read MSB, then LSB
        value = (uint16_t)i2cBus->read() << 8;
        value |= i2cBus->read();
    } else if (i2cBus->available()) {
        value = i2cBus->read();
    }
    // Drain excess bytes
    for (uint8_t i = 0; i < responseWidth - 1; i++) {
        if (i2cBus->available())
            i2cBus->read();
    }
    LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value);
    return value;
}

bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const
{
    TwoWire *i2cBus = fetchI2CBus(addr);
    i2cBus->beginTransmission(addr.address);
    if (command > 0xFF) {
        i2cBus->write((uint8_t)(command >> 8));
    }
    i2cBus->write((uint8_t)(command & 0xFF));
    if (i2cBus->endTransmission() != 0) {
        return false;
    }
    delay(20);
    uint8_t received = i2cBus->requestFrom(addr.address, expectedLength);
    bool match = (received == expectedLength);
    while (i2cBus->available())
        i2cBus->read();
    return match;
}

#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
#include "../modules/Telemetry/Sensor/SEN5XSensor.h"
#include "../modules/Telemetry/Sensor/SEN6XSensor.h"
bool probeSEN5X(TwoWire *i2cBus, uint8_t address, ScanI2C::I2CPort port)
{
    SEN5XSensor sen5xsensor;
    return sen5xsensor.probe(i2cBus, address, port);
}

bool probeSEN6X(TwoWire *i2cBus, uint8_t address, ScanI2C::I2CPort port)
{
    SEN6XSensor sen6xsensor;
    return sen6xsensor.probe(i2cBus, address, port);
}

bool probeHM330x(TwoWire *i2cBus, uint8_t address)
{

    i2cBus->beginTransmission(address);
    i2cBus->write(0X88);
    byte ret = i2cBus->endTransmission();

    if (ret == 0) {
        return true;
    }

    return false;
}
#endif

#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
static uint8_t crcSHT2X(const uint8_t *data, uint8_t len)
{
    uint8_t crc = 0;
    for (uint8_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t bit = 0; bit < 8; bit++) {
            crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : crc << 1;
        }
    }
    return crc;
}

bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
{
    uint8_t serialA[8] = {0};
    uint8_t serialB[6] = {0};

    i2cBus->beginTransmission(address);
    i2cBus->write(0xFA);
    i2cBus->write(0x0F);

    if (i2cBus->endTransmission() != 0)
        return false;

    if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialA)) != sizeof(serialA))
        return false;

    for (uint8_t i = 0; i < sizeof(serialA); i++) {
        if (!i2cBus->available())
            return false;
        serialA[i] = i2cBus->read();
    }

    i2cBus->beginTransmission(address);
    i2cBus->write(0xFC);
    i2cBus->write(0xC9);

    if (i2cBus->endTransmission() != 0)
        return false;

    if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialB)) != sizeof(serialB))
        return false;

    for (uint8_t i = 0; i < sizeof(serialB); i++) {
        if (!i2cBus->available())
            return false;
        serialB[i] = i2cBus->read();
    }

    return crcSHT2X(&serialA[0], 1) == serialA[1] && crcSHT2X(&serialA[2], 1) == serialA[3] &&
           crcSHT2X(&serialA[4], 1) == serialA[5] && crcSHT2X(&serialA[6], 1) == serialA[7] &&
           crcSHT2X(&serialB[0], 2) == serialB[2] && crcSHT2X(&serialB[3], 2) == serialB[5];
}
#endif

// Everest Semiconductor ES7210 4-channel audio ADC, as found on the T-Deck. Its AD1/AD0 straps
// select 0x40..0x43, so it lands squarely on the INA/SHT2X addresses. Chip ID registers and their
// reset values, per the ES7210 datasheet rev 2.0.
static constexpr uint8_t ES7210_CHIP_ID1_REG = 0x3D;
static constexpr uint8_t ES7210_CHIP_ID1 = 0x72;
static constexpr uint8_t ES7210_CHIP_ID0_REG = 0x3E;
static constexpr uint8_t ES7210_CHIP_ID0 = 0x10;

static bool readByteRegister(TwoWire *i2cBus, uint8_t address, uint8_t reg, uint8_t &value)
{
    i2cBus->beginTransmission(address);
    i2cBus->write(reg);

    if (i2cBus->endTransmission() != 0)
        return false;

    if (i2cBus->requestFrom(address, (uint8_t)1) != 1 || !i2cBus->available())
        return false;

    value = i2cBus->read();
    return true;
}

// The INA219 has no manufacturer or die ID register to check, so it is inferred from "something
// answered here and it wasn't anything else we know". That makes positively identifying the other
// occupants of this address the only way to keep them out of the fallback.
static bool detectES7210(TwoWire *i2cBus, uint8_t address)
{
    uint8_t id = 0;

    // Read the high ID byte first so anything that clearly isn't an ES7210 costs a single
    // transaction, then confirm with the low byte.
    if (!readByteRegister(i2cBus, address, ES7210_CHIP_ID1_REG, id) || id != ES7210_CHIP_ID1)
        return false;

    return readByteRegister(i2cBus, address, ES7210_CHIP_ID0_REG, id) && id == ES7210_CHIP_ID0;
}

#define SCAN_SIMPLE_CASE(ADDR, T, ...)                                                                                           \
    case ADDR:                                                                                                                   \
        logFoundDevice(__VA_ARGS__);                                                                                             \
        type = T;                                                                                                                \
        break;

void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
{
    concurrency::LockGuard guard((concurrency::Lock *)&lock);

    LOG_DEBUG("Scan for I2C devices on port %d", port);

    uint8_t err;

    DeviceAddress addr(port, 0x00);

    uint16_t registerValue = 0x00;
    ScanI2C::DeviceType type;
    TwoWire *i2cBus;
#ifdef RV3028_RTC
    Melopero_RV3028 rtc;
#endif

#if WIRE_INTERFACES_COUNT == 2
    if (port == I2CPort::WIRE1) {
#if defined(SENSECAP_INDICATOR)
        i2cBus = i2cProxy; // WIRE1 is bridged to the RP2040
#else
        i2cBus = &Wire1;
#endif
    } else {
#endif
        i2cBus = &Wire;
#if WIRE_INTERFACES_COUNT == 2
    }
#endif

    // We only need to scan 112 addresses, the rest is reserved for special purposes
    // 0x00 General Call
    // 0x01 CBUS addresses
    // 0x02 Reserved for different bus formats
    // 0x03 Reserved for future purposes
    // 0x04-0x07 High Speed Master Code
    // 0x78-0x7B 10-bit slave addressing
    // 0x7C-0x7F Reserved for future purposes

    for (addr.address = 8; addr.address < 120; addr.address++) {
#if defined(HAS_QMA6100P) && (defined(ARCH_NRF52) || defined(NRF52_SERIES) || defined(NRF52))
        bool nrf52QmaFound = false;
#endif
        if (asize != 0) {
            if (!in_array(address, asize, (uint8_t)addr.address))
                continue;
            LOG_DEBUG("Scan address 0x%x", (uint8_t)addr.address);
        }
        // For QMA6100P candidates on nRF52, use bounded I2C probing; otherwise use normal Wire
#if defined(HAS_QMA6100P) && (defined(ARCH_NRF52) || defined(NRF52_SERIES) || defined(NRF52))
        if (addr.address == QMA6100P_ADDRESS_LOW || addr.address == QMA6100P_ADDRESS_HIGH) {
            nrf52QmaFound = probeQMA6100P(addr.address);
            err = nrf52QmaFound ? 0 : 2;
        } else {
            i2cBus->beginTransmission(addr.address);
#ifdef ARCH_PORTDUINO
            err = 2;
            if ((addr.address >= 0x30 && addr.address <= 0x37) || (addr.address >= 0x50 && addr.address <= 0x5F)) {
                if (i2cBus->read() != -1)
                    err = 0;
            } else {
                err = i2cBus->writeQuick((uint8_t)0);
            }
            if (err != 0)
                err = 2;
#else
            err = i2cBus->endTransmission();
#endif
        }
#else
        i2cBus->beginTransmission(addr.address);
#ifdef ARCH_PORTDUINO
        err = 2;
        if ((addr.address >= 0x30 && addr.address <= 0x37) || (addr.address >= 0x50 && addr.address <= 0x5F)) {
            if (i2cBus->read() != -1)
                err = 0;
        } else {
            err = i2cBus->writeQuick((uint8_t)0);
        }
        if (err != 0)
            err = 2;
#else
        err = i2cBus->endTransmission();
#endif
#endif
        type = NONE;
        if (err == 0) {
            switch (addr.address) {
            case SSD1306_ADDRESS_H:
            case SSD1306_ADDRESS_L:
                type = probeOLED(addr);
                break;

#ifdef RV3028_RTC
            case RV3028_RTC:
                // foundDevices[addr] = RTC_RV3028;
                type = RTC_RV3028;
                logFoundDevice("RV3028", (uint8_t)addr.address);
                rtc.initI2C(*i2cBus);
                // Update RTC EEPROM settings, if necessary
                if (rtc.readEEPROMRegister(0x35) != 0x07) {
                    rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout
                }
                if (rtc.readEEPROMRegister(0x37) != 0xB4) {
                    rtc.writeEEPROMRegister(0x37, 0xB4);
                }
                break;
#endif

#ifdef PCF8563_RTC
                SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address)
#endif
#ifdef RX8130CE_RTC
                SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
#endif

#ifdef PCF85063_RTC
                SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address)
#endif

            case CARDKB_ADDR:
                // Do we have the RAK14006 instead?
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);
                if (registerValue == 0x02) {
                    // KEYPAD_VERSION
                    logFoundDevice("RAK14004", (uint8_t)addr.address);
                    type = RAK14004;
                } else {
                    logFoundDevice("M5 cardKB", (uint8_t)addr.address);
                    type = CARDKB;
                }
                break;

            case TDECK_KB_ADDR:
                // Do we have the T-Deck keyboard or the T-Deck Pro battery sensor?
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);
                if (registerValue != 0) {
                    logFoundDevice("BQ27220", (uint8_t)addr.address);
                    type = BQ27220;
                } else {
                    logFoundDevice("TDECKKB", (uint8_t)addr.address);
                    type = TDECKKB;
                }
                break;
            case BBQ10_KB_ADDR:
                // Check status register (0xF0) for DS284X status and one-wire reset
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xF0), 1);
                if (registerValue & 0x16) { // One-wire reset after power-on
                    type = DS248X;
                    logFoundDevice("DS2482-800", (uint8_t)addr.address);
                    break;
                }
                type = BBQ10KB;
                logFoundDevice("BB Q10", (uint8_t)addr.address);
                break;
                SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
#ifdef HAS_NCP5623
                SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address);
#endif
            case XPOWERS_AXP192_AXP2101_ADDRESS:
                // Do we have the axp2101/192 or the TCA8418
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x90), 1);
                if (registerValue == 0x0) {
                    logFoundDevice("TCA8418", (uint8_t)addr.address);
                    type = TCA8418KB;
                } else {
                    logFoundDevice("AXP192/AXP2101", (uint8_t)addr.address);
                    type = PMU_AXP192_AXP2101;
                }
                break;
            case BME_ADDR:
            case BME_ADDR_ALTERNATE:
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xD0), 1); // GET_ID
                switch (registerValue) {
                case 0x61:
                    logFoundDevice("BME680", (uint8_t)addr.address);
                    type = BME_680;
                    break;
                case 0x60:
                    logFoundDevice("BME280", (uint8_t)addr.address);
                    type = BME_280;
                    break;
                case 0x55:
                    logFoundDevice("BMP085/BMP180", (uint8_t)addr.address);
                    type = BMP_085;
                    break;
                case 0x00:
                    // do we have a DPS310 instead?
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0D), 1);
                    switch (registerValue) {
                    case 0x10:
                        logFoundDevice("DPS310", (uint8_t)addr.address);
                        type = DPS310;
                        break;
                    case 0x11:
                        logFoundDevice("SPA06-003", (uint8_t)addr.address);
                        type = SPA06;
                        break;
                    }
                    if (type == DPS310 || type == SPA06) {
                        break;
                    }
                default:
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID
                    switch (registerValue) {
                    case 0x50: // BMP-388 should be 0x50
                        logFoundDevice("BMP-388", (uint8_t)addr.address);
                        type = BMP_3XX;
                        break;
                    case 0x60: // BMP-390 should be 0x60
                        logFoundDevice("BMP-390", (uint8_t)addr.address);
                        type = BMP_3XX;
                        break;
                    case 0x58: // BMP-280 should be 0x58
                    default:
                        logFoundDevice("BMP-280", (uint8_t)addr.address);
                        type = BMP_280;
                        break;
                    }
                    break;
                }
                break;
#ifndef HAS_NCP5623
            case AHT10_ADDR:
                logFoundDevice("AHT10", (uint8_t)addr.address);
                type = AHT10;
                break;
#endif
#if !defined(M5STACK_UNITC6L)
            case INA_ADDR: // same as HM330X, ES7210 and SHT2X
            case INA_ADDR_ALTERNATE:
            case INA_ADDR_WAVESHARE_UPS: {
                uint16_t mfg = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);

                LOG_DEBUG("Register MFG_UID: 0x%x", mfg);

                // Only read DIE_UID for vendors we recognize as INA-compatible to avoid
                // an extra I2C transaction + delay on other devices sharing this address.
                if (mfg == 0x5449 || mfg == 0x190F) {
                    uint16_t die = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
                    LOG_DEBUG("Register DIE_UID: 0x%x", die);

                    // TI INA226 or fully compatible clones (e.g. TPA626)
                    if (mfg == 0x5449 && die == 0x2260) {
                        logFoundDevice("INA226", (uint8_t)addr.address);
                        type = INA226;
                        break;
                    }
                    // Silergy SQ52201 (INA226-compatible with different IDs)
                    else if (mfg == 0x190F && die == 0x0000) {
                        logFoundDevice("INA226 (SQ52201)", (uint8_t)addr.address);
                        type = INA226;
                        break;
                    }
                    // TI INA260
                    else if (mfg == 0x5449) {
                        logFoundDevice("INA260", (uint8_t)addr.address);
                        type = INA260;
                        break;
                    }
                }

                // The ES7210 audio ADC shares this address on some boards (T-Deck). It answers
                // none of the checks above, so identify it here and leave the type unset - driving
                // an audio codec as if it were a power monitor crashes the device. See #11115.
                if (detectES7210(i2cBus, (uint8_t)addr.address)) {
                    LOG_INFO("ES7210 audio codec at 0x%x, not a power sensor", (uint8_t)addr.address);
                    break;
                }
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_TELEMETRY
                if (detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) {
                    logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address);
                    type = SHTXX;
                    break;
                }
#endif
                // Get INA219 configuration register 0x00 expecting 0x399F after power-on reset
                uint16_t registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2);
                if (registerValue == 0x399F) {
                    logFoundDevice("INA219", (uint8_t)addr.address);
                    type = INA219;
                    break;
                }
#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && HAS_TELEMETRY
                // Assume HM330x as the weakest detection method if none of the above ones are found
                if (probeHM330x(i2cBus, addr.address)) {
                    logFoundDevice("HM330X", (uint8_t)addr.address);
                    type = HM330X;
                    break;
                }
#endif
            }
            case INA3221_ADDR:
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
                LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue);
                if (registerValue == 0x5449) {
                    logFoundDevice("INA3221", (uint8_t)addr.address);
                    type = INA3221;
                } else {
                    /* check the first 2 bytes of the 6 byte response register
                    LARK FW 1.0 should return:
                    RESPONSE_STATUS STATUS_SUCCESS (0x53)
                    RESPONSE_CMD CMD_GET_VERSION (0x05)
                    RESPONSE_LEN_L 0x02
                    RESPONSE_LEN_H 0x00
                    RESPONSE_PAYLOAD 0x01
                    RESPONSE_PAYLOAD+1 0x00
                    */
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x05), 6, true);
                    LOG_DEBUG("Register MFG_UID 05: 0x%x", registerValue);
                    if (registerValue == 0x5305) {
                        logFoundDevice("DFRobot Lark", (uint8_t)addr.address);
                        type = DFROBOT_LARK;
                    }
                    // else: probably a RAK12500/UBLOX GPS on I2C
                }
                break;
#endif
            case MCP9808_ADDR:
                // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some
                // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips.
                {
#ifdef HAS_STK8XXX
                    // Check register 0x00 for 0x8700 response to ID STK8BA53 chip.
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 2);
                    if (registerValue == 0x8700) {
                        type = STK8BAXX;
                        logFoundDevice("STK8BAXX", (uint8_t)addr.address);
                        break;
                    }
#endif
                    // Check register 0x07 for 0x0400 response to ID MCP9808 chip.
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x07), 2);
                    if (registerValue == 0x0400) {
                        type = MCP9808;
                        logFoundDevice("MCP9808", (uint8_t)addr.address);
                        break;
                    }

                    // Check register 0x0F for 0x3300 response to ID LIS3DH chip.
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
                    if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
                        type = LIS3DH;
                        logFoundDevice("LIS3DH", (uint8_t)addr.address);
                        break;
                    }

                    // Check status register (0xF0) for DS284X status and one-wire reset
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xF0), 1);
                    if (registerValue & 0x16) { // One-wire reset after power-on
                        type = DS248X;
                        logFoundDevice("DS248X", (uint8_t)addr.address);
                    }
                    break;
                }
            case SHTXX_ADDR:     // same as OPT3001_ADDR_ALT
            case SHTXX_ADDR_ALT: // same as OPT3001_ADDR
                if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
                    type = OPT3001;
                    logFoundDevice("OPT3001", (uint8_t)addr.address);
                } else { // SHTXX
                    type = SHTXX;
                    logFoundDevice("SHTXX", (uint8_t)addr.address);
                }

                break;

                SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTXX, "SHTXX", (uint8_t)addr.address)
            case RCWL9620_ADDR:
                // get MAX30102 PARTID
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1);
                if (registerValue == 0x15) {
                    type = MAX30102;
                    logFoundDevice("MAX30102", (uint8_t)addr.address);
                    break;
                } else {
                    type = RCWL9620;
                    logFoundDevice("RCWL9620", (uint8_t)addr.address);
                }
                break;

            case LPS22HB_ADDR_ALT:
                // SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response
                if (i2cCommandResponseLength(addr, 0xD060, 48)) {
                    type = SFA30;
                    logFoundDevice("SFA30", (uint8_t)addr.address);
                    break;
                }
                // Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1)
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1);
                if (registerValue == 0xB1) {
                    type = LPS22HB;
                    logFoundDevice("LPS22HB", (uint8_t)addr.address);
                }
                break;
                SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address)
            case DS248X_ADDR_ALT3:
                // Check status register (0xF0) for DS284X status and one-wire reset
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xF0), 1);
                if (registerValue & 0x16) { // One-wire reset after power-on
                    type = DS248X;
                    logFoundDevice("DS2482-800", (uint8_t)addr.address);
                    break;
                }
                break;

            case QMC6310U_ADDR:
                // Check status register (0xF0) for DS284X status and one-wire reset
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xF0), 1);
                if (registerValue & 0x16) { // One-wire reset after power-on
                    type = DS248X;
                    logFoundDevice("DS2482-800", (uint8_t)addr.address);
                    break;
                }
                type = QMC6310U;
                logFoundDevice("QMC6310U", (uint8_t)addr.address);
                break;

            case QMI8658_ADDR: // same as BQ25896_ADDR and SEN6X_ADDR
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID
                if (registerValue == 0xC0) {
                    type = BQ24295;
                    logFoundDevice("BQ24295", (uint8_t)addr.address);
                    break;
                }
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID
                if ((registerValue & 0b00000011) == 0b00000010) {
                    type = BQ25896;
                    logFoundDevice("BQ25896", (uint8_t)addr.address);
                    break;
                }
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID
                if (registerValue == 0x6A) {
                    type = LSM6DS3;
                    logFoundDevice("LSM6DS3", (uint8_t)addr.address);
                } else if (registerValue == 0x6B) {
                    type = ISM330DHCX;
                    logFoundDevice("ISM330DHCX", (uint8_t)addr.address);
                } else {
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
                    if (probeSEN6X(i2cBus, addr.address, port)) {
                        type = SEN6X;
                        logFoundDevice("SEN6X", addr.address);
                        break;
                    }
#endif
                    type = QMI8658;
                    logFoundDevice("QMI8658", (uint8_t)addr.address);
                }
                break;

                SCAN_SIMPLE_CASE(QMC5883L_ADDR, QMC5883L, "QMC5883L", (uint8_t)addr.address)
            case HMC5883L_ADDR:
                // WHO_AM_I first: the DS248X probe below reads a reserved address on the IIS2MDCTR
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x4FU), 1); // get ID
                if (registerValue == 0x40) {
                    type = IIS2MDCTR;
                    logFoundDevice("IIS2MDCTR", (uint8_t)addr.address);
                    break;
                }

                // Check status register (0xF0) for DS284X status and one-wire reset
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xF0), 1);
                if (registerValue & 0x16) { // One-wire reset after power-on
                    type = DS248X;
                    logFoundDevice("DS2482-800", (uint8_t)addr.address);
                    break;
                }
                type = HMC5883L;
                logFoundDevice("HMC5883L", (uint8_t)addr.address);
                break;
#ifdef HAS_QMA6100P
                SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address)
#else
                SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
#endif
            case MMC5983MA_ADDR:
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x2F), 1);
                if (registerValue == 0x30) {
                    type = MMC5983MA;
                    logFoundDevice("MMC5983MA", (uint8_t)addr.address);
#ifdef HAS_LP5562
                } else {
                    type = LP5562;
                    logFoundDevice("LP5562", (uint8_t)addr.address);
#endif
                }
                break;
            case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
                if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
                    type = LIS3DH;
                    logFoundDevice("LIS3DH", (uint8_t)addr.address);
                    break;
                } else if ((registerValue & 0xFF00) == 0x1100) {
                    // Silan SC7A20: LIS3DH register map, but answers 0x11 here.
                    type = SC7A20;
                    logFoundDevice("SC7A20", (uint8_t)addr.address);
                    break;
                }

                // Check status register (0xF0) for DS284X status and one-wire reset
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xF0), 1);
                if (registerValue & 0x16) { // One-wire reset after power-on
                    type = DS248X;
                    logFoundDevice("DS2482-800", (uint8_t)addr.address);
                    break;
                }

                type = BMA423;
                logFoundDevice("BMA423", (uint8_t)addr.address);

                break;
            case TCA9535_ADDR:
            case RAK120352_ADDR:
            case RAK120353_ADDR:
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1);
                if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20)
                    type = RAK12035;
                    logFoundDevice("RAK12035", (uint8_t)addr.address);
                } else {
                    type = TCA9535;
                    logFoundDevice("TCA9535", (uint8_t)addr.address);
                }

                break;

                SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
                SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
            case TCA9555_ADDR:
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1);
                if (registerValue == 0x13) {
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
                    if (registerValue == 0x81) {
                        type = DA217;
                        logFoundDevice("DA217", (uint8_t)addr.address);
                    } else {
                        type = TCA9555;
                        logFoundDevice("TCA9555", (uint8_t)addr.address);
                    }
                } else {
                    type = TCA9555;
                    logFoundDevice("TCA9555", (uint8_t)addr.address);
                }
                break;
            case TSL25911_ADDR:
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1);
                if (registerValue == 0x50) {
                    type = TSL2591;
                    logFoundDevice("TSL25911", (uint8_t)addr.address);
                } else {
                    type = TSL2561;
                    logFoundDevice("TSL2561", (uint8_t)addr.address);
                }
                break;

                SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address);
                SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address);
                SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address);
            case DFROBOT_RAIN_ADDR:
                // Check status register (0xF0) for DS284X status and one-wire reset
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xF0), 1);
                if (registerValue & 0x16) { // One-wire reset after power-on
                    type = DS248X;
                    logFoundDevice("DS2482-800", (uint8_t)addr.address);
                    break;
                }
                type = DFROBOT_RAIN;
                logFoundDevice("DFRobot Rain Gauge", (uint8_t)addr.address);
                break;
                SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
                SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
                SCAN_SIMPLE_CASE(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address);
            case CST328_ADDR:
                // Check status register (0xF0) for DS284X status and one-wire reset
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xF0), 1);
                if (registerValue & 0x16) { // One-wire reset after power-on
                    type = DS248X;
                    logFoundDevice("DS2482-800", (uint8_t)addr.address);
                    break;
                }
                // Do we have the CST328 or the CST226SE,CST3530
                {
                    // T-Deck pro V1.1 new touch panel use CST3530
                    int retry = 5;
                    while (retry--) {
                        uint8_t buffer[7];
                        uint8_t r_cmd[] = {0x0d0, 0x03, 0x00, 0x00};
                        i2cBus->beginTransmission(addr.address);
                        i2cBus->write(r_cmd, sizeof(r_cmd));
                        if (i2cBus->endTransmission() == 0) {
                            i2cBus->requestFrom((int)addr.address, 7);
                            i2cBus->readBytes(buffer, 7);
                            if (buffer[2] == 0xCA && buffer[3] == 0xCA) {
                                logFoundDevice("CST3530", (uint8_t)addr.address);
                                type = CST3530;
                                break;
                            }
                        }
                        uint8_t cmd1[] = {0xD0, 0x00, 0x04, 0x00};
                        i2cBus->beginTransmission(addr.address);
                        i2cBus->write(cmd1, sizeof(cmd1));
                        i2cBus->endTransmission();
                        delay(50);
                    }
                }
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
                if (registerValue == 0xA9) {
                    type = CST226SE;
                    logFoundDevice("CST226SE", (uint8_t)addr.address);
                } else {
                    type = CST328;
                    logFoundDevice("CST328", (uint8_t)addr.address);
                }
                break;

                SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address);
            case LTR553ALS_ADDR:
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register
                if (registerValue == 0x92) {                                                       // LTR553ALS Part ID
                    type = LTR553ALS;
                    logFoundDevice("LTR553ALS", (uint8_t)addr.address);
                } else {
                    // Test BH1750 - send power on command
                    i2cBus->beginTransmission(addr.address);
                    i2cBus->write(0x01); // Power On command
                    uint8_t bh1750_error = i2cBus->endTransmission();
                    if (bh1750_error == 0) {
                        type = BH1750;
                        logFoundDevice("BH1750", (uint8_t)addr.address);
                    } else {
                        LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address);
                    }
                }
                break;

                SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
            case SCD4X_ADDR: {
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1);
                if (registerValue == 0x18) {
                    logFoundDevice("CW2015", (uint8_t)addr.address);
                    type = CW2015;
                } else {
                    logFoundDevice("SCD4X", (uint8_t)addr.address);
                    type = SCD4X;
                }
                break;
            }
            case BMM150_ADDR:
#if defined(HAS_QMA6100P) && (defined(ARCH_NRF52) || defined(NRF52_SERIES) || defined(NRF52))
                if (nrf52QmaFound) {
                    logFoundDevice("QMA6100P", (uint8_t)addr.address);
                    type = QMA6100P;
                    break;
                }
#endif
                logFoundDevice("BMM150", (uint8_t)addr.address);
                type = BMM150;
                break;
#ifdef HAS_TPS65233
                SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address);
#endif

            case MLX90614_ADDR_DEF:
                // Do we have the MLX90614 or the MPR121KB or the CST226SE
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x06), 1);
                if (registerValue == 0xAB) {
                    type = CST226SE;
                    logFoundDevice("CST226SE", (uint8_t)addr.address);
                } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1) == 0x5a) {
                    type = MLX90614;
                    logFoundDevice("MLX90614", (uint8_t)addr.address);
                } else {
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS
                    if (registerValue == 0xe0) {
                        type = DRV2605;
                        logFoundDevice("DRV2605", (uint8_t)addr.address);
                    } else {
                        type = MPR121KB;
                        logFoundDevice("MPR121KB", (uint8_t)addr.address);
                    }
                }
                break;

            case ICM20948_ADDR:     // same as BMX160_ADDR, BMI270_ADDR_ALT, ICM42607P_ADDR_ALT, and SEN5X_ADDR
            case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR, and ICM42607P_ADDR
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
                type = ICM20948;
                logFoundDevice("ICM20948", (uint8_t)addr.address);
                break;
#endif
                if (registerValue == 0xEA) {
                    type = ICM20948;
                    logFoundDevice("ICM20948", (uint8_t)addr.address);
                    break;
                } else if (registerValue == 0x24) {
                    type = BMI270;
                    logFoundDevice("BMI270", (uint8_t)addr.address);
                    break;
                } else if (registerValue == 0xD8) { // BMX160 chip ID at register 0x00
                    type = BMX160;
                    logFoundDevice("BMX160", (uint8_t)addr.address);
                    break;
                } else {
                    registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x75), 1); // WHO_AM_I
                    if (registerValue == 0x60) {
                        type = ICM42607P;
                        logFoundDevice("ICM-42607-P", (uint8_t)addr.address);
                        break;
                    } else if (registerValue == 0x68) { // WHO_AM_I from datasheet
                        type = MPU6050;
                        logFoundDevice("MPU6050", (uint8_t)addr.address);
                        break;
                    }
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
                    if (probeSEN5X(i2cBus, addr.address, port)) {
                        type = SEN5X;
                        logFoundDevice("SEN5X", addr.address);
                        break;
                    }
#endif
                }
                break;

            case CGRADSENS_ADDR:
                // Register 0x00 of the RadSens sensor contains is product identifier 0x7D
                // Undocumented, but some devices return a product identifier of 0x7A
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
                if (registerValue == 0x7D || registerValue == 0x7A) {
                    type = CGRADSENS;
                    logFoundDevice("ClimateGuard RadSens", (uint8_t)addr.address);
                    break;
                } else {
                    LOG_DEBUG("Unexpected Device ID for RadSense: addr=0x%x id=0x%x", CGRADSENS_ADDR, registerValue);
                }
                break;

            case 0x48: {
                // T=1oI2C soft reset; an SE050 answers A5 E0 00 3F 19. requestFrom() is
                // required: readBytes() only drains the RX buffer requestFrom() fills.
                const uint8_t getInfo[] = {0x5A, 0xC0, 0x00, 0xFF, 0xFC};
                const uint8_t expectedInfo[] = {0xA5, 0xE0, 0x00, 0x3F, 0x19};
                uint8_t info[sizeof(expectedInfo)] = {0};
                size_t len = 0;
                bool isSE050 = false;

                i2cBus->beginTransmission(addr.address);
                i2cBus->write(getInfo, sizeof(getInfo));
                if (i2cBus->endTransmission() == 0) {
                    delay(2); // guard time before the answer can be read back
                    len = i2cBus->requestFrom((uint8_t)addr.address, (uint8_t)sizeof(info));
                    if (len == sizeof(info)) {
                        for (size_t i = 0; i < sizeof(info); i++)
                            info[i] = i2cBus->read();
                        isSE050 = (memcmp(expectedInfo, info, sizeof(info)) == 0);
                    }
                }

                if (isSE050) {
                    LOG_INFO("NXP SE050 crypto chip found");
                    type = NXP_SE050;
                    break;
                }

                // ADS1X15 default config register is 8583h
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2);
                if (registerValue == 0x8583 || registerValue == 0x8580 || registerValue == 0xf700) {
                    type = ADS1X15;
                    logFoundDevice("ADS1X15 ADC", (uint8_t)addr.address);
                    break;
                }

                LOG_INFO("FT6336U touchscreen found");
                type = FT6336U;
                break;
            }

            case ADS1X15_ADDR_ALT1:
            case ADS1X15_ADDR_ALT2:
            case ADS1X15_ADDR_ALT3: {
                // ADS1X15 default config register is 8583h
                registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2);
                if (registerValue == 0x8583 || registerValue == 0x8580 || registerValue == 0xf700) {
                    type = ADS1X15_ALT;
                    logFoundDevice("ADS1X15_ALT", (uint8_t)addr.address);
                    break;
                }
                break;
            }

            default:
                LOG_INFO("Device found at address 0x%x was not able to be enumerated", (uint8_t)addr.address);
            }
        } else if (err == 4) {
            LOG_ERROR("Unknown error at address 0x%x", (uint8_t)addr.address);
        }

        // Check if a type was found for the enumerated device - save, if so
        if (type != NONE) {
            deviceAddresses[type] = addr;
            foundDevices[addr] = type;
        }
    }
}

void ScanI2CTwoWire::scanPort(I2CPort port)
{
    scanPort(port, nullptr, 0);
}

TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address)
{
    if (address.port == ScanI2C::I2CPort::WIRE) {
        return &Wire;
    } else {
#if defined(SENSECAP_INDICATOR)
        return i2cProxy; // WIRE1 is bridged to the RP2040
#elif WIRE_INTERFACES_COUNT == 2
        return &Wire1;
#else
        return &Wire;
#endif
    }
}

size_t ScanI2CTwoWire::countDevices() const
{
    return foundDevices.size();
}

void ScanI2CTwoWire::logFoundDevice(const char *device, uint8_t address)
{
    LOG_INFO("%s found at address 0x%x", device, address);
}
#endif
