Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions include/opendisplay_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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");
Expand Down
8 changes: 8 additions & 0 deletions src/config_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions src/display_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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)");
}
Expand Down Expand Up @@ -1863,6 +1878,7 @@ float readChipTemperature() {

void updatemsdata(){
// od_log_debug("updatemsdata() called (mloopcounter: %u)", mloopcounter);
pollAht20SensorsForMsd();
pollSht40SensorsForMsd();
pollBq27220ForMsd();
float batteryVoltage = readBatteryVoltage();
Expand Down
53 changes: 53 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <HardwareSerial.h>
#ifndef OPENDISPLAY_LOG_UART_RX
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions src/sensor_aht20.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#include "sensor_aht20.h"
#include "sensor_sht40.h"
#include "display_service.h"
#include "structs.h"
#include "od_log.h"

#include <Arduino.h>
#include <Wire.h>

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;
}
}
6 changes: 6 additions & 0 deletions src/sensor_aht20.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef SENSOR_AHT20_H
#define SENSOR_AHT20_H

void pollAht20SensorsForMsd(void);

#endif
8 changes: 4 additions & 4 deletions src/sensor_sht40.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions src/sensor_sht40.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#ifndef SENSOR_SHT40_H
#define SENSOR_SHT40_H

#include <stdint.h>

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
2 changes: 1 addition & 1 deletion tools/od-device-cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading