diff --git a/include/opendisplay_structs.h b/include/opendisplay_structs.h index f8b0cb2..5598084 100644 --- a/include/opendisplay_structs.h +++ b/include/opendisplay_structs.h @@ -79,6 +79,15 @@ * encryption active). Firmware sets it when encryption_enabled and master * key are non-zero; dynamic[11] is all 0xFF when no display is configured. * (Folded back from the Firmware side, PR #157 — parity restore.) + * - DeviceFlags bit5: OD_DEVICE_FLAG_NM420_INIT (apply RockBase NM-EPD-420 + * low-power init -- drive the board's unused peripheral enable/gate pins + * LOW after config load: LoRa EN, ES8311 CODEC_EN, audio PA_CTRL, AHT20 + * TEMP_CTL and battery ADC_EN; AHT20/ADC_EN pulse on only while sampled). + * Bits 5-7 were reserved, so the DeviceFlags group + * comment now reads bits 6-7 reserved. + * - SensorType 6 is nPM1300 (already used by another OpenDisplay firmware + * family); SensorType 7 is AHT20, publishing the same 3-byte + * temperature/humidity MSD value as SHT40. GPIO40 gates its supply. * - Add each payload-layout change here as it lands. On the next version bump, * move these under a new "MAJOR.MINOR (YYYY-MM-DD)" heading. * @@ -428,12 +437,13 @@ enum ManufacturerId { #define OD_COMM_MODE_OEPL (1u << 1) /* @doc "OEPL (OpenEPaperLink) transfer supported" */ #define OD_COMM_MODE_WIFI (1u << 2) /* @doc "WiFi transfer supported" */ -/* SystemConfig.device_flags @bits DeviceFlags (bits 5-7 reserved). */ +/* SystemConfig.device_flags @bits DeviceFlags (bits 6-7 reserved). */ #define OD_DEVICE_FLAG_PWR_PIN (1u << 0) /* @doc "device has external power management for the display etc." */ #define OD_DEVICE_FLAG_XIAO_INIT (1u << 1) /* @doc "apply Seeed XIAO low-power init" */ #define OD_DEVICE_FLAG_WS_PP_INIT (1u << 2) /* @doc "apply Waveshare PhotoPainter low-power init" */ #define OD_DEVICE_FLAG_PWR_LATCH (1u << 3) /* @doc "self-holding MOSFET latch on pwr_pin_2; optional long-press shutdown button on pwr_pin_3" */ #define OD_DEVICE_FLAG_PWR_LATCH_DFF (1u << 4) /* @doc "74AHC1G79 D-FF latch: pwr_pin_2=D (hold), pwr_pin_3=CP (clock); command 0x0052 releases latch" */ +#define OD_DEVICE_FLAG_NM420_INIT (1u << 5) /* @doc "apply RockBase NM-EPD-420 low-power init: default peripheral and ADC enable pins LOW; hold them LOW in deep sleep" */ /** @struct SystemConfig @packet 0x01 @required * @doc "Host IC identity, communication capabilities, and power-management pins. @@ -789,7 +799,9 @@ enum SensorType { OD_SENSOR_TYPE_HUMIDITY = 2, /**< @doc "generic humidity sensor" */ OD_SENSOR_TYPE_AXP2101 = 3, /**< @doc "AXP2101 power-management IC" */ OD_SENSOR_TYPE_SHT40 = 4, /**< @doc "Sensirion SHT40 (I2C); writes 3 LE bytes into MSD dynamic data (21 bits: 0.1C, 0.1% RH)" */ - OD_SENSOR_TYPE_BQ27220 = 5 /**< @doc "TI BQ27220 fuel gauge (I2C 0x55); voltage in MSD bytes 14-15, one packed dynamic byte (bits 0-6 SOC%, bit7 charging)" */ + OD_SENSOR_TYPE_BQ27220 = 5, /**< @doc "TI BQ27220 fuel gauge (I2C 0x55); voltage in MSD bytes 14-15, one packed dynamic byte (bits 0-6 SOC%, bit7 charging)" */ + OD_SENSOR_TYPE_NPM1300 = 6, /**< @doc "nPM1300 PMIC (other firmware family; not implemented by this firmware)" */ + OD_SENSOR_TYPE_AHT20 = 7 /**< @doc "AHT20 (I2C 0x38); wake-time sample in SHT40-compatible 3-byte MSD format. NM-EPD-420 power gate GPIO40 is off between samples." */ }; /** @struct SensorData @packet 0x23 @repeatable max=4 @@ -799,8 +811,8 @@ struct SensorData { uint8_t instance_number; /**< @doc "0-based sensor-block index." */ uint16_t sensor_type; /**< @enum SensorType @endian le @doc "sensor type." */ uint8_t bus_id; /**< @doc "data_bus instance this sensor lives on." */ - uint8_t i2c_addr_7bit; /**< @doc "I2C 7-bit address; 0 or 0xFF = per-sensor default (SHT40 0x44, BQ27220 0x55)." @default 0xFF */ - uint8_t msd_data_start_byte; /**< @doc "first dynamicreturndata index this sensor publishes into. SHT40 uses 3 bytes (0/0xFF = default 7); BQ27220 uses 1 packed byte (0xFF = do not publish)." @default 0xFF */ + uint8_t i2c_addr_7bit; /**< @doc "I2C 7-bit address; 0 or 0xFF = per-sensor default (SHT40 0x44, BQ27220 0x55, AHT20 0x38)." @default 0xFF */ + uint8_t msd_data_start_byte; /**< @doc "first dynamicreturndata index this sensor publishes into. SHT40/AHT20 use 3 bytes (0/0xFF = default 7); BQ27220 uses 1 packed byte (0xFF = do not publish)." @default 0xFF */ uint8_t reserved[24]; /**< @reserved @doc "future use; must be 0." */ } __attribute__((packed)); OD_STATIC_ASSERT(sizeof(struct SensorData) == 30, "SensorData wire size"); diff --git a/src/config_parser.cpp b/src/config_parser.cpp index 5e0bfde..a587609 100644 --- a/src/config_parser.cpp +++ b/src/config_parser.cpp @@ -30,6 +30,7 @@ using namespace Adafruit_LittleFS_Namespace; #define DEVICE_FLAG_WS_PP_INIT (1 << 2) #define DEVICE_FLAG_BATTERY_LATCH (1 << 3) #define DEVICE_FLAG_PWR_LATCH_DFF (1 << 4) +#define DEVICE_FLAG_NM420_INIT (1 << 5) #endif // The parse-time dumps and printConfigSummary() are od_log_debug, which the @@ -53,6 +54,7 @@ extern bool wifiInitialized; void xiaoinit(); void powerDownExternalFlashFromConfig(void); void ws_pp_init(); +void nm420_init(); extern bool encryptionInitialized; // Defined in main.h (the single-inclusion globals header), so it needs an extern @@ -746,6 +748,7 @@ void printConfigSummary(){ od_log_debug(" WS_PP_INIT flag: %s", (globalConfig.system_config.device_flags & DEVICE_FLAG_WS_PP_INIT) ? "enabled" : "disabled"); od_log_debug(" BATTERY_LATCH flag: %s", (globalConfig.system_config.device_flags & DEVICE_FLAG_BATTERY_LATCH) ? "enabled" : "disabled"); od_log_debug(" PWR_LATCH_DFF flag: %s", (globalConfig.system_config.device_flags & DEVICE_FLAG_PWR_LATCH_DFF) ? "enabled" : "disabled"); + od_log_debug(" NM420_INIT flag: %s", (globalConfig.system_config.device_flags & DEVICE_FLAG_NM420_INIT) ? "enabled" : "disabled"); od_log_debug("Power Pin: %u", globalConfig.system_config.pwr_pin); od_log_debug("Power Pin 2: %u", globalConfig.system_config.pwr_pin_2); od_log_debug("Power Pin 3: %u", globalConfig.system_config.pwr_pin_3); @@ -950,6 +953,11 @@ void full_config_init() { ws_pp_init(); od_log_info("ws_pp_init() completed"); } + if (globalConfig.loaded && (globalConfig.system_config.device_flags & DEVICE_FLAG_NM420_INIT)) { + od_log_info("Device flag DEVICE_FLAG_NM420_INIT is set, calling nm420_init()..."); + nm420_init(); + od_log_info("nm420_init() completed"); + } // Must run after config load: latch pins/flag come from globalConfig. powerLatchBegin(); } else { diff --git a/src/display_service.cpp b/src/display_service.cpp index 734b88e..9374cd0 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -8,6 +8,7 @@ #include "od_log.h" #include "buzzer_control.h" #include "sensor_sht40.h" +#include "sensor_aht20.h" #include "sensor_bq27220.h" #include "sensor_axp2101.h" #include "communication.h" @@ -845,6 +846,17 @@ int mapEpd(int id){ case 0x004A: return EP40_SPECTRA_400x600; case 0x004B: return EP27_176x264; case 0x004C: return EP27_176x264_4GRAY; + // Values 1000+ are the M3 / EPD-nRF5 driver line's PanelIC block, which + // names the same physical panels by their controller instead of by the + // bb_epaper EP* identifier. Only the ones with a bb_epaper equivalent are + // mapped; everything else in that range falls through to UNDEFINED. + // + // 1004 "JD79668 4.2\" B/W/R/Y" is the controller-side name for the same + // 4.2" four-colour class as OD_PANEL_IC_EP42YR_400X300 (0x003A): the Good + // Display GDEM042F52 / JD79668, and the GDEY0420F51 (HX8717) it replaced. + // They share one UC81xx-family command set, so they share one init + // sequence. Configs written by the other firmware line then work here. + case OD_PANEL_IC_JD79668_420_BWRY: return EP42YR_400x300; // 1004 default: return EP_PANEL_UNDEFINED; } } @@ -1188,6 +1200,9 @@ void initSensors(){ else if(sensor->sensor_type == OD_SENSOR_TYPE_SHT40){ od_log_debug(" SHT40 (I2C + MSD slot)"); } + else if(sensor->sensor_type == OD_SENSOR_TYPE_AHT20){ + od_log_debug(" AHT20 (wake-time I2C + MSD slot)"); + } else if(sensor->sensor_type == OD_SENSOR_TYPE_BQ27220){ od_log_debug(" BQ27220 fuel gauge (MSD voltage + optional dynamic SOC/status bytes)"); } @@ -1863,6 +1878,7 @@ float readChipTemperature() { void updatemsdata(){ // od_log_debug("updatemsdata() called (mloopcounter: %u)", mloopcounter); + pollAht20SensorsForMsd(); pollSht40SensorsForMsd(); pollBq27220ForMsd(); float batteryVoltage = readBatteryVoltage(); diff --git a/src/main.cpp b/src/main.cpp index 1f1b322..7b828ea 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,11 @@ #include "od_log.h" #include "watchdog.h" +#if defined(TARGET_ESP32) && defined(CONFIG_IDF_TARGET_ESP32S3) +#include "driver/gpio.h" +static constexpr uint8_t kNm420OffPins[] = {47, 44, 41, 40, 43}; +#endif + #if defined(TARGET_ESP32) && defined(OPENDISPLAY_LOG_UART) #include #ifndef OPENDISPLAY_LOG_UART_RX @@ -1228,6 +1233,7 @@ void enterDeepSleep(bool force, uint16_t overrideSleepSeconds) { od_log_flush(); // drain UART/Serial prior to deep sleep delay(100); // Brief delay to ensure serial output is sent powerLatchHoldForSleep(); + nm420HoldForSleep(); esp_deep_sleep_start(); } #endif @@ -1453,6 +1459,53 @@ void ws_pp_init(){ od_log_info("Photo Printer initialized"); } +// RockBase IoT NM-EPD-420 (ESP32-S3 + 4.2" panel), selected by +// DEVICE_FLAG_NM420_INIT. +// +// The board carries a LoRa module, an ES8311 codec, an audio PA and an AHT20 +// temperature sensor. None of them is supported by this firmware, and none of +// their enable lines can be described in a config packet -- the wire format has +// no "spare output pin" field, so the pins simply do not appear in any block. +// Untouched they keep the ESP32 reset default (input, no pull), which leaves each +// rail at whatever the board's own resistor decides; drive them to their OFF +// level explicitly instead. +// +// All four are active HIGH (HIGH = on): +// GPIO47 MODULE_EN / LoRa EN LoRa module power +// GPIO44 CODEC_EN ES8311 power +// GPIO41 PA_CTRL external audio amplifier enable +// GPIO40 TEMP_CTL AHT20 power gate +// +// GPIO43 (ADC_EN, active HIGH) defaults LOW as well; the battery reader pulses +// it HIGH only during a measurement, then LOW again while the CPU is awake. +// The configured pins (panel 1/2/4/5/6/46, I2C 38/39, USER key 45) are driven by +// their own init blocks and must not be touched here. +void nm420_init(){ +#if defined(TARGET_ESP32) && defined(CONFIG_IDF_TARGET_ESP32S3) + for (uint8_t pin : kNm420OffPins) { + gpio_set_level((gpio_num_t)pin, 0); + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); + gpio_hold_dis((gpio_num_t)pin); + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + } + od_log_info("NM-EPD-420 init: peripheral enables and battery ADC gate LOW"); +#endif +} + +#ifdef TARGET_ESP32 +void nm420HoldForSleep() { +#if defined(CONFIG_IDF_TARGET_ESP32S3) + if (!globalConfig.loaded || !(globalConfig.system_config.device_flags & DEVICE_FLAG_NM420_INIT)) return; + for (uint8_t pin : kNm420OffPins) { + digitalWrite(pin, LOW); + gpio_hold_en((gpio_num_t)pin); + } + gpio_deep_sleep_hold_en(); +#endif +} +#endif + #ifdef TARGET_NRF void powerDownExternalFlashFromConfig(void) { if (!globalConfig.loaded || globalConfig.flash_config_count == 0) { diff --git a/src/main.h b/src/main.h index 430b3c1..34b7c2a 100644 --- a/src/main.h +++ b/src/main.h @@ -56,6 +56,7 @@ using namespace Adafruit_LittleFS_Namespace; #define DEVICE_FLAG_WS_PP_INIT (1 << 2) // Bit 2: Call ws_pp_init() after config load (Waveshare Photo Printer) #define DEVICE_FLAG_BATTERY_LATCH (1 << 3) // Bit 3: Self-holding battery latch on pwr_pin_2; optional active-low long-press shutdown button on pwr_pin_3 #define DEVICE_FLAG_PWR_LATCH_DFF (1 << 4) // Bit 4: 74AHC1G79 D-FF latch; pwr_pin_2=D, pwr_pin_3=CP; release via command 0x0052 +#define DEVICE_FLAG_NM420_INIT (1 << 5) // Bit 5: Call nm420_init() after config load (RockBase NM-EPD-420 -- holds the unused peripheral enable rails LOW) #ifdef TARGET_NRF // The Bluefruit stack objects (BLEDfu / BLEService / BLECharacteristic) are now @@ -191,7 +192,9 @@ bool powerDownExternalFlash(uint8_t mosiPin, uint8_t misoPin, uint8_t sckPin, ui void powerDownExternalFlashFromConfig(void); void xiaoinit(); void ws_pp_init(); +void nm420_init(); #ifdef TARGET_ESP32 +void nm420HoldForSleep(); void fullSetupAfterConnection(); // force: sleep even with a client connected (explicit host request 0x0053). // overrideSleepSeconds: nonzero replaces deep_sleep_time_seconds for this one diff --git a/src/sensor_aht20.cpp b/src/sensor_aht20.cpp new file mode 100644 index 0000000..ff11f78 --- /dev/null +++ b/src/sensor_aht20.cpp @@ -0,0 +1,100 @@ +#include "sensor_aht20.h" +#include "sensor_sht40.h" +#include "display_service.h" +#include "structs.h" +#include "od_log.h" + +#include +#include + +extern struct GlobalConfig globalConfig; + +static uint8_t aht20Crc(const uint8_t* data, uint8_t length) { + uint8_t crc = 0xFF; + for (uint8_t index = 0; index < length; index++) { + crc ^= data[index]; + for (uint8_t bit = 0; bit < 8; bit++) { + crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1); + } + } + return crc; +} + +static bool aht20Command(uint8_t address, const uint8_t* command, uint8_t length) { + Wire.beginTransmission(address); + Wire.write(command, length); + return Wire.endTransmission() == 0; +} + +static bool aht20Read(uint8_t address, int16_t* temperatureCenti, uint16_t* humidityCenti) { + const uint8_t initialize[] = {0xBE, 0x08, 0x00}; + if (!aht20Command(address, initialize, sizeof(initialize))) return false; + delay(10); + + const uint8_t trigger[] = {0xAC, 0x33, 0x00}; + for (uint8_t attempt = 0; attempt < 2; attempt++) { + if (!aht20Command(address, trigger, sizeof(trigger))) return false; + delay(85); + if (Wire.requestFrom(address, (size_t)7, true) != 7) continue; + + uint8_t sample[7]; + for (uint8_t index = 0; index < sizeof(sample); index++) sample[index] = Wire.read(); + if ((sample[0] & 0x88) != 0x08 || aht20Crc(sample, 6) != sample[6]) continue; + + const uint32_t humidityRaw = ((uint32_t)sample[1] << 12) | + ((uint32_t)sample[2] << 4) | (sample[3] >> 4); + const uint32_t temperatureRaw = ((uint32_t)(sample[3] & 0x0F) << 16) | + ((uint32_t)sample[4] << 8) | sample[5]; + *humidityCenti = (uint16_t)(((uint64_t)humidityRaw * 10000u + 524288u) / 1048576u); + *temperatureCenti = (int16_t)(((uint64_t)temperatureRaw * 20000u + 524288u) / 1048576u - 5000); + return true; + } + return false; +} + +void pollAht20SensorsForMsd(void) { + static bool sampledThisBoot = false; + if (sampledThisBoot) return; + + for (uint8_t index = 0; index < globalConfig.sensor_count; index++) { + const SensorData* sensor = &globalConfig.sensors[index]; + if (sensor->sensor_type != OD_SENSOR_TYPE_AHT20) continue; + sampledThisBoot = true; + + uint8_t start = sensor->msd_data_start_byte; + if (start == 0 || start == 0xFF) start = 7; + if (start > 8) return; + +#if defined(TARGET_ESP32) && defined(CONFIG_IDF_TARGET_ESP32S3) + const bool gated = (globalConfig.system_config.device_flags & OD_DEVICE_FLAG_NM420_INIT) != 0; + if (gated) { + digitalWrite(40, HIGH); + delay(100); + } +#else + const bool gated = false; +#endif + int16_t temperatureCenti = 0; + uint16_t humidityCenti = 0; + uint8_t busId = sensor->bus_id == 0xFF ? 0 : sensor->bus_id; + const uint8_t address = (sensor->i2c_addr_7bit == 0 || sensor->i2c_addr_7bit == 0xFF) + ? 0x38 : sensor->i2c_addr_7bit; + bool valid = initOrRestoreWireForBus(busId) && + aht20Read(address, &temperatureCenti, &humidityCenti); + if (gated) { + if (globalConfig.sensor_count == 1 && globalConfig.touch_controller_count == 0) { + invalidateOpenDisplayWire(); + } + digitalWrite(40, LOW); + } + if (valid) { + writeTempHumidityMsd(start, temperatureCenti, humidityCenti); + od_log_info("AHT20: wake-time sample %.2f C, %.2f %%RH (bus %u, addr 0x%02X)", + temperatureCenti / 100.0f, humidityCenti / 100.0f, busId, address); + } else { + writeTempHumidityMsdInvalid(start); + od_log_warn("AHT20: wake-time sample failed (bus %u, addr 0x%02X)", busId, address); + } + return; + } +} \ No newline at end of file diff --git a/src/sensor_aht20.h b/src/sensor_aht20.h new file mode 100644 index 0000000..203e6b7 --- /dev/null +++ b/src/sensor_aht20.h @@ -0,0 +1,6 @@ +#ifndef SENSOR_AHT20_H +#define SENSOR_AHT20_H + +void pollAht20SensorsForMsd(void); + +#endif \ No newline at end of file diff --git a/src/sensor_sht40.cpp b/src/sensor_sht40.cpp index 99d068d..c6c05d8 100644 --- a/src/sensor_sht40.cpp +++ b/src/sensor_sht40.cpp @@ -172,7 +172,7 @@ static void sht40_probe_bus_once(uint8_t bus_id) { } } -static void write_sht40_invalid(uint8_t start) { +void writeTempHumidityMsdInvalid(uint8_t start) { dynamicreturndata[start] = 0xFF; dynamicreturndata[start + 1] = 0xFF; dynamicreturndata[start + 2] = 0xFF; @@ -190,7 +190,7 @@ static int round_centi_to_deci(int16_t c) { // MSD (3 bytes LE): v = rh_deci | (tu << 10); rh_deci 0..1000 = 0..100.0% RH (0.1% steps); // tu = temp(0.1°C) + 400. Decode: t_deci = (v>>10 & 0x7FF) - 400; temp_centi = t_deci*10; rh_centi = (v&0x3FF)*10 -static void write_sht40_msd(uint8_t start, int16_t temp_centi, uint16_t rh_centi) { +void writeTempHumidityMsd(uint8_t start, int16_t temp_centi, uint16_t rh_centi) { int t_deci = round_centi_to_deci(temp_centi); if (t_deci < -400) { t_deci = -400; @@ -266,10 +266,10 @@ void pollSht40SensorsForMsd(void) { err, bid, globalConfig.data_buses[bid].pin_1, globalConfig.data_buses[bid].pin_2); logged_fail = true; } - write_sht40_invalid(start); + writeTempHumidityMsdInvalid(start); continue; } logged_fail = false; - write_sht40_msd(start, tc, rhc); + writeTempHumidityMsd(start, tc, rhc); } } diff --git a/src/sensor_sht40.h b/src/sensor_sht40.h index fa78d83..3f44c40 100644 --- a/src/sensor_sht40.h +++ b/src/sensor_sht40.h @@ -1,7 +1,11 @@ #ifndef SENSOR_SHT40_H #define SENSOR_SHT40_H +#include + void initSht40Sensors(void); void pollSht40SensorsForMsd(void); +void writeTempHumidityMsdInvalid(uint8_t start); +void writeTempHumidityMsd(uint8_t start, int16_t temp_centi, uint16_t rh_centi); #endif diff --git a/tools/od-device-cli.py b/tools/od-device-cli.py index 27e5107..2af7e8a 100755 --- a/tools/od-device-cli.py +++ b/tools/od-device-cli.py @@ -538,7 +538,7 @@ class Block(NamedTuple): # sets only trigger a warning (not an error) on encode - the firmware may define values # this tool doesn't know about yet. VALID_ENUMS: dict[tuple[str, str], set[int]] = { - ("sensors", "sensor_type"): {1, 2, 3, 4, 5}, # SENSOR_TYPE_TEMPERATURE..BQ27220 + ("sensors", "sensor_type"): {1, 2, 3, 4, 5, 6, 7}, # SENSOR_TYPE_TEMPERATURE..AHT20 ("displays", "color_scheme"): {0, 1, 2, 3, 4, 5, 6, 7, 8, 100, 101, 102}, # COLOR_SCHEME_MONO..BWGBRY_SPLIT, RGB565/RGB888/RGB16BPC ("touch_controllers", "touch_ic_type"): {0, 1}, # TOUCH_IC_NONE, TOUCH_IC_GT911 ("power_option", "capacity_estimator"): {1, 2, 3, 4, 5}, # CAPACITY_EST_LI_ION..SEEED_LI_ION