diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a36a627..028e95c63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. - Berry `introspect.module` option to not cache module entry (#23451) - Berry `webserver.remove_route` to revert `webserver.on` (#23452) - Berry `compile` and `tasmota.compile` option to compile in local context (#23457) +- Support for AP33772S USB PD Sink Controller as used in CentyLab RotoPD ### Breaking Changed diff --git a/CODE_OWNERS.md b/CODE_OWNERS.md index 0732e5f25..07513d6ad 100644 --- a/CODE_OWNERS.md +++ b/CODE_OWNERS.md @@ -91,7 +91,7 @@ In addition to @arendst the following code is mainly owned by: | xdrv_77_wizmote | @arendst | xdrv_78_telnet | @arendst | xdrv_79_esp32_ble | @staars, @btsimonh -| xdrv_80 | +| xdrv_80_wireguard_client | @s-hadinger | xdrv_81_esp32_webcam | @gemu, @philrich | xdrv_82_esp32_ethernet | @arendst | xdrv_83_esp32_watch | @gemu @@ -101,11 +101,12 @@ In addition to @arendst the following code is mainly owned by: | xdrv_88_esp32_shelly_pro | @arendst | xdrv_89_ | | xdrv_90_esp32_dingtian_relay | @barbudor -| xdrv_91_ | +| xdrv_91_esp32_twai | @arendst | xdrv_92_ | | xdrv_93_ | | xdrv_94_ | | | +| xdrv_119_i2c_ap33772s | @arendst | xdrv_120_xyzmodem | @arendst | xdrv_121_gpioviewer | @arendst | xdrv_122_file_settings_demo | @arendst diff --git a/I2CDEVICES.md b/I2CDEVICES.md index e442dd6cf..822415a7d 100644 --- a/I2CDEVICES.md +++ b/I2CDEVICES.md @@ -130,6 +130,6 @@ Index | Define | Driver | Device | Address(es) | Bus2 | Descrip 90 | USE_RX8010 | xdrv_56 | RX8010 | 0x32 | Yes | RX8010 RTC from IOTTIMER 91 | USE_MS5837 | xsns_116 | MS5837 | 0x76 | | Pressure and temperature sensor 92 | USE_PCF85063 | xdrv_56 | PCF85063 | 0x51 | | PCF85063 Real time clock - 93 | USE_AS33772S | xdrv_119 | AS33772S | 0x52 | | AS33772S USB PD Sink Controller + 93 | USE_AS33772S | xdrv_119 | AS33772S | 0x52 | Yes | AS33772S USB PD Sink Controller NOTE: Bus2 supported on ESP32 only. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d30440302..d1c1a9590 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -121,6 +121,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm - Command `JsonPP |backlog ;...` to enable JSON PP only once - Support for multi channel AU915-928 LoRaWanBridge by Rob Clark [#23372](https://github.com/arendst/Tasmota/issues/23372) - Support for LoRaWan Rx1 and Rx2 profiles [#23394](https://github.com/arendst/Tasmota/issues/23394) +- Support for AP33772S USB PD Sink Controller as used in CentyLab RotoPD - Allow temporary change of DisplayDimmer [#23406](https://github.com/arendst/Tasmota/issues/23406) - WebUI status line for MQTT and TLS, added `FUNC_WEB_STATUS_LEFT` and `FUNC_WEB_STATUS_RIGHT` event [#23354](https://github.com/arendst/Tasmota/issues/23354) - WebUI heap status [#23356](https://github.com/arendst/Tasmota/issues/23356) diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index f84b5b734..18b39524e 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -787,6 +787,7 @@ // #define USE_SPL06_007 // [I2cDriver87] Enable SPL06_007 pressure and temperature sensor (I2C addresses 0x76) (+2k5 code) // #define USE_QMP6988 // [I2cDriver88] Enable QMP6988 pressure and temperature sensor (I2C address 0x56 or 0x70) (+2k9 code) // #define USE_MS5837 // [I2cDriver91] Enable MS5837 sensor (I2C address 0x76) (+2k7 code) +// #define USE_AP33772S // [I2cDriver93] Enable AP33772S USB PD Sink Controller (I2C addresses 0x52) (+3k1 code) // #define USE_RTC_CHIPS // Enable RTC chip support and NTP server - Select only one // #define USE_DS3231 // [I2cDriver26] Enable DS3231 RTC - used by Ulanzi TC001 (I2C address 0x68) (+1k2 code) diff --git a/tasmota/tasmota_support/support_features.ino b/tasmota/tasmota_support/support_features.ino index b92e78451..8fcb48129 100644 --- a/tasmota/tasmota_support/support_features.ino +++ b/tasmota/tasmota_support/support_features.ino @@ -952,8 +952,12 @@ constexpr uint32_t feature[] = { #ifdef USE_XYZMODEM 0x00010000 | // xdrv_120_xyzmodem.ino #endif -// 0x00020000 | // -// 0x00040000 | // +#ifdef USE_WIREGUARD + 0x00020000 | // xdrv_80_wireguard_client +#endif +#if defined(USE_I2C) && defined(USE_AP33772S) + 0x00040000 | // xdrv_119_i2c_ap33772s +#endif // 0x00080000 | // // 0x00100000 | // // 0x00200000 | // diff --git a/tasmota/tasmota_xdrv_driver/xdrv_119_i2c_ap33772s.ino b/tasmota/tasmota_xdrv_driver/xdrv_119_i2c_ap33772s.ino new file mode 100644 index 000000000..2835772e4 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_119_i2c_ap33772s.ino @@ -0,0 +1,951 @@ +/* + xdrv_119_i2c_ap33772s.ino - AP33772S USB PD Sink Controller support for Tasmota + + SPDX-FileCopyrightText: 2025 Theo Arends + + SPDX-License-Identifier: GPL-3.0-only +*/ + +#ifdef USE_I2C +#ifdef USE_AP33772S +/*********************************************************************************************\ + * AP33772S - USB Power Delivery (PD) Sink Controller + * + * SPR - Standard Power Range = PD3.0 + * Fixed 3A: 5V, 9V, 12V, 15V, 20V (15-60W) and 5A: 20V (60-100W) + * PPS - Programmable Power Supply (3V3 to 21V in steps of 20mV (AP33772S = 100mV)) + * EPR - Extended Power Range = PD3.1 + * Fixed 5A: 28V, 36V, 48V (100-240W) + * AVS - Adjustable Voltage Supply (15V to 28V or 48V (AP33772S = 28V) in steps of 100mV (AP33772S = 200mV)) + * PDO - Power Delivery Object or Power Data Object + * + * Datasheet https://www.diodes.com/datasheet/download/AP33772S.pdf + * Library https://github.com/CentyLab/AP33772S-Cpp + * Hardware https://github.com/CentyLab/RotoPD + * + * Tested using: + * UGreen CD226 100W 4-Port USB GaN Fast Charger (PPS only) + * Novoo NCEU120D-255C2 120W 3-Port USB GaN Fast Charger (PPS only) + * + * KOWSI KWS-X1 USB type C Voltage and Current monitor (PPS and AVS) + * + * I2C Address: 0x52 +\*********************************************************************************************/ + +#define XDRV_119 119 +#define XI2C_93 93 // See I2CDEVICES.md + +//#define AP33772S_DEBUG + +#define AP33772S_I2C_ADDR 0x52 +#define AP33772S_MAX_PDO_ENTRIES 13 // Maximum number of PDO entries + +#define AP33772S_CMD_STATUS 0x01 // Reset to 0 after every Read +#define AP33772S_CMD_MASK 0x02 +#define AP33772S_CMD_OPMODE 0x03 +#define AP33772S_CMD_CONFIG 0x04 +#define AP33772S_CMD_PDCONFIG 0x05 +#define AP33772S_CMD_SYSTEM 0x06 + +// Temperature setting register +#define AP33772S_CMD_TR25 0x0C +#define AP33772S_CMD_TR50 0x0D +#define AP33772S_CMD_TR75 0x0E +#define AP33772S_CMD_TR100 0x0F + +// Power reading related +#define AP33772S_CMD_VOLTAGE 0x11 +#define AP33772S_CMD_CURRENT 0x12 +#define AP33772S_CMD_TEMP 0x13 +#define AP33772S_CMD_VREQ 0x14 +#define AP33772S_CMD_IREQ 0x15 + +#define AP33772S_CMD_VSELMIN 0x16 // Minimum Selection Voltage +#define AP33772S_CMD_UVPTHR 0x17 +#define AP33772S_CMD_OVPTHR 0x18 +#define AP33772S_CMD_OCPTHR 0x19 +#define AP33772S_CMD_OTPTHR 0x1A +#define AP33772S_CMD_DRTHR 0x1B + +#define AP33772S_CMD_SRCPDO 0x20 + +#define AP33772S_CMD_PD_REQMSG 0x31 +#define AP33772S_CMD_PD_CMDMSG 0x32 +#define AP33772S_CMD_PD_MSGRLT 0x33 + +typedef struct RDO_DATA_t { + union { + struct { + unsigned int VOLTAGE_SEL: 8; // Bits 7:0, Output Voltage Select + unsigned int CURRENT_SEL: 4; // Bits 11:8, Operating Current Select + unsigned int PDO_INDEX: 4; // Bits 15:12, Source PDO index select + } REQMSG_Fields; + struct { + byte byte0; + byte byte1; + }; + unsigned long data; + }; +} RDO_DATA_t; + +typedef struct SRC_SPRandEPR_PDO_Fields_t { + union { + struct { + unsigned int voltage_max: 8; // Bits 7:0, VOLTAGE_MAX field + unsigned int peak_current: 2; // Bits 9:8, PEAK_CURRENT field + unsigned int current_max: 4; // Bits 13:10, CURRENT_MAX field + unsigned int type: 1; // Bit 14, TYPE field + unsigned int detect: 1; // Bit 15, DETECT field + } fixed; + struct { + unsigned int voltage_max: 8; // Bits 7:0, VOLTAGE_MAX field + unsigned int voltage_min: 2; // Bits 9:8, VOLTAGE_MIN field + unsigned int current_max: 4; // Bits 13:10, CURRENT_MAX field + unsigned int type: 1; // Bit 14, TYPE field + unsigned int detect: 1; // Bit 15, DETECT field + } pps; + struct { + unsigned int voltage_max: 8; // Bits 7:0, VOLTAGE_MAX field + unsigned int voltage_min: 2; // Bits 9:8, VOLTAGE_MIN field + unsigned int current_max: 4; // Bits 13:10, CURRENT_MAX field + unsigned int type: 1; // Bit 14, TYPE field + unsigned int detect: 1; // Bit 15, DETECT field + } avs; + struct { + uint8_t byte0; + uint8_t byte1; + }; + }; + unsigned long data; +} SRC_SPRandEPR_PDO_Fields_t; + +typedef struct Ap33772s_t { + SRC_SPRandEPR_PDO_Fields_t SRC_SPRandEPRpdoArray[AP33772S_MAX_PDO_ENTRIES]; + + int voltage_PPS; + int current_PPS; + + int voltage_AVS_byte; + int current_AVS_byte; + int index_AVS; + + int slider_voltage; + int slider_current; + + uint8_t read_buffer[2]; + uint8_t write_buffer[2]; + int8_t index_PPS_user; + int8_t index_AVS_user; + bool output; + uint8_t bus; +} Ap33772s_t; +Ap33772s_t* Ap33772s = nullptr; + +/*********************************************************************************************/ + +#ifdef AP33772S_DEBUG + +String AP33772S_DisplayCurrentRange(uint32_t current_max) { + String result = "Invalid value"; + switch (current_max) { + case 0: + result = "0.00A ~ 1.24A (Less than)"; + break; + case 1: + result = "1.25A ~ 1.49A"; + break; + case 2: + result = "1.50A ~ 1.74A"; + break; + case 3: + result = "1.75A ~ 1.99A"; + break; + case 4: + result = "2.00A ~ 2.24A"; + break; + case 5: + result = "2.25A ~ 2.49A"; + break; + case 6: + result = "2.50A ~ 2.74A"; + break; + case 7: + result = "2.75A ~ 2.99A"; + break; + case 8: + result = "3.00A ~ 3.24A"; + break; + case 9: + result = "3.25A ~ 3.49A"; + break; + case 10: + result = "3.50A ~ 3.74A"; + break; + case 11: + result = "3.75A ~ 3.99A"; + break; + case 12: + result = "4.00A ~ 4.24A"; + break; + case 13: + result = "4.25A ~ 4.49A"; + break; + case 14: + result = "4.50A ~ 4.99A"; + break; + case 15: + result = "5.00A ~ (More than)"; + break; + default: + break; + } + return result; +} + +String AP33772S_DisplayEPRVoltageMin(uint32_t current_max) { + String result = "Invalid value"; + switch (current_max) { + case 0: + result = "Reserved"; + break; + case 1: + result = "15000mV ~"; + break; + case 2: + result = "15000mV < VOLTAGE_MIN ≤ 20000mV "; + break; + case 3: + result = "others"; + break; + default: + break; + } + return result; +} + +String AP33772S_DisplaySPRVoltageMin(uint32_t current_max) { + String result = "Invalid value"; + switch (current_max) { + case 0: + result = "Reserved"; + break; + case 1: + result = "3300mV ~"; + break; + case 2: + result = "3300mV < VOLTAGE_MIN ≤ 5000mV "; + break; + case 3: + result = "others"; + break; + default: + break; + } + return result; +} + +/** + * @brief Decode PDO information from SRC_SPRandEPR_PDO_Fields + * @param pdoIndex feed from loop + */ +void AP33772S_DisplayPDOInfo(int pdoIndex) { + // Determine if it's SPR or EPR based on pdoIndex + bool isEPR = (pdoIndex >= 7 && pdoIndex <= 12); // 1-6 for SPR, 7-12 for EPR + // Check if both bytes are zero + if (Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].byte0 == 0 && Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].byte1 == 0) { + return; // If both bytes are zero, exit the function + } + + // Print the PDO type and index + // Now, the individual fields can be accessed through the union in the struct + if (Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].fixed.type == 0) { // Fixed PDO + // Print parsed values + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: %s_PDO%d Fixed_PDO %dmV %s"), + (pdoIndex <= 6) ? "SPR" : "EPR", + pdoIndex+1, + Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].fixed.voltage_max * (isEPR ? 200 : 100), // Voltage in 200mV units for EPR, 100mV for SPR + AP33772S_DisplayCurrentRange(Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].fixed.current_max).c_str() // Assuming displayCurrentRange function is available + ); + } else { // PPS or AVS PDO + // Print parsed values + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: %s_PDO%d %s_PDO %s %dmV %s"), + (pdoIndex <= 6) ? "SPR" : "EPR", + pdoIndex+1, + (isEPR) ? "AVS" : "PPS", + (isEPR) ? AP33772S_DisplayEPRVoltageMin(Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].avs.voltage_min).c_str() : // Assuming displayVoltageMin function is available + AP33772S_DisplaySPRVoltageMin(Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].pps.voltage_min).c_str(), // Assuming displayVoltageMin function is available + Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].fixed.voltage_max * (isEPR ? 200 : 100), // Maximum Voltage in 200mV units for EPR, 100mV for SPR + AP33772S_DisplayCurrentRange(Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex].fixed.current_max).c_str() // Assuming displayCurrentRange function is available + ); + + } +} + +#endif // AP33772S_DEBUG + +/*********************************************************************************************/ + +void AP33772S_I2cRead(uint8_t cmdAddr, byte len) { + // clear readBuffer + for (uint32_t i = 0; i < 2; i++) { + Ap33772s->read_buffer[i] = 0; + } + I2cReadBuffer(AP33772S_I2C_ADDR, cmdAddr, Ap33772s->read_buffer, len, Ap33772s->bus); +} + +void AP33772S_I2cWrite(byte cmdAddr, byte len) { + I2cWriteBuffer(AP33772S_I2C_ADDR, cmdAddr, Ap33772s->write_buffer, len, Ap33772s->bus); +} + +/*********************************************************************************************/ + +/** + * @brief take in current in mA unit + * @return value from 0 to 15 + * @return -1 if there is an error + */ +int AP33772S_CurrentMap(int current) { + // Check if the value is out of bounds + if (current < 0 || current > 5000) { + return -1; // Return -1 for invalid inputs + } + + // If value is below 1250, return 0 + if (current < 1250) { + return 0; + } + + // Calculate the result for ranges above 1250 + return ((current - 1250) / 250) + 1; +} + +/** + * @brief Request fixed PDO voltage, work for both standard and EPR mode + * @param pdoIndex index 1 + * @param max_current unit in mA + */ +void AP33772S_SetFixPDO(int pdoIndex, int max_current) { + RDO_DATA_t rdoData; + + // Max current sanity check + if (max_current <= 0) { return; } + + // For Fix voltage, only need to set PDO_INDEX and CURRENT_SEL + // No need to change the selected voltage + // handle the same in standard as well as EPR + + // PDO index need to be fixed type + if (Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].fixed.type == 0) { + // Now that we are in fix PDO mode +// AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: Type is fixed")); + + rdoData.REQMSG_Fields.PDO_INDEX = pdoIndex; // Index 1 + + // Serial.printf("You entered current: %dmA", max_current); //DEBUG + // Serial.println(SRC_SPRandEPRpdoArray[pdoIndex].fixed.current_max); //DEBUG + // Serial.println(currentMap(max_current)); //DEBUG + if (AP33772S_CurrentMap(max_current) > Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].fixed.current_max) { + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: Current not in range")); + return; // Check if current setting is in range + } + + rdoData.REQMSG_Fields.CURRENT_SEL = AP33772S_CurrentMap(max_current); + // Note: For profile less than or equal to 3A power, CURRENT_SEL = 9 will not work. + // rdoData.REQMSG_Fields.CURRENT_SEL = 9; + + Ap33772s->write_buffer[0] = rdoData.byte0; // Store the upper 8 bits + Ap33772s->write_buffer[1] = rdoData.byte1; // Store the lower 8 bits + AP33772S_I2cWrite(AP33772S_CMD_PD_REQMSG, 2); + } +} + +/** + * @brief Request PPS voltage + * @param pdoIndex index 1 + * @param target_voltage unit in mV + * @param max_current unit in mA + * @bug only work if min PPS voltage is 3.3V + */ +void AP33772S_SetPPSPDO(int pdoIndex, int target_voltage, int max_current) { + RDO_DATA_t rdoData; + + int voltage_min_decoded; + // Sanity check include, check if the value is in EPR range (index < 8) and also AVS mode + if ((pdoIndex < 8) && (Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].pps.type == 1)) { +// AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: Type is PPS")); + // Now that we are in PPS mode + + rdoData.REQMSG_Fields.PDO_INDEX = pdoIndex; // Index 1 + + if (AP33772S_CurrentMap(max_current) > Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].pps.current_max) { + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: PPS Current not in range")); + return; // Check if current setting is in range + } + + // Decode voltage_min + if (Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].pps.voltage_min > 0) { + voltage_min_decoded = 3300; + } + + if ((target_voltage < voltage_min_decoded) || + (target_voltage > Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].pps.voltage_max*100)) { + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: PPS Voltage not in range")); + return; // Check if current setting is in range + } + + rdoData.REQMSG_Fields.VOLTAGE_SEL = target_voltage/100; // Output Voltage in 200mV units + rdoData.REQMSG_Fields.CURRENT_SEL = AP33772S_CurrentMap(max_current); + + Ap33772s->write_buffer[0] = rdoData.byte0; // Store the upper 8 bits + Ap33772s->write_buffer[1] = rdoData.byte1; // Store the lower 8 bits + AP33772S_I2cWrite(AP33772S_CMD_PD_REQMSG, 2); + } +} + +/** + * @brief Request AVS voltage + * @param pdoIndex index 1 + * @param target_voltage unit in mV + * @param max_current unit in mA + * @bug only work if min AVS voltage is 15V, AVS max voltage is not capped at 30V + */ +void AP33772S_SetAVSPDO(int pdoIndex, int target_voltage, int max_current) { + RDO_DATA_t rdoData; + + int voltage_min_decoded; + // Sanity check include, check if the value is in EPR range (index >= 8) and also AVS mode + if ((pdoIndex >= 8) && (Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].avs.type == 1)) { +// AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: Type is AVS")); + // Now that we are in AVS mode + + // Decode voltage_min + if (Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].avs.voltage_min > 0) { + voltage_min_decoded = 15000; + } + + rdoData.REQMSG_Fields.PDO_INDEX = pdoIndex; // Index 1 + + if (AP33772S_CurrentMap(max_current) > Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].avs.current_max) { + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: AVS Current not in range.")); + return; // Check if current setting is in range + } + + // Decode voltage_min + if (Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].avs.voltage_min > 0) { + voltage_min_decoded = 15000; + } + + if ((target_voltage < voltage_min_decoded) || + (target_voltage > Ap33772s->SRC_SPRandEPRpdoArray[pdoIndex-1].avs.voltage_max*200)) { + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: AVS Voltage not in range")); + return; // Check if current setting is in range + } + + rdoData.REQMSG_Fields.VOLTAGE_SEL = target_voltage/200; // Output Voltage in 200mV units + rdoData.REQMSG_Fields.CURRENT_SEL = AP33772S_CurrentMap(max_current); + + Ap33772s->write_buffer[0] = rdoData.byte0; // Store the upper 8 bits + Ap33772s->write_buffer[1] = rdoData.byte1; // Store the lower 8 bits + AP33772S_I2cWrite(AP33772S_CMD_PD_REQMSG, 2); + + Ap33772s->index_AVS = rdoData.REQMSG_Fields.PDO_INDEX; + Ap33772s->voltage_AVS_byte = rdoData.REQMSG_Fields.VOLTAGE_SEL; + Ap33772s->current_AVS_byte = rdoData.REQMSG_Fields.CURRENT_SEL; + } + return; +} + +/** + * @brief Set resistance value of 10K NTC at 25C, 50C, 75C and 100C. + * Default is 10000, 4161, 1928, 974Ohm + * @param TR25, TR50, TR75, TR100 unit in Ohm + * @attention Blocking function due to long I2C write, min blocking time 15ms + */ +void AP33772S_SetNTC(int TR25, int TR50, int TR75, int TR100) { + Ap33772s->write_buffer[0] = TR25 & 0xff; + Ap33772s->write_buffer[1] = (TR25 >> 8) & 0xff; + AP33772S_I2cWrite(AP33772S_CMD_TR25, 2); + delay(5); + Ap33772s->write_buffer[0] = TR50 & 0xff; + Ap33772s->write_buffer[1] = (TR50 >> 8) & 0xff; + AP33772S_I2cWrite(AP33772S_CMD_TR50, 2); + delay(5); + Ap33772s->write_buffer[0] = TR75 & 0xff; + Ap33772s->write_buffer[1] = (TR75 >> 8) & 0xff; + AP33772S_I2cWrite(AP33772S_CMD_TR75, 2); + delay(5); + Ap33772s->write_buffer[0] = TR100 & 0xff; + Ap33772s->write_buffer[1] = (TR100 >> 8) & 0xff; + AP33772S_I2cWrite(AP33772S_CMD_TR100, 2); +} + +/** + * @brief Read NTC temperature + * @return tempearture in C + */ +int AP33772S_ReadTemp(void) { + AP33772S_I2cRead(AP33772S_CMD_TEMP, 1); + return Ap33772s->read_buffer[0]; // I2C read return 1C/LSB +} + +/** + * @brief Read VBUS voltage + * @return voltage in mV + */ +int AP33772S_ReadVoltage(void) { + AP33772S_I2cRead(AP33772S_CMD_VOLTAGE, 2); + return ((Ap33772s->read_buffer[1] << 8) | Ap33772s->read_buffer[0]) * 80; // I2C read return 80mV/LSB +} + +/** + * @brief Read VBUS current + * @return current in mA + */ +int AP33772S_ReadCurrent(void) { + AP33772S_I2cRead(AP33772S_CMD_CURRENT, 1); + return Ap33772s->read_buffer[0] * 24; // I2C read return 24mA/LSB +} + +/** + * @brief Read VREQ The latest requested voltage negotiated with the source + * @return voltage in mV + */ +int AP33772S_ReadVREQ(void) { + AP33772S_I2cRead(AP33772S_CMD_VREQ, 1); + return Ap33772s->read_buffer[0] * 50; // I2C read return 50mV/LSB +} + +/** + * @brief Read IREQ The latest requested voltage negotiated with the source + * @return current in mA + */ +int AP33772S_ReadIREQ(void) { + AP33772S_I2cRead(AP33772S_CMD_IREQ, 1); + return Ap33772s->read_buffer[0] * 10; // I2C read return 10mA/LSB +} + +/** + * @brief Read VSELMIN register. The Minimum Selection Voltage + * @return voltage in mV + */ +int AP33772S_ReadVSELMIN(void) { + AP33772S_I2cRead(AP33772S_CMD_VSELMIN, 1); + return Ap33772s->read_buffer[0] * 200; // I2C read return 200mV/LSB +} + +/** + * @brief Set VSELMIN register. The Minimum Selection Voltage + * @param voltage in mV + */ +void AP33772S_SetVSELMIN(int voltage) { + Ap33772s->write_buffer[0] = voltage / 200; // 200mV/LSB + AP33772S_I2cWrite(AP33772S_CMD_VSELMIN, 1); +} + +/** + * @brief Read UVP Threshold, percentage(%) of VREQ + * @return percentage, should only return 80%, 75%, or 70%. -1 for error + */ +int AP33772S_ReadUVPTHR(void) { + AP33772S_I2cRead(AP33772S_CMD_UVPTHR, 1); + switch(Ap33772s->read_buffer[0]) { + case 1: + return 80; + case 2: + return 75; + case 3: + return 70; + } + return -1; +} + +/** + * @brief Set UVP Threshold, percentage(%) of VREQ + * @param value percentage. If 80% then value = 80 + */ +void AP33772S_SetUVPTHR(int value) { + if ((value >= 70) && (value <= 80)) { + switch(value) { + case 80: + Ap33772s->write_buffer[0] = 1; + break; + case 75: + Ap33772s->write_buffer[0] = 2; + break; + case 70: + Ap33772s->write_buffer[0] = 3; + break; + default: + return; // Error + } + AP33772S_I2cWrite(AP33772S_CMD_UVPTHR, 1); + } +} + +/** + * @brief Read OVP Threshold Voltage is the VREQ voltage plus OVPTHR offset voltage (mV) + * @return voltage in mV + */ +int AP33772S_ReadOVPTHR(void) { + AP33772S_I2cRead(AP33772S_CMD_OVPTHR, 1); + return Ap33772s->read_buffer[0] * 80; // I2C read return 80mV/LSB +} + +/** + * @brief Set OVP Threshold Voltage is the VREQ voltage plus OVPTHR offset voltage (mV) + * @param voltage in mV + */ +void AP33772S_SetOVPTHR(int value) { + Ap33772s->write_buffer[0] = value / 80; // 80mV/LSB + AP33772S_I2cWrite(AP33772S_CMD_OVPTHR, 1); +} + +int AP33772S_ReadOCPTHR(void) { + AP33772S_I2cRead(AP33772S_CMD_OCPTHR, 1); + return Ap33772s->read_buffer[0] * 50; // I2C read return 50mA/LSB +} + +void AP33772S_SetOCPTHR(int value) { + Ap33772s->write_buffer[0] = value/50; // 50mA/LSB + AP33772S_I2cWrite(AP33772S_CMD_OCPTHR, 1); +} + +int AP33772S_ReadOTPTHR(void) { + AP33772S_I2cRead(AP33772S_CMD_OTPTHR, 1); + return Ap33772s->read_buffer[0]; // I2C read return 1C/LSB +} + +void AP33772S_SetOTPTHR(int value) { + Ap33772s->write_buffer[0] = value; // 1C/LSB + AP33772S_I2cWrite(AP33772S_CMD_OTPTHR, 1); +} + +int AP33772S_ReadDRTHR(void) { + AP33772S_I2cRead(AP33772S_CMD_DRTHR, 1); + return Ap33772s->read_buffer[0]; // I2C read return 1C/LSB +} + +void AP33772S_SetDRTHR(int value) { + Ap33772s->write_buffer[0] = value; // 1C/LSB + AP33772S_I2cWrite(AP33772S_CMD_DRTHR, 1); +} + +/** + * @brief Turn on/off the NMOS switch + * @param flag 0 or 1 for OFF/ON + * @return 1 if flag make sense + * @bug can add code to check Vout voltage to ensure on or off, worry about settle time required for VOUT + */ +bool AP33772S_SetOutput(uint8_t flag) { + switch(flag) { + case 0: + Ap33772s->write_buffer[0] = 0b00010001; // Turn off + AP33772S_I2cWrite(AP33772S_CMD_SYSTEM, 1); + return true; + break; //Sanity + case 1: + Ap33772s->write_buffer[0] = 0b00010010; // Turn on + AP33772S_I2cWrite(AP33772S_CMD_SYSTEM, 1); + return true; + break; //Sanity + default: + return false; // Error, dont know the input + } +} + +/*********************************************************************************************/ + +void AP33772S_Init(void) { + uint32_t bus; + for (bus = 0; bus < 2; bus++) { + if (!I2cSetDevice(AP33772S_I2C_ADDR, bus)) { + continue; + } + uint8_t data[26]; + delay(100); // We expect only one chip + + if (!I2cReadBuffer(AP33772S_I2C_ADDR, AP33772S_CMD_SRCPDO, data, sizeof(data), bus)) { + Ap33772s = (Ap33772s_t*)calloc(sizeof(Ap33772s_t), 1); // Need calloc to reset registers to 0/false + if (nullptr == Ap33772s) { return; } + Ap33772s->bus = bus; + I2cSetActiveFound(AP33772S_I2C_ADDR, "AP33772S", Ap33772s->bus); + + Ap33772s->index_PPS_user = -1; + Ap33772s->index_AVS_user = -1; + // Store the bytes in the array of structs + for (uint32_t i = 0; i < sizeof(data); i += 2) { + uint32_t pdo_index = (i / 2); // Calculate the PDO index + Ap33772s->SRC_SPRandEPRpdoArray[pdo_index].byte0 = data[i]; + Ap33772s->SRC_SPRandEPRpdoArray[pdo_index].byte1 = data[i + 1]; + + // @brief Search through the list of profile and look for PPS, AVS + // @bug If system has 2 PPS profiles. The index only show the last one. Use displayPDOInfo() to check. + if ((pdo_index < 7) && (Ap33772s->SRC_SPRandEPRpdoArray[pdo_index].pps.type == 1)) { + Ap33772s->index_PPS_user = pdo_index +1; + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: Found PPS profile %d"), Ap33772s->index_PPS_user); + } + else if ((pdo_index >= 7) && (Ap33772s->SRC_SPRandEPRpdoArray[pdo_index].avs.type == 1)) { + Ap33772s->index_AVS_user = pdo_index +1; + AddLog(LOG_LEVEL_DEBUG, PSTR("AP3: Found AVS profile %d"), Ap33772s->index_AVS_user); + } +#ifdef AP33772S_DEBUG + AP33772S_DisplayPDOInfo(pdo_index); +#endif // AP33772S_DEBUG + } + + AP33772S_SetOutput(0); // Default output OFF + Ap33772s->voltage_PPS = 5000; // Default 5V + Ap33772s->current_PPS = 3000; // Default 3A + AP33772S_SetPPSPDO(Ap33772s->index_PPS_user, Ap33772s->voltage_PPS, Ap33772s->current_PPS); + break; + } + } +} + +/** + * Some charger will disconnect with sink if no refresh request is sent within 1s + * Spamming request to keep power on +*/ +void AP33772S_KeepAlive(void) { + if (Ap33772s->output) { + AP33772S_SetPPSPDO(Ap33772s->index_PPS_user, Ap33772s->voltage_PPS, Ap33772s->current_PPS); + } +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +const char kAP33772SCommands[] PROGMEM = "PD|" // Prefix + "Output|Voltage|Current"; + +void (* const AP33772SCommands[])(void) PROGMEM = { + &CmndPDOutput, &CmndPDVoltage, &CmndPDCurrent }; + +void CmndPDOutput(void) { + // PDOutput - Show output state + // PDOutput 0 - Turn output off + // PDOutput 1 - Turn output on + // PDOutput 2 - Toggle output + if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 2)) { + switch (XdrvMailbox.payload) { + case 0: // Off + Ap33772s->output = 0; + break; + case 1: // On + Ap33772s->output = 1; + break; + case 2: // Toggle + Ap33772s->output ^= 1; + break; + } + AP33772S_SetOutput(Ap33772s->output); + } + ResponseCmndChar(GetStateText(Ap33772s->output)); +} + +void CmndPDVoltage(void) { + // PDVoltage - Show PPS voltage + // PDVoltage 5000 - Set PPS voltage to 5000 mV or 5V + if ((XdrvMailbox.payload >= 3300) && (XdrvMailbox.payload <= 20000)) { + Ap33772s->voltage_PPS = XdrvMailbox.payload; + if (Ap33772s->current_PPS < 1250) { + Ap33772s->current_PPS = 1250; // Default 1.25A + } + AP33772S_SetPPSPDO(Ap33772s->index_PPS_user, Ap33772s->voltage_PPS, Ap33772s->current_PPS); + } + uint32_t voltage = AP33772S_ReadVoltage(); + ResponseCmndNumber(voltage); +} + +void CmndPDCurrent(void) { + // PDCurrent - Show max PPS current + // PDCurrent 2000 - Set max PPS current to 2000mA or 2A + if ((XdrvMailbox.payload >= 1250) && (XdrvMailbox.payload <= 5000)) { + Ap33772s->current_PPS = XdrvMailbox.payload; + if (Ap33772s->voltage_PPS < 3300) { + Ap33772s->voltage_PPS = 5000; // Default 5V + } + AP33772S_SetPPSPDO(Ap33772s->index_PPS_user, Ap33772s->voltage_PPS, Ap33772s->current_PPS); + } + uint32_t current = AP33772S_ReadCurrent(); + ResponseCmndNumber(current); +} + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +#ifdef USE_WEBSERVER +const char HTTP_MSG_SLIDER_AP33772S[] PROGMEM = + "" + "" + "
" + " %s" + "" + "
" + "" + ""; + +const char HTTP_MSG_BUTTON_AP33772S[] PROGMEM = + ""; + +void AP33772S_WebAddMainSlider(void) { + WSContentSend_P(HTTP_TABLE100); + + Ap33772s->slider_voltage = Ap33772s->voltage_PPS; + WSContentSend_P(HTTP_MSG_SLIDER_AP33772S, // Voltage + 2, 100, // colspan, width + 1, // d1191 - Unique HTML id (not used) + 1, // s1191 - Unique span HTML id - Used for label updates + "V", // label + 1, // i1191 - Unique range HTML id - Used for slider updates + 3300, 20000, // Range 3V3 to 20V + Ap33772s->slider_voltage, // Range value + 1, // i1191 - Value id + 1, 1, 1, 1 // s1191, i1191, i1191, s1191 - Script ids + ); + WSContentSend_P(PSTR("")); + + Ap33772s->slider_current = Ap33772s->current_PPS; + WSContentSend_P(HTTP_MSG_SLIDER_AP33772S, // Current + 1, 85, // colspan, width + 2, // d1192 - Unique HTML id (not used) + 2, // s1192 - Unique span HTML id - Used for label updates + "A max", // label + 2, // i1192 - Unique range HTML id - Used for slider updates + 1250, 5000, // Range 1.25A to 5A + Ap33772s->slider_current, // Range value + 2, // i1192 - Value id + 2, 2, 2, 2 // s1192, i1192, i1192, s1192 - Script ids + ); + WSContentSend_P(HTTP_MSG_BUTTON_AP33772S, // Output + WebColor((Ap33772s->output) ? COL_BUTTON : COL_BUTTON_OFF)); + WSContentSend_P(PSTR("")); + + WSContentSend_P(PSTR("")); +} + +/*-------------------------------------------------------------------------------------------*/ + +void AP33772S_WebUpdateButtonSliderState(void) { + WSContentSend_P(PSTR("")); // Terminate current {t} + WSContentSend_P(HTTP_MSG_EXEC_JAVASCRIPT); // "voltage_PPS != Ap33772s->slider_voltage) { + if (WebUpdateSliderTime()) { + Ap33772s->slider_voltage = Ap33772s->voltage_PPS; + } + WSContentSend_P(PSTR("eb('i1191').value=%d;eb('s1191').innerHTML=%d/1000;"), + Ap33772s->voltage_PPS, Ap33772s->voltage_PPS); + } + if (Ap33772s->current_PPS != Ap33772s->slider_current) { + if (WebUpdateSliderTime()) { + Ap33772s->slider_current = Ap33772s->current_PPS; + } + WSContentSend_P(PSTR("eb('i1192').value=%d;eb('s1192').innerHTML=%d/1000;"), + Ap33772s->current_PPS, Ap33772s->current_PPS); + } + WSContentSend_P(PSTR("eb('k119').style='background:#%06x';"), + WebColor((Ap33772s->output) ? COL_BUTTON : COL_BUTTON_OFF)); + WSContentSend_P(PSTR("\">{t}")); // Restart {t} = +} + +/*********************************************************************************************/ + +void AP33772S_WebGetArg(void) { + char tmp[8]; // WebGetArg numbers only + char svalue[32]; // Command and number parameter + + WebGetArg("i1191", tmp, sizeof(tmp)); + if (strlen(tmp)) { + snprintf_P(svalue, sizeof(svalue), PSTR("PDVoltage %s"), tmp); + ExecuteWebCommand(svalue); + } + WebGetArg("i1192", tmp, sizeof(tmp)); + if (strlen(tmp)) { + snprintf_P(svalue, sizeof(svalue), PSTR("PDCurrent %s"), tmp); + ExecuteWebCommand(svalue); + } + WebGetArg(PSTR("k119"), tmp, sizeof(tmp)); + if (strlen(tmp)) { + snprintf_P(svalue, sizeof(svalue), PSTR("PDOutput 2")); + ExecuteWebCommand(svalue); + } +} +#endif // USE_WEBSERVER + +/*********************************************************************************************/ + +void AP33772S_Show(bool json) { + float f_voltage = (float)AP33772S_ReadVoltage() / 1000; + float f_current = (float)AP33772S_ReadCurrent() / 1000; + float f_power = f_voltage * f_current; + float f_temperature = ConvertTemp(AP33772S_ReadTemp()); + + if (json) { + ResponseAppend_P(",\"AP33772S\":{\"" D_JSON_VOLTAGE "\":%*_f,\"" D_JSON_CURRENT "\":%*_f,\"" D_JSON_POWERUSAGE "\":%*_f,\"" D_JSON_TEMPERATURE "\":%*_f}", + Settings->flag2.voltage_resolution, &f_voltage, + Settings->flag2.current_resolution, &f_current, + Settings->flag2.wattage_resolution, &f_power, + Settings->flag2.temperature_resolution, &f_temperature); +#ifdef USE_WEBSERVER + } else { + WSContentSend_Voltage("AP33772S", f_voltage); + WSContentSend_Current("AP33772S", f_current); + WSContentSend_PD(HTTP_SNS_F_POWER, "AP33772S", Settings->flag2.wattage_resolution, &f_power); + WSContentSend_Temp("AP33772S", f_temperature); + AP33772S_WebUpdateButtonSliderState(); +#endif // USE_WEBSERVER + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv119(uint32_t function) { + if (!I2cEnabled(XI2C_93)) { return false; } + + bool result = false; + + if (FUNC_INIT == function) { + AP33772S_Init(); + } + else if (Ap33772s) { + switch (function) { + case FUNC_EVERY_250_MSECOND: + AP33772S_KeepAlive(); + break; + case FUNC_JSON_APPEND: + AP33772S_Show(1); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + AP33772S_Show(0); + break; + case FUNC_WEB_ADD_MAIN_BUTTON: + AP33772S_WebAddMainSlider(); + break; + case FUNC_WEB_GET_ARG: + AP33772S_WebGetArg(); + break; +#endif // USE_WEBSERVER + case FUNC_COMMAND: + result = DecodeCommand(kAP33772SCommands, AP33772SCommands); + break; + case FUNC_ACTIVE: + result = true; + break; + } + } + return result; +} + +#endif // USE_AP33772S +#endif // USE_I2C + diff --git a/tools/decode-status.py b/tools/decode-status.py index acd9ba636..1ea0792cd 100755 --- a/tools/decode-status.py +++ b/tools/decode-status.py @@ -312,7 +312,7 @@ a_features = [[ "USE_SPI_LORA","USE_SPL06_007","USE_QMP6988","USE_WOOLIIS", "USE_HX711_M5SCALES","USE_RX8010","USE_PCF85063","USE_ESP32_TWAI", "USE_C8_CO2_5K","USE_WIZMOTE","USE_V9240","USE_TELNET", - "USE_XYZMODEM","","","", + "USE_XYZMODEM","USE_WIREGUARD","USE_AP33772S","", "","","","", "","","","", "","","","" @@ -343,7 +343,7 @@ else: obj = json.load(fp) def StartDecode(): - print ("\n*** decode-status.py v14.5.0.3 by Theo Arends and Jacek Ziolkowski ***") + print ("\n*** decode-status.py v14.6.0.2 by Theo Arends and Jacek Ziolkowski ***") # print("Decoding\n{}".format(obj))