Latest commit

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PhotonFrame

An indoor solar-powered e-paper photo frame with intelligent power management.

Overview

PhotonFrame is a FireBeetle ESP32-E powered 7.3" Spectra 6 (7-color) e-paper display that:

  • Fetches images from Nextcloud WebDAV
  • Monitors solar panel power via INA228 sensor
  • Adjusts refresh intervals based on available power + battery voltage
  • Reports telemetry to Home Assistant via MQTT
  • Supports OTA updates (Arduino OTA, Nextcloud, GitHub)

Hardware Requirements

ComponentModelNotes
MicrocontrollerDFRobot FireBeetle ESP32-EAny ESP32 with sufficient GPIO
DisplayGood Display GDEP073E017.3" 800x480 Spectra 6 (7-color)
Power SensorINA228 moduleAdafruit or generic, 15mΩ shunt
Solar Panel5V indoor panel~100-200mW for indoor light
Battery3.7V LiPo1000-2000mAh recommended

Wiring

Pinout Summary

FireBeetle ESP32-E Pin Assignments
==================================
I2C (INA228 Power Sensor)
GPIO 21 ─────── SDA
GPIO 22 ─────── SCL
GPIO 13 ─────── INA228 VCC (switched power)
SPI (Spectra 6 Display)
GPIO 18 ─────── SCK (Clock)
GPIO 23 ─────── MOSI (DIN)
GPIO 19 ─────── MISO (unused)
GPIO 3 ─────── CS (Chip Select)
GPIO 17 ─────── DC (Data/Command)
GPIO 16 ─────── RST (Reset)
GPIO 4 ─────── BUSY
Power Monitoring
GPIO 34 ─────── Battery voltage (built-in ADC divider)
Status
GPIO 2 ─────── Built-in LED

Wiring Diagram

 ┌─────────────────────────────────────┐
│ FireBeetle ESP32-E │
│ │
INA228 │ │ Spectra 6 Display
┌──────────┐ │ │ ┌──────────────┐
│ VCC ─────┼────────┤ GPIO 13 │ │ │
│ GND ─────┼────────┤ GND GPIO 18 ├────┤ CLK │
│ SDA ─────┼────────┤ GPIO 21 GPIO 23 ├────┤ DIN (MOSI) │
│ SCL ─────┼────────┤ GPIO 22 GPIO 3 ├────┤ CS │
│ │ │ GPIO 17 ├────┤ DC │
│ VIN+ ────┼─ Solar+│ GPIO 16 ├────┤ RST │
│ VIN- ────┼─ Solar-│ GPIO 4 ├────┤ BUSY │
└──────────┘ │ 3.3V ├────┤ VCC │
│ GND ├────┤ GND │
Solar Panel │ │ └──────────────┘
┌──────────┐ │ │
│ + ───────┼─ VIN+ on INA228 │
│ - ───────┼─ VIN- on INA228 ─── Battery charge circuit │
└──────────┘ │ │
└─────────────────────────────────────┘

INA228 Connection Details

The INA228 measures current through its shunt resistor. Connect:

  • VIN+: Solar panel positive
  • VIN-: To battery charging circuit (through shunt)
  • VCC: Connected to GPIO 13 for switchable power
  • GND: Common ground
  • SDA/SCL: I2C bus (GPIO 21/22)

I2C Address: Default 0x40 (A0=GND, A1=GND)

Software Setup

1. Install PlatformIO

Install PlatformIO IDE for VS Code.

2. Clone Project

cd~/Documents/PlatformIO/Projects
git clone https://github.com/Dreadmond/PhotonFrame.git
cd PhotonFrame

3. Configure Secrets

cp secrets.example.h secrets.h

Edit secrets.h with your credentials:

  • WiFi SSID and password
  • MQTT broker details
  • Nextcloud WebDAV URL and credentials
  • Arduino OTA password

4. Build and Upload

pio run -t upload
pio device monitor

Intelligent Refresh Algorithm

PhotonFrame uses a hybrid power-aware refresh algorithm:

BatterySolar PowerRefresh IntervalPower State
≥3.9V>100µW30 minutesabundant
≥3.9V<100µW2 hoursneutral
3.6-3.9V>100µW1 hourneutral
3.6-3.9V<100µW4 hoursconserving
3.4-3.6V>50µW4 hoursconserving
3.4-3.6V<50µW8 hourslow
<3.4Vany24 hoursemergency

Power Budget

  • Display refresh: ~0.4mAh per cycle (15-20s @ 80mA)
  • WiFi + MQTT: ~0.1mAh per wake cycle
  • Deep sleep: ~10µA
  • Indoor solar: ~50-200µW typical

OTA Updates

Arduino OTA (Development)

The device listens for Arduino OTA connections for 10 seconds after each boot.

# Upload via OTA (after initial USB flash)
pio run -t upload --upload-port photonframe.local

Credentials:

  • Hostname: photonframe.local
  • Password: Set in secrets.h (default: photonframe123)

To keep the device awake for OTA, send MQTT command:

{"action": "ota_mode"}

Device stays awake for 5 minutes with blinking LED.

Nextcloud OTA (Production)

  1. Build: pio run
  2. Upload .pio/build/firebeetle32/firmware.bin to: /Shared/firmware/photonframe/firmware.bin
  3. Device auto-updates on next boot
  4. File deleted after successful update

GitHub OTA (Fallback)

  1. Create GitHub release with tag v1.x.x
  2. Attach firmware.bin to release
  3. Device checks GitHub if no Nextcloud update

MQTT Integration

Topics

TopicDirectionDescription
photonframe/stateOutAll sensor data (JSON, retained)
photonframe/availabilityOutonline/offline (LWT)
photonframe/commandInCommands (JSON)
photonframe/statusOutStatus messages

State Payload

{
"battery_percentage": 85,
"battery_voltage": 3.92,
"solar_voltage_mV": 450.2,
"solar_current_uA": 125.3,
"solar_power_uW": 56.4,
"ina_temperature": 24.5,
"power_state": "neutral",
"next_refresh_seconds": 7200,
"firmware_version": "1.0.0",
"wifi_rssi": -52,
"free_heap": 180000,
"boot_count": 42,
"successful_updates": 38,
"error_count": 0,
"last_error": "none",
"ip_address": "192.168.1.123"
}

Commands

Send JSON to photonframe/command:

CommandDescription
{"action": "update_display"}Force image refresh on next boot
{"action": "reboot"}Restart device immediately
{"action": "check_ota"}Check Nextcloud/GitHub for updates
{"action": "ota_mode"}Stay awake 5 min for Arduino OTA
{"action": "clear_errors"}Reset error counters
{"action": "force_ha_discovery"}Republish HA discovery configs

Home Assistant Auto-Discovery

PhotonFrame registers sensors automatically via MQTT discovery:

  • Battery percentage & voltage
  • Solar voltage, current, power
  • INA228 temperature
  • Power state
  • Next refresh time
  • WiFi signal strength
  • Boot count, errors, firmware version

Sensors use expire_after: 90000 (25 hours) for deep-sleep compatibility.

Nextcloud Image Setup

Image Requirements

  • Format: PNG (8-bit RGB)
  • Resolution: 800x480 (or will be scaled to fit)
  • Max size: 500KB
  • Colors: Automatically quantized to 7-color palette

Folder Structure

Nextcloud/
├── Photos/EPaper/
│ ├── display.png # Primary image
│ └── fallback.png # Backup image
└── Shared/firmware/photonframe/
└── firmware.bin # OTA updates

Troubleshooting

Excessive Battery Drain / Negative Solar Current

If you observe:

  • Battery voltage steadily declining with no recovery
  • Solar current readings frequently negative (e.g., -50 to -150 µA)
  • Short sleep intervals despite low/no sunlight

Cause: Prior to v1.0.2, a bug in readPower() used abs(current) instead of max(0, current). This made negative (reverse) current appear as positive power, triggering shorter sleep intervals even when the solar panel wasn't actually charging the battery.

Solution: Update to firmware v1.0.2 or later. The fix ensures negative current = 0 usable power, which correctly triggers longer sleep intervals (8-24 hours) when solar isn't providing charge.

Understanding negative current: The INA228 on the high side of the solar panel measures current flowing INTO the energy harvester. Negative readings indicate reverse current flow (e.g., input capacitor discharge through the solar panel's internal resistance during low-light conditions). This is normal behavior but should not be counted as usable solar power.

INA228 Not Detected

Check serial output for:

Powering on INA228...
Initializing I2C on SDA=21, SCL=22
Scanning for INA228 at address 0x40...
Manufacturer ID read: 0x5449 <- Should be 0x5449

If ID is 0x0000 or 0xFFFF:

  1. Verify GPIO 13 is connected to INA228 VCC
  2. Check SDA (GPIO 21) and SCL (GPIO 22) connections
  3. Verify INA228 address matches (A0/A1 pins)

Display Not Updating

  1. Check SPI wiring, especially BUSY pin (GPIO 4)
  2. Verify display VCC is 3.3V (not 5V!)
  3. Check serial output for PNG decode errors
  4. Ensure image is valid PNG, under 500KB

MQTT Unavailable in Home Assistant

  1. Device uses expire_after instead of availability topic
  2. Sensors stay valid for 25 hours during deep sleep
  3. Send {"action": "force_ha_discovery"} to republish configs

WiFi Connection Issues

  1. Device creates PhotonFrame-Setup AP if WiFi fails
  2. Connect to AP, configure via captive portal
  3. Check credentials in secrets.h

Configuration Reference

secrets.h

#defineSECRET_WIFI_SSID"YourWiFi"
#defineSECRET_WIFI_PASSWORD"YourPassword"
#defineSECRET_MQTT_BROKER"192.168.1.100"
#defineSECRET_MQTT_PORT1883
#defineSECRET_MQTT_USERNAME"mqtt_user"
#defineSECRET_MQTT_PASSWORD"mqtt_pass"
#defineSECRET_MQTT_CLIENT_ID"photonframe_001"
#defineSECRET_NEXTCLOUD_URL"https://cloud.example.com/remote.php/dav/files/User/Photos/EPaper/"
#defineSECRET_NEXTCLOUD_USER"username"
#defineSECRET_NEXTCLOUD_PASS"app-password"
#defineSECRET_NEXTCLOUD_PHOTO"display.png"
#defineSECRET_NEXTCLOUD_FALLBACK"fallback.png"
#defineOTA_HOSTNAME"photonframe"
#defineOTA_PASSWORD"photonframe123"

config.h Defaults

ConstantValueDescription
INA228_I2C_ADDR0x40I2C address
INA228_SHUNT_RESISTOR0.01515mΩ shunt
VOLTAGE_CRITICAL3.4VEmergency threshold
VOLTAGE_LOW3.6VLow battery threshold
VOLTAGE_MEDIUM3.9VMedium battery threshold
POWER_ABUNDANT_UW100µW for "abundant" state
POWER_LOW_UW50µW for "low" state

Project Structure

PhotonFrame/
├── platformio.ini # Build configuration
├── secrets.h # Credentials (gitignored)
├── secrets.example.h # Template
├── README.md
├── src/
│ ├── main.cpp # Main firmware
│ ├── config.h # Pin definitions, constants
│ ├── display.h/cpp # Spectra 6 e-paper driver
│ ├── power.h/cpp # INA228 driver, sleep logic
│ └── ota.h # Nextcloud/GitHub OTA
└── docs/
└── PINOUT.md # Detailed wiring reference

Changelog

v1.0.2

  • Fixed: Power calculation bug that caused excessive battery drain
    • readPower() now uses max(0, current) instead of abs(current)
    • Negative solar current (reverse flow) correctly results in 0 power
    • Longer sleep intervals now properly trigger when solar isn't charging

v1.0.1

  • Fixed PNG display rendering
  • Fixed indoor solar power measurement for low-current scenarios

v1.0.0

  • Initial release

License

MIT License

Credits

About

Indoor solar-powered e-paper display with INA228 power monitoring and intelligent refresh scheduling

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PhotonFrame

An indoor solar-powered e-paper photo frame with intelligent power management.

Overview

PhotonFrame is a FireBeetle ESP32-E powered 7.3" Spectra 6 (7-color) e-paper display that:

  • Fetches images from Nextcloud WebDAV
  • Monitors solar panel power via INA228 sensor
  • Adjusts refresh intervals based on available power + battery voltage
  • Reports telemetry to Home Assistant via MQTT
  • Supports OTA updates (Arduino OTA, Nextcloud, GitHub)

Hardware Requirements

ComponentModelNotes
MicrocontrollerDFRobot FireBeetle ESP32-EAny ESP32 with sufficient GPIO
DisplayGood Display GDEP073E017.3" 800x480 Spectra 6 (7-color)
Power SensorINA228 moduleAdafruit or generic, 15mΩ shunt
Solar Panel5V indoor panel~100-200mW for indoor light
Battery3.7V LiPo1000-2000mAh recommended

Wiring

Pinout Summary

FireBeetle ESP32-E Pin Assignments
==================================
I2C (INA228 Power Sensor)
GPIO 21 ─────── SDA
GPIO 22 ─────── SCL
GPIO 13 ─────── INA228 VCC (switched power)
SPI (Spectra 6 Display)
GPIO 18 ─────── SCK (Clock)
GPIO 23 ─────── MOSI (DIN)
GPIO 19 ─────── MISO (unused)
GPIO 3 ─────── CS (Chip Select)
GPIO 17 ─────── DC (Data/Command)
GPIO 16 ─────── RST (Reset)
GPIO 4 ─────── BUSY
Power Monitoring
GPIO 34 ─────── Battery voltage (built-in ADC divider)
Status
GPIO 2 ─────── Built-in LED

Wiring Diagram

 ┌─────────────────────────────────────┐
│ FireBeetle ESP32-E │
│ │
INA228 │ │ Spectra 6 Display
┌──────────┐ │ │ ┌──────────────┐
│ VCC ─────┼────────┤ GPIO 13 │ │ │
│ GND ─────┼────────┤ GND GPIO 18 ├────┤ CLK │
│ SDA ─────┼────────┤ GPIO 21 GPIO 23 ├────┤ DIN (MOSI) │
│ SCL ─────┼────────┤ GPIO 22 GPIO 3 ├────┤ CS │
│ │ │ GPIO 17 ├────┤ DC │
│ VIN+ ────┼─ Solar+│ GPIO 16 ├────┤ RST │
│ VIN- ────┼─ Solar-│ GPIO 4 ├────┤ BUSY │
└──────────┘ │ 3.3V ├────┤ VCC │
│ GND ├────┤ GND │
Solar Panel │ │ └──────────────┘
┌──────────┐ │ │
│ + ───────┼─ VIN+ on INA228 │
│ - ───────┼─ VIN- on INA228 ─── Battery charge circuit │
└──────────┘ │ │
└─────────────────────────────────────┘

INA228 Connection Details

The INA228 measures current through its shunt resistor. Connect:

  • VIN+: Solar panel positive
  • VIN-: To battery charging circuit (through shunt)
  • VCC: Connected to GPIO 13 for switchable power
  • GND: Common ground
  • SDA/SCL: I2C bus (GPIO 21/22)

I2C Address: Default 0x40 (A0=GND, A1=GND)

Software Setup

1. Install PlatformIO

Install PlatformIO IDE for VS Code.

2. Clone Project

cd~/Documents/PlatformIO/Projects
git clone https://github.com/Dreadmond/PhotonFrame.git
cd PhotonFrame

3. Configure Secrets

cp secrets.example.h secrets.h

Edit secrets.h with your credentials:

  • WiFi SSID and password
  • MQTT broker details
  • Nextcloud WebDAV URL and credentials
  • Arduino OTA password

4. Build and Upload

pio run -t upload
pio device monitor

Intelligent Refresh Algorithm

PhotonFrame uses a hybrid power-aware refresh algorithm:

BatterySolar PowerRefresh IntervalPower State
≥3.9V>100µW30 minutesabundant
≥3.9V<100µW2 hoursneutral
3.6-3.9V>100µW1 hourneutral
3.6-3.9V<100µW4 hoursconserving
3.4-3.6V>50µW4 hoursconserving
3.4-3.6V<50µW8 hourslow
<3.4Vany24 hoursemergency

Power Budget

  • Display refresh: ~0.4mAh per cycle (15-20s @ 80mA)
  • WiFi + MQTT: ~0.1mAh per wake cycle
  • Deep sleep: ~10µA
  • Indoor solar: ~50-200µW typical

OTA Updates

Arduino OTA (Development)

The device listens for Arduino OTA connections for 10 seconds after each boot.

# Upload via OTA (after initial USB flash)
pio run -t upload --upload-port photonframe.local

Credentials:

  • Hostname: photonframe.local
  • Password: Set in secrets.h (default: photonframe123)

To keep the device awake for OTA, send MQTT command:

{"action": "ota_mode"}

Device stays awake for 5 minutes with blinking LED.

Nextcloud OTA (Production)

  1. Build: pio run
  2. Upload .pio/build/firebeetle32/firmware.bin to: /Shared/firmware/photonframe/firmware.bin
  3. Device auto-updates on next boot
  4. File deleted after successful update

GitHub OTA (Fallback)

  1. Create GitHub release with tag v1.x.x
  2. Attach firmware.bin to release
  3. Device checks GitHub if no Nextcloud update

MQTT Integration

Topics

TopicDirectionDescription
photonframe/stateOutAll sensor data (JSON, retained)
photonframe/availabilityOutonline/offline (LWT)
photonframe/commandInCommands (JSON)
photonframe/statusOutStatus messages

State Payload

{
"battery_percentage": 85,
"battery_voltage": 3.92,
"solar_voltage_mV": 450.2,
"solar_current_uA": 125.3,
"solar_power_uW": 56.4,
"ina_temperature": 24.5,
"power_state": "neutral",
"next_refresh_seconds": 7200,
"firmware_version": "1.0.0",
"wifi_rssi": -52,
"free_heap": 180000,
"boot_count": 42,
"successful_updates": 38,
"error_count": 0,
"last_error": "none",
"ip_address": "192.168.1.123"
}

Commands

Send JSON to photonframe/command:

CommandDescription
{"action": "update_display"}Force image refresh on next boot
{"action": "reboot"}Restart device immediately
{"action": "check_ota"}Check Nextcloud/GitHub for updates
{"action": "ota_mode"}Stay awake 5 min for Arduino OTA
{"action": "clear_errors"}Reset error counters
{"action": "force_ha_discovery"}Republish HA discovery configs

Home Assistant Auto-Discovery

PhotonFrame registers sensors automatically via MQTT discovery:

  • Battery percentage & voltage
  • Solar voltage, current, power
  • INA228 temperature
  • Power state
  • Next refresh time
  • WiFi signal strength
  • Boot count, errors, firmware version

Sensors use expire_after: 90000 (25 hours) for deep-sleep compatibility.

Nextcloud Image Setup

Image Requirements

  • Format: PNG (8-bit RGB)
  • Resolution: 800x480 (or will be scaled to fit)
  • Max size: 500KB
  • Colors: Automatically quantized to 7-color palette

Folder Structure

Nextcloud/
├── Photos/EPaper/
│ ├── display.png # Primary image
│ └── fallback.png # Backup image
└── Shared/firmware/photonframe/
└── firmware.bin # OTA updates

Troubleshooting

Excessive Battery Drain / Negative Solar Current

If you observe:

  • Battery voltage steadily declining with no recovery
  • Solar current readings frequently negative (e.g., -50 to -150 µA)
  • Short sleep intervals despite low/no sunlight

Cause: Prior to v1.0.2, a bug in readPower() used abs(current) instead of max(0, current). This made negative (reverse) current appear as positive power, triggering shorter sleep intervals even when the solar panel wasn't actually charging the battery.

Solution: Update to firmware v1.0.2 or later. The fix ensures negative current = 0 usable power, which correctly triggers longer sleep intervals (8-24 hours) when solar isn't providing charge.

Understanding negative current: The INA228 on the high side of the solar panel measures current flowing INTO the energy harvester. Negative readings indicate reverse current flow (e.g., input capacitor discharge through the solar panel's internal resistance during low-light conditions). This is normal behavior but should not be counted as usable solar power.

INA228 Not Detected

Check serial output for:

Powering on INA228...
Initializing I2C on SDA=21, SCL=22
Scanning for INA228 at address 0x40...
Manufacturer ID read: 0x5449 <- Should be 0x5449

If ID is 0x0000 or 0xFFFF:

  1. Verify GPIO 13 is connected to INA228 VCC
  2. Check SDA (GPIO 21) and SCL (GPIO 22) connections
  3. Verify INA228 address matches (A0/A1 pins)

Display Not Updating

  1. Check SPI wiring, especially BUSY pin (GPIO 4)
  2. Verify display VCC is 3.3V (not 5V!)
  3. Check serial output for PNG decode errors
  4. Ensure image is valid PNG, under 500KB

MQTT Unavailable in Home Assistant

  1. Device uses expire_after instead of availability topic
  2. Sensors stay valid for 25 hours during deep sleep
  3. Send {"action": "force_ha_discovery"} to republish configs

WiFi Connection Issues

  1. Device creates PhotonFrame-Setup AP if WiFi fails
  2. Connect to AP, configure via captive portal
  3. Check credentials in secrets.h

Configuration Reference

secrets.h

#defineSECRET_WIFI_SSID"YourWiFi"
#defineSECRET_WIFI_PASSWORD"YourPassword"
#defineSECRET_MQTT_BROKER"192.168.1.100"
#defineSECRET_MQTT_PORT1883
#defineSECRET_MQTT_USERNAME"mqtt_user"
#defineSECRET_MQTT_PASSWORD"mqtt_pass"
#defineSECRET_MQTT_CLIENT_ID"photonframe_001"
#defineSECRET_NEXTCLOUD_URL"https://cloud.example.com/remote.php/dav/files/User/Photos/EPaper/"
#defineSECRET_NEXTCLOUD_USER"username"
#defineSECRET_NEXTCLOUD_PASS"app-password"
#defineSECRET_NEXTCLOUD_PHOTO"display.png"
#defineSECRET_NEXTCLOUD_FALLBACK"fallback.png"
#defineOTA_HOSTNAME"photonframe"
#defineOTA_PASSWORD"photonframe123"

config.h Defaults

ConstantValueDescription
INA228_I2C_ADDR0x40I2C address
INA228_SHUNT_RESISTOR0.01515mΩ shunt
VOLTAGE_CRITICAL3.4VEmergency threshold
VOLTAGE_LOW3.6VLow battery threshold
VOLTAGE_MEDIUM3.9VMedium battery threshold
POWER_ABUNDANT_UW100µW for "abundant" state
POWER_LOW_UW50µW for "low" state

Project Structure

PhotonFrame/
├── platformio.ini # Build configuration
├── secrets.h # Credentials (gitignored)
├── secrets.example.h # Template
├── README.md
├── src/
│ ├── main.cpp # Main firmware
│ ├── config.h # Pin definitions, constants
│ ├── display.h/cpp # Spectra 6 e-paper driver
│ ├── power.h/cpp # INA228 driver, sleep logic
│ └── ota.h # Nextcloud/GitHub OTA
└── docs/
└── PINOUT.md # Detailed wiring reference

Changelog

v1.0.2

  • Fixed: Power calculation bug that caused excessive battery drain
    • readPower() now uses max(0, current) instead of abs(current)
    • Negative solar current (reverse flow) correctly results in 0 power
    • Longer sleep intervals now properly trigger when solar isn't charging

v1.0.1

  • Fixed PNG display rendering
  • Fixed indoor solar power measurement for low-current scenarios

v1.0.0

  • Initial release

License

MIT License

Credits

About

Indoor solar-powered e-paper display with INA228 power monitoring and intelligent refresh scheduling

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PhotonFrame

An indoor solar-powered e-paper photo frame with intelligent power management.

Overview

PhotonFrame is a FireBeetle ESP32-E powered 7.3" Spectra 6 (7-color) e-paper display that:

  • Fetches images from Nextcloud WebDAV
  • Monitors solar panel power via INA228 sensor
  • Adjusts refresh intervals based on available power + battery voltage
  • Reports telemetry to Home Assistant via MQTT
  • Supports OTA updates (Arduino OTA, Nextcloud, GitHub)

Hardware Requirements

ComponentModelNotes
MicrocontrollerDFRobot FireBeetle ESP32-EAny ESP32 with sufficient GPIO
DisplayGood Display GDEP073E017.3" 800x480 Spectra 6 (7-color)
Power SensorINA228 moduleAdafruit or generic, 15mΩ shunt
Solar Panel5V indoor panel~100-200mW for indoor light
Battery3.7V LiPo1000-2000mAh recommended

Wiring

Pinout Summary

FireBeetle ESP32-E Pin Assignments
==================================
I2C (INA228 Power Sensor)
GPIO 21 ─────── SDA
GPIO 22 ─────── SCL
GPIO 13 ─────── INA228 VCC (switched power)
SPI (Spectra 6 Display)
GPIO 18 ─────── SCK (Clock)
GPIO 23 ─────── MOSI (DIN)
GPIO 19 ─────── MISO (unused)
GPIO 3 ─────── CS (Chip Select)
GPIO 17 ─────── DC (Data/Command)
GPIO 16 ─────── RST (Reset)
GPIO 4 ─────── BUSY
Power Monitoring
GPIO 34 ─────── Battery voltage (built-in ADC divider)
Status
GPIO 2 ─────── Built-in LED

Wiring Diagram

 ┌─────────────────────────────────────┐
│ FireBeetle ESP32-E │
│ │
INA228 │ │ Spectra 6 Display
┌──────────┐ │ │ ┌──────────────┐
│ VCC ─────┼────────┤ GPIO 13 │ │ │
│ GND ─────┼────────┤ GND GPIO 18 ├────┤ CLK │
│ SDA ─────┼────────┤ GPIO 21 GPIO 23 ├────┤ DIN (MOSI) │
│ SCL ─────┼────────┤ GPIO 22 GPIO 3 ├────┤ CS │
│ │ │ GPIO 17 ├────┤ DC │
│ VIN+ ────┼─ Solar+│ GPIO 16 ├────┤ RST │
│ VIN- ────┼─ Solar-│ GPIO 4 ├────┤ BUSY │
└──────────┘ │ 3.3V ├────┤ VCC │
│ GND ├────┤ GND │
Solar Panel │ │ └──────────────┘
┌──────────┐ │ │
│ + ───────┼─ VIN+ on INA228 │
│ - ───────┼─ VIN- on INA228 ─── Battery charge circuit │
└──────────┘ │ │
└─────────────────────────────────────┘

INA228 Connection Details

The INA228 measures current through its shunt resistor. Connect:

  • VIN+: Solar panel positive
  • VIN-: To battery charging circuit (through shunt)
  • VCC: Connected to GPIO 13 for switchable power
  • GND: Common ground
  • SDA/SCL: I2C bus (GPIO 21/22)

I2C Address: Default 0x40 (A0=GND, A1=GND)

Software Setup

1. Install PlatformIO

Install PlatformIO IDE for VS Code.

2. Clone Project

cd~/Documents/PlatformIO/Projects
git clone https://github.com/Dreadmond/PhotonFrame.git
cd PhotonFrame

3. Configure Secrets

cp secrets.example.h secrets.h

Edit secrets.h with your credentials:

  • WiFi SSID and password
  • MQTT broker details
  • Nextcloud WebDAV URL and credentials
  • Arduino OTA password

4. Build and Upload

pio run -t upload
pio device monitor

Intelligent Refresh Algorithm

PhotonFrame uses a hybrid power-aware refresh algorithm:

BatterySolar PowerRefresh IntervalPower State
≥3.9V>100µW30 minutesabundant
≥3.9V<100µW2 hoursneutral
3.6-3.9V>100µW1 hourneutral
3.6-3.9V<100µW4 hoursconserving
3.4-3.6V>50µW4 hoursconserving
3.4-3.6V<50µW8 hourslow
<3.4Vany24 hoursemergency

Power Budget

  • Display refresh: ~0.4mAh per cycle (15-20s @ 80mA)
  • WiFi + MQTT: ~0.1mAh per wake cycle
  • Deep sleep: ~10µA
  • Indoor solar: ~50-200µW typical

OTA Updates

Arduino OTA (Development)

The device listens for Arduino OTA connections for 10 seconds after each boot.

# Upload via OTA (after initial USB flash)
pio run -t upload --upload-port photonframe.local

Credentials:

  • Hostname: photonframe.local
  • Password: Set in secrets.h (default: photonframe123)

To keep the device awake for OTA, send MQTT command:

{"action": "ota_mode"}

Device stays awake for 5 minutes with blinking LED.

Nextcloud OTA (Production)

  1. Build: pio run
  2. Upload .pio/build/firebeetle32/firmware.bin to: /Shared/firmware/photonframe/firmware.bin
  3. Device auto-updates on next boot
  4. File deleted after successful update

GitHub OTA (Fallback)

  1. Create GitHub release with tag v1.x.x
  2. Attach firmware.bin to release
  3. Device checks GitHub if no Nextcloud update

MQTT Integration

Topics

TopicDirectionDescription
photonframe/stateOutAll sensor data (JSON, retained)
photonframe/availabilityOutonline/offline (LWT)
photonframe/commandInCommands (JSON)
photonframe/statusOutStatus messages

State Payload

{
"battery_percentage": 85,
"battery_voltage": 3.92,
"solar_voltage_mV": 450.2,
"solar_current_uA": 125.3,
"solar_power_uW": 56.4,
"ina_temperature": 24.5,
"power_state": "neutral",
"next_refresh_seconds": 7200,
"firmware_version": "1.0.0",
"wifi_rssi": -52,
"free_heap": 180000,
"boot_count": 42,
"successful_updates": 38,
"error_count": 0,
"last_error": "none",
"ip_address": "192.168.1.123"
}

Commands

Send JSON to photonframe/command:

CommandDescription
{"action": "update_display"}Force image refresh on next boot
{"action": "reboot"}Restart device immediately
{"action": "check_ota"}Check Nextcloud/GitHub for updates
{"action": "ota_mode"}Stay awake 5 min for Arduino OTA
{"action": "clear_errors"}Reset error counters
{"action": "force_ha_discovery"}Republish HA discovery configs

Home Assistant Auto-Discovery

PhotonFrame registers sensors automatically via MQTT discovery:

  • Battery percentage & voltage
  • Solar voltage, current, power
  • INA228 temperature
  • Power state
  • Next refresh time
  • WiFi signal strength
  • Boot count, errors, firmware version

Sensors use expire_after: 90000 (25 hours) for deep-sleep compatibility.

Nextcloud Image Setup

Image Requirements

  • Format: PNG (8-bit RGB)
  • Resolution: 800x480 (or will be scaled to fit)
  • Max size: 500KB
  • Colors: Automatically quantized to 7-color palette

Folder Structure

Nextcloud/
├── Photos/EPaper/
│ ├── display.png # Primary image
│ └── fallback.png # Backup image
└── Shared/firmware/photonframe/
└── firmware.bin # OTA updates

Troubleshooting

Excessive Battery Drain / Negative Solar Current

If you observe:

  • Battery voltage steadily declining with no recovery
  • Solar current readings frequently negative (e.g., -50 to -150 µA)
  • Short sleep intervals despite low/no sunlight

Cause: Prior to v1.0.2, a bug in readPower() used abs(current) instead of max(0, current). This made negative (reverse) current appear as positive power, triggering shorter sleep intervals even when the solar panel wasn't actually charging the battery.

Solution: Update to firmware v1.0.2 or later. The fix ensures negative current = 0 usable power, which correctly triggers longer sleep intervals (8-24 hours) when solar isn't providing charge.

Understanding negative current: The INA228 on the high side of the solar panel measures current flowing INTO the energy harvester. Negative readings indicate reverse current flow (e.g., input capacitor discharge through the solar panel's internal resistance during low-light conditions). This is normal behavior but should not be counted as usable solar power.

INA228 Not Detected

Check serial output for:

Powering on INA228...
Initializing I2C on SDA=21, SCL=22
Scanning for INA228 at address 0x40...
Manufacturer ID read: 0x5449 <- Should be 0x5449

If ID is 0x0000 or 0xFFFF:

  1. Verify GPIO 13 is connected to INA228 VCC
  2. Check SDA (GPIO 21) and SCL (GPIO 22) connections
  3. Verify INA228 address matches (A0/A1 pins)

Display Not Updating

  1. Check SPI wiring, especially BUSY pin (GPIO 4)
  2. Verify display VCC is 3.3V (not 5V!)
  3. Check serial output for PNG decode errors
  4. Ensure image is valid PNG, under 500KB

MQTT Unavailable in Home Assistant

  1. Device uses expire_after instead of availability topic
  2. Sensors stay valid for 25 hours during deep sleep
  3. Send {"action": "force_ha_discovery"} to republish configs

WiFi Connection Issues

  1. Device creates PhotonFrame-Setup AP if WiFi fails
  2. Connect to AP, configure via captive portal
  3. Check credentials in secrets.h

Configuration Reference

secrets.h

#defineSECRET_WIFI_SSID"YourWiFi"
#defineSECRET_WIFI_PASSWORD"YourPassword"
#defineSECRET_MQTT_BROKER"192.168.1.100"
#defineSECRET_MQTT_PORT1883
#defineSECRET_MQTT_USERNAME"mqtt_user"
#defineSECRET_MQTT_PASSWORD"mqtt_pass"
#defineSECRET_MQTT_CLIENT_ID"photonframe_001"
#defineSECRET_NEXTCLOUD_URL"https://cloud.example.com/remote.php/dav/files/User/Photos/EPaper/"
#defineSECRET_NEXTCLOUD_USER"username"
#defineSECRET_NEXTCLOUD_PASS"app-password"
#defineSECRET_NEXTCLOUD_PHOTO"display.png"
#defineSECRET_NEXTCLOUD_FALLBACK"fallback.png"
#defineOTA_HOSTNAME"photonframe"
#defineOTA_PASSWORD"photonframe123"

config.h Defaults

ConstantValueDescription
INA228_I2C_ADDR0x40I2C address
INA228_SHUNT_RESISTOR0.01515mΩ shunt
VOLTAGE_CRITICAL3.4VEmergency threshold
VOLTAGE_LOW3.6VLow battery threshold
VOLTAGE_MEDIUM3.9VMedium battery threshold
POWER_ABUNDANT_UW100µW for "abundant" state
POWER_LOW_UW50µW for "low" state

Project Structure

PhotonFrame/
├── platformio.ini # Build configuration
├── secrets.h # Credentials (gitignored)
├── secrets.example.h # Template
├── README.md
├── src/
│ ├── main.cpp # Main firmware
│ ├── config.h # Pin definitions, constants
│ ├── display.h/cpp # Spectra 6 e-paper driver
│ ├── power.h/cpp # INA228 driver, sleep logic
│ └── ota.h # Nextcloud/GitHub OTA
└── docs/
└── PINOUT.md # Detailed wiring reference

Changelog

v1.0.2

  • Fixed: Power calculation bug that caused excessive battery drain
    • readPower() now uses max(0, current) instead of abs(current)
    • Negative solar current (reverse flow) correctly results in 0 power
    • Longer sleep intervals now properly trigger when solar isn't charging

v1.0.1

  • Fixed PNG display rendering
  • Fixed indoor solar power measurement for low-current scenarios

v1.0.0

  • Initial release

License

MIT License

Credits

About

Indoor solar-powered e-paper display with INA228 power monitoring and intelligent refresh scheduling

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PhotonFrame

An indoor solar-powered e-paper photo frame with intelligent power management.

Overview

PhotonFrame is a FireBeetle ESP32-E powered 7.3" Spectra 6 (7-color) e-paper display that:

  • Fetches images from Nextcloud WebDAV
  • Monitors solar panel power via INA228 sensor
  • Adjusts refresh intervals based on available power + battery voltage
  • Reports telemetry to Home Assistant via MQTT
  • Supports OTA updates (Arduino OTA, Nextcloud, GitHub)

Hardware Requirements

ComponentModelNotes
MicrocontrollerDFRobot FireBeetle ESP32-EAny ESP32 with sufficient GPIO
DisplayGood Display GDEP073E017.3" 800x480 Spectra 6 (7-color)
Power SensorINA228 moduleAdafruit or generic, 15mΩ shunt
Solar Panel5V indoor panel~100-200mW for indoor light
Battery3.7V LiPo1000-2000mAh recommended

Wiring

Pinout Summary

FireBeetle ESP32-E Pin Assignments
==================================
I2C (INA228 Power Sensor)
GPIO 21 ─────── SDA
GPIO 22 ─────── SCL
GPIO 13 ─────── INA228 VCC (switched power)
SPI (Spectra 6 Display)
GPIO 18 ─────── SCK (Clock)
GPIO 23 ─────── MOSI (DIN)
GPIO 19 ─────── MISO (unused)
GPIO 3 ─────── CS (Chip Select)
GPIO 17 ─────── DC (Data/Command)
GPIO 16 ─────── RST (Reset)
GPIO 4 ─────── BUSY
Power Monitoring
GPIO 34 ─────── Battery voltage (built-in ADC divider)
Status
GPIO 2 ─────── Built-in LED

Wiring Diagram

 ┌─────────────────────────────────────┐
│ FireBeetle ESP32-E │
│ │
INA228 │ │ Spectra 6 Display
┌──────────┐ │ │ ┌──────────────┐
│ VCC ─────┼────────┤ GPIO 13 │ │ │
│ GND ─────┼────────┤ GND GPIO 18 ├────┤ CLK │
│ SDA ─────┼────────┤ GPIO 21 GPIO 23 ├────┤ DIN (MOSI) │
│ SCL ─────┼────────┤ GPIO 22 GPIO 3 ├────┤ CS │
│ │ │ GPIO 17 ├────┤ DC │
│ VIN+ ────┼─ Solar+│ GPIO 16 ├────┤ RST │
│ VIN- ────┼─ Solar-│ GPIO 4 ├────┤ BUSY │
└──────────┘ │ 3.3V ├────┤ VCC │
│ GND ├────┤ GND │
Solar Panel │ │ └──────────────┘
┌──────────┐ │ │
│ + ───────┼─ VIN+ on INA228 │
│ - ───────┼─ VIN- on INA228 ─── Battery charge circuit │
└──────────┘ │ │
└─────────────────────────────────────┘

INA228 Connection Details

The INA228 measures current through its shunt resistor. Connect:

  • VIN+: Solar panel positive
  • VIN-: To battery charging circuit (through shunt)
  • VCC: Connected to GPIO 13 for switchable power
  • GND: Common ground
  • SDA/SCL: I2C bus (GPIO 21/22)

I2C Address: Default 0x40 (A0=GND, A1=GND)

Software Setup

1. Install PlatformIO

Install PlatformIO IDE for VS Code.

2. Clone Project

cd~/Documents/PlatformIO/Projects
git clone https://github.com/Dreadmond/PhotonFrame.git
cd PhotonFrame

3. Configure Secrets

cp secrets.example.h secrets.h

Edit secrets.h with your credentials:

  • WiFi SSID and password
  • MQTT broker details
  • Nextcloud WebDAV URL and credentials
  • Arduino OTA password

4. Build and Upload

pio run -t upload
pio device monitor

Intelligent Refresh Algorithm

PhotonFrame uses a hybrid power-aware refresh algorithm:

BatterySolar PowerRefresh IntervalPower State
≥3.9V>100µW30 minutesabundant
≥3.9V<100µW2 hoursneutral
3.6-3.9V>100µW1 hourneutral
3.6-3.9V<100µW4 hoursconserving
3.4-3.6V>50µW4 hoursconserving
3.4-3.6V<50µW8 hourslow
<3.4Vany24 hoursemergency

Power Budget

  • Display refresh: ~0.4mAh per cycle (15-20s @ 80mA)
  • WiFi + MQTT: ~0.1mAh per wake cycle
  • Deep sleep: ~10µA
  • Indoor solar: ~50-200µW typical

OTA Updates

Arduino OTA (Development)

The device listens for Arduino OTA connections for 10 seconds after each boot.

# Upload via OTA (after initial USB flash)
pio run -t upload --upload-port photonframe.local

Credentials:

  • Hostname: photonframe.local
  • Password: Set in secrets.h (default: photonframe123)

To keep the device awake for OTA, send MQTT command:

{"action": "ota_mode"}

Device stays awake for 5 minutes with blinking LED.

Nextcloud OTA (Production)

  1. Build: pio run
  2. Upload .pio/build/firebeetle32/firmware.bin to: /Shared/firmware/photonframe/firmware.bin
  3. Device auto-updates on next boot
  4. File deleted after successful update

GitHub OTA (Fallback)

  1. Create GitHub release with tag v1.x.x
  2. Attach firmware.bin to release
  3. Device checks GitHub if no Nextcloud update

MQTT Integration

Topics

TopicDirectionDescription
photonframe/stateOutAll sensor data (JSON, retained)
photonframe/availabilityOutonline/offline (LWT)
photonframe/commandInCommands (JSON)
photonframe/statusOutStatus messages

State Payload

{
"battery_percentage": 85,
"battery_voltage": 3.92,
"solar_voltage_mV": 450.2,
"solar_current_uA": 125.3,
"solar_power_uW": 56.4,
"ina_temperature": 24.5,
"power_state": "neutral",
"next_refresh_seconds": 7200,
"firmware_version": "1.0.0",
"wifi_rssi": -52,
"free_heap": 180000,
"boot_count": 42,
"successful_updates": 38,
"error_count": 0,
"last_error": "none",
"ip_address": "192.168.1.123"
}

Commands

Send JSON to photonframe/command:

CommandDescription
{"action": "update_display"}Force image refresh on next boot
{"action": "reboot"}Restart device immediately
{"action": "check_ota"}Check Nextcloud/GitHub for updates
{"action": "ota_mode"}Stay awake 5 min for Arduino OTA
{"action": "clear_errors"}Reset error counters
{"action": "force_ha_discovery"}Republish HA discovery configs

Home Assistant Auto-Discovery

PhotonFrame registers sensors automatically via MQTT discovery:

  • Battery percentage & voltage
  • Solar voltage, current, power
  • INA228 temperature
  • Power state
  • Next refresh time
  • WiFi signal strength
  • Boot count, errors, firmware version

Sensors use expire_after: 90000 (25 hours) for deep-sleep compatibility.

Nextcloud Image Setup

Image Requirements

  • Format: PNG (8-bit RGB)
  • Resolution: 800x480 (or will be scaled to fit)
  • Max size: 500KB
  • Colors: Automatically quantized to 7-color palette

Folder Structure

Nextcloud/
├── Photos/EPaper/
│ ├── display.png # Primary image
│ └── fallback.png # Backup image
└── Shared/firmware/photonframe/
└── firmware.bin # OTA updates

Troubleshooting

Excessive Battery Drain / Negative Solar Current

If you observe:

  • Battery voltage steadily declining with no recovery
  • Solar current readings frequently negative (e.g., -50 to -150 µA)
  • Short sleep intervals despite low/no sunlight

Cause: Prior to v1.0.2, a bug in readPower() used abs(current) instead of max(0, current). This made negative (reverse) current appear as positive power, triggering shorter sleep intervals even when the solar panel wasn't actually charging the battery.

Solution: Update to firmware v1.0.2 or later. The fix ensures negative current = 0 usable power, which correctly triggers longer sleep intervals (8-24 hours) when solar isn't providing charge.

Understanding negative current: The INA228 on the high side of the solar panel measures current flowing INTO the energy harvester. Negative readings indicate reverse current flow (e.g., input capacitor discharge through the solar panel's internal resistance during low-light conditions). This is normal behavior but should not be counted as usable solar power.

INA228 Not Detected

Check serial output for:

Powering on INA228...
Initializing I2C on SDA=21, SCL=22
Scanning for INA228 at address 0x40...
Manufacturer ID read: 0x5449 <- Should be 0x5449

If ID is 0x0000 or 0xFFFF:

  1. Verify GPIO 13 is connected to INA228 VCC
  2. Check SDA (GPIO 21) and SCL (GPIO 22) connections
  3. Verify INA228 address matches (A0/A1 pins)

Display Not Updating

  1. Check SPI wiring, especially BUSY pin (GPIO 4)
  2. Verify display VCC is 3.3V (not 5V!)
  3. Check serial output for PNG decode errors
  4. Ensure image is valid PNG, under 500KB

MQTT Unavailable in Home Assistant

  1. Device uses expire_after instead of availability topic
  2. Sensors stay valid for 25 hours during deep sleep
  3. Send {"action": "force_ha_discovery"} to republish configs

WiFi Connection Issues

  1. Device creates PhotonFrame-Setup AP if WiFi fails
  2. Connect to AP, configure via captive portal
  3. Check credentials in secrets.h

Configuration Reference

secrets.h

#defineSECRET_WIFI_SSID"YourWiFi"
#defineSECRET_WIFI_PASSWORD"YourPassword"
#defineSECRET_MQTT_BROKER"192.168.1.100"
#defineSECRET_MQTT_PORT1883
#defineSECRET_MQTT_USERNAME"mqtt_user"
#defineSECRET_MQTT_PASSWORD"mqtt_pass"
#defineSECRET_MQTT_CLIENT_ID"photonframe_001"
#defineSECRET_NEXTCLOUD_URL"https://cloud.example.com/remote.php/dav/files/User/Photos/EPaper/"
#defineSECRET_NEXTCLOUD_USER"username"
#defineSECRET_NEXTCLOUD_PASS"app-password"
#defineSECRET_NEXTCLOUD_PHOTO"display.png"
#defineSECRET_NEXTCLOUD_FALLBACK"fallback.png"
#defineOTA_HOSTNAME"photonframe"
#defineOTA_PASSWORD"photonframe123"

config.h Defaults

ConstantValueDescription
INA228_I2C_ADDR0x40I2C address
INA228_SHUNT_RESISTOR0.01515mΩ shunt
VOLTAGE_CRITICAL3.4VEmergency threshold
VOLTAGE_LOW3.6VLow battery threshold
VOLTAGE_MEDIUM3.9VMedium battery threshold
POWER_ABUNDANT_UW100µW for "abundant" state
POWER_LOW_UW50µW for "low" state

Project Structure

PhotonFrame/
├── platformio.ini # Build configuration
├── secrets.h # Credentials (gitignored)
├── secrets.example.h # Template
├── README.md
├── src/
│ ├── main.cpp # Main firmware
│ ├── config.h # Pin definitions, constants
│ ├── display.h/cpp # Spectra 6 e-paper driver
│ ├── power.h/cpp # INA228 driver, sleep logic
│ └── ota.h # Nextcloud/GitHub OTA
└── docs/
└── PINOUT.md # Detailed wiring reference

Changelog

v1.0.2

  • Fixed: Power calculation bug that caused excessive battery drain
    • readPower() now uses max(0, current) instead of abs(current)
    • Negative solar current (reverse flow) correctly results in 0 power
    • Longer sleep intervals now properly trigger when solar isn't charging

v1.0.1

  • Fixed PNG display rendering
  • Fixed indoor solar power measurement for low-current scenarios

v1.0.0

  • Initial release

License

MIT License

Credits

About

Indoor solar-powered e-paper display with INA228 power monitoring and intelligent refresh scheduling

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PhotonFrame

An indoor solar-powered e-paper photo frame with intelligent power management.

Overview

PhotonFrame is a FireBeetle ESP32-E powered 7.3" Spectra 6 (7-color) e-paper display that:

  • Fetches images from Nextcloud WebDAV
  • Monitors solar panel power via INA228 sensor
  • Adjusts refresh intervals based on available power + battery voltage
  • Reports telemetry to Home Assistant via MQTT
  • Supports OTA updates (Arduino OTA, Nextcloud, GitHub)

Hardware Requirements

ComponentModelNotes
MicrocontrollerDFRobot FireBeetle ESP32-EAny ESP32 with sufficient GPIO
DisplayGood Display GDEP073E017.3" 800x480 Spectra 6 (7-color)
Power SensorINA228 moduleAdafruit or generic, 15mΩ shunt
Solar Panel5V indoor panel~100-200mW for indoor light
Battery3.7V LiPo1000-2000mAh recommended

Wiring

Pinout Summary

FireBeetle ESP32-E Pin Assignments
==================================
I2C (INA228 Power Sensor)
GPIO 21 ─────── SDA
GPIO 22 ─────── SCL
GPIO 13 ─────── INA228 VCC (switched power)
SPI (Spectra 6 Display)
GPIO 18 ─────── SCK (Clock)
GPIO 23 ─────── MOSI (DIN)
GPIO 19 ─────── MISO (unused)
GPIO 3 ─────── CS (Chip Select)
GPIO 17 ─────── DC (Data/Command)
GPIO 16 ─────── RST (Reset)
GPIO 4 ─────── BUSY
Power Monitoring
GPIO 34 ─────── Battery voltage (built-in ADC divider)
Status
GPIO 2 ─────── Built-in LED

Wiring Diagram

 ┌─────────────────────────────────────┐
│ FireBeetle ESP32-E │
│ │
INA228 │ │ Spectra 6 Display
┌──────────┐ │ │ ┌──────────────┐
│ VCC ─────┼────────┤ GPIO 13 │ │ │
│ GND ─────┼────────┤ GND GPIO 18 ├────┤ CLK │
│ SDA ─────┼────────┤ GPIO 21 GPIO 23 ├────┤ DIN (MOSI) │
│ SCL ─────┼────────┤ GPIO 22 GPIO 3 ├────┤ CS │
│ │ │ GPIO 17 ├────┤ DC │
│ VIN+ ────┼─ Solar+│ GPIO 16 ├────┤ RST │
│ VIN- ────┼─ Solar-│ GPIO 4 ├────┤ BUSY │
└──────────┘ │ 3.3V ├────┤ VCC │
│ GND ├────┤ GND │
Solar Panel │ │ └──────────────┘
┌──────────┐ │ │
│ + ───────┼─ VIN+ on INA228 │
│ - ───────┼─ VIN- on INA228 ─── Battery charge circuit │
└──────────┘ │ │
└─────────────────────────────────────┘

INA228 Connection Details

The INA228 measures current through its shunt resistor. Connect:

  • VIN+: Solar panel positive
  • VIN-: To battery charging circuit (through shunt)
  • VCC: Connected to GPIO 13 for switchable power
  • GND: Common ground
  • SDA/SCL: I2C bus (GPIO 21/22)

I2C Address: Default 0x40 (A0=GND, A1=GND)

Software Setup

1. Install PlatformIO

Install PlatformIO IDE for VS Code.

2. Clone Project

cd~/Documents/PlatformIO/Projects
git clone https://github.com/Dreadmond/PhotonFrame.git
cd PhotonFrame

3. Configure Secrets

cp secrets.example.h secrets.h

Edit secrets.h with your credentials:

  • WiFi SSID and password
  • MQTT broker details
  • Nextcloud WebDAV URL and credentials
  • Arduino OTA password

4. Build and Upload

pio run -t upload
pio device monitor

Intelligent Refresh Algorithm

PhotonFrame uses a hybrid power-aware refresh algorithm:

BatterySolar PowerRefresh IntervalPower State
≥3.9V>100µW30 minutesabundant
≥3.9V<100µW2 hoursneutral
3.6-3.9V>100µW1 hourneutral
3.6-3.9V<100µW4 hoursconserving
3.4-3.6V>50µW4 hoursconserving
3.4-3.6V<50µW8 hourslow
<3.4Vany24 hoursemergency

Power Budget

  • Display refresh: ~0.4mAh per cycle (15-20s @ 80mA)
  • WiFi + MQTT: ~0.1mAh per wake cycle
  • Deep sleep: ~10µA
  • Indoor solar: ~50-200µW typical

OTA Updates

Arduino OTA (Development)

The device listens for Arduino OTA connections for 10 seconds after each boot.

# Upload via OTA (after initial USB flash)
pio run -t upload --upload-port photonframe.local

Credentials:

  • Hostname: photonframe.local
  • Password: Set in secrets.h (default: photonframe123)

To keep the device awake for OTA, send MQTT command:

{"action": "ota_mode"}

Device stays awake for 5 minutes with blinking LED.

Nextcloud OTA (Production)

  1. Build: pio run
  2. Upload .pio/build/firebeetle32/firmware.bin to: /Shared/firmware/photonframe/firmware.bin
  3. Device auto-updates on next boot
  4. File deleted after successful update

GitHub OTA (Fallback)

  1. Create GitHub release with tag v1.x.x
  2. Attach firmware.bin to release
  3. Device checks GitHub if no Nextcloud update

MQTT Integration

Topics

TopicDirectionDescription
photonframe/stateOutAll sensor data (JSON, retained)
photonframe/availabilityOutonline/offline (LWT)
photonframe/commandInCommands (JSON)
photonframe/statusOutStatus messages

State Payload

{
"battery_percentage": 85,
"battery_voltage": 3.92,
"solar_voltage_mV": 450.2,
"solar_current_uA": 125.3,
"solar_power_uW": 56.4,
"ina_temperature": 24.5,
"power_state": "neutral",
"next_refresh_seconds": 7200,
"firmware_version": "1.0.0",
"wifi_rssi": -52,
"free_heap": 180000,
"boot_count": 42,
"successful_updates": 38,
"error_count": 0,
"last_error": "none",
"ip_address": "192.168.1.123"
}

Commands

Send JSON to photonframe/command:

CommandDescription
{"action": "update_display"}Force image refresh on next boot
{"action": "reboot"}Restart device immediately
{"action": "check_ota"}Check Nextcloud/GitHub for updates
{"action": "ota_mode"}Stay awake 5 min for Arduino OTA
{"action": "clear_errors"}Reset error counters
{"action": "force_ha_discovery"}Republish HA discovery configs

Home Assistant Auto-Discovery

PhotonFrame registers sensors automatically via MQTT discovery:

  • Battery percentage & voltage
  • Solar voltage, current, power
  • INA228 temperature
  • Power state
  • Next refresh time
  • WiFi signal strength
  • Boot count, errors, firmware version

Sensors use expire_after: 90000 (25 hours) for deep-sleep compatibility.

Nextcloud Image Setup

Image Requirements

  • Format: PNG (8-bit RGB)
  • Resolution: 800x480 (or will be scaled to fit)
  • Max size: 500KB
  • Colors: Automatically quantized to 7-color palette

Folder Structure

Nextcloud/
├── Photos/EPaper/
│ ├── display.png # Primary image
│ └── fallback.png # Backup image
└── Shared/firmware/photonframe/
└── firmware.bin # OTA updates

Troubleshooting

Excessive Battery Drain / Negative Solar Current

If you observe:

  • Battery voltage steadily declining with no recovery
  • Solar current readings frequently negative (e.g., -50 to -150 µA)
  • Short sleep intervals despite low/no sunlight

Cause: Prior to v1.0.2, a bug in readPower() used abs(current) instead of max(0, current). This made negative (reverse) current appear as positive power, triggering shorter sleep intervals even when the solar panel wasn't actually charging the battery.

Solution: Update to firmware v1.0.2 or later. The fix ensures negative current = 0 usable power, which correctly triggers longer sleep intervals (8-24 hours) when solar isn't providing charge.

Understanding negative current: The INA228 on the high side of the solar panel measures current flowing INTO the energy harvester. Negative readings indicate reverse current flow (e.g., input capacitor discharge through the solar panel's internal resistance during low-light conditions). This is normal behavior but should not be counted as usable solar power.

INA228 Not Detected

Check serial output for:

Powering on INA228...
Initializing I2C on SDA=21, SCL=22
Scanning for INA228 at address 0x40...
Manufacturer ID read: 0x5449 <- Should be 0x5449

If ID is 0x0000 or 0xFFFF:

  1. Verify GPIO 13 is connected to INA228 VCC
  2. Check SDA (GPIO 21) and SCL (GPIO 22) connections
  3. Verify INA228 address matches (A0/A1 pins)

Display Not Updating

  1. Check SPI wiring, especially BUSY pin (GPIO 4)
  2. Verify display VCC is 3.3V (not 5V!)
  3. Check serial output for PNG decode errors
  4. Ensure image is valid PNG, under 500KB

MQTT Unavailable in Home Assistant

  1. Device uses expire_after instead of availability topic
  2. Sensors stay valid for 25 hours during deep sleep
  3. Send {"action": "force_ha_discovery"} to republish configs

WiFi Connection Issues

  1. Device creates PhotonFrame-Setup AP if WiFi fails
  2. Connect to AP, configure via captive portal
  3. Check credentials in secrets.h

Configuration Reference

secrets.h

#defineSECRET_WIFI_SSID"YourWiFi"
#defineSECRET_WIFI_PASSWORD"YourPassword"
#defineSECRET_MQTT_BROKER"192.168.1.100"
#defineSECRET_MQTT_PORT1883
#defineSECRET_MQTT_USERNAME"mqtt_user"
#defineSECRET_MQTT_PASSWORD"mqtt_pass"
#defineSECRET_MQTT_CLIENT_ID"photonframe_001"
#defineSECRET_NEXTCLOUD_URL"https://cloud.example.com/remote.php/dav/files/User/Photos/EPaper/"
#defineSECRET_NEXTCLOUD_USER"username"
#defineSECRET_NEXTCLOUD_PASS"app-password"
#defineSECRET_NEXTCLOUD_PHOTO"display.png"
#defineSECRET_NEXTCLOUD_FALLBACK"fallback.png"
#defineOTA_HOSTNAME"photonframe"
#defineOTA_PASSWORD"photonframe123"

config.h Defaults

ConstantValueDescription
INA228_I2C_ADDR0x40I2C address
INA228_SHUNT_RESISTOR0.01515mΩ shunt
VOLTAGE_CRITICAL3.4VEmergency threshold
VOLTAGE_LOW3.6VLow battery threshold
VOLTAGE_MEDIUM3.9VMedium battery threshold
POWER_ABUNDANT_UW100µW for "abundant" state
POWER_LOW_UW50µW for "low" state

Project Structure

PhotonFrame/
├── platformio.ini # Build configuration
├── secrets.h # Credentials (gitignored)
├── secrets.example.h # Template
├── README.md
├── src/
│ ├── main.cpp # Main firmware
│ ├── config.h # Pin definitions, constants
│ ├── display.h/cpp # Spectra 6 e-paper driver
│ ├── power.h/cpp # INA228 driver, sleep logic
│ └── ota.h # Nextcloud/GitHub OTA
└── docs/
└── PINOUT.md # Detailed wiring reference

Changelog

v1.0.2

  • Fixed: Power calculation bug that caused excessive battery drain
    • readPower() now uses max(0, current) instead of abs(current)
    • Negative solar current (reverse flow) correctly results in 0 power
    • Longer sleep intervals now properly trigger when solar isn't charging

v1.0.1

  • Fixed PNG display rendering
  • Fixed indoor solar power measurement for low-current scenarios

v1.0.0

  • Initial release

License

MIT License

Credits

About

Indoor solar-powered e-paper display with INA228 power monitoring and intelligent refresh scheduling

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PhotonFrame

An indoor solar-powered e-paper photo frame with intelligent power management.

Overview

PhotonFrame is a FireBeetle ESP32-E powered 7.3" Spectra 6 (7-color) e-paper display that:

  • Fetches images from Nextcloud WebDAV
  • Monitors solar panel power via INA228 sensor
  • Adjusts refresh intervals based on available power + battery voltage
  • Reports telemetry to Home Assistant via MQTT
  • Supports OTA updates (Arduino OTA, Nextcloud, GitHub)

Hardware Requirements

ComponentModelNotes
MicrocontrollerDFRobot FireBeetle ESP32-EAny ESP32 with sufficient GPIO
DisplayGood Display GDEP073E017.3" 800x480 Spectra 6 (7-color)
Power SensorINA228 moduleAdafruit or generic, 15mΩ shunt
Solar Panel5V indoor panel~100-200mW for indoor light
Battery3.7V LiPo1000-2000mAh recommended

Wiring

Pinout Summary

FireBeetle ESP32-E Pin Assignments
==================================
I2C (INA228 Power Sensor)
GPIO 21 ─────── SDA
GPIO 22 ─────── SCL
GPIO 13 ─────── INA228 VCC (switched power)
SPI (Spectra 6 Display)
GPIO 18 ─────── SCK (Clock)
GPIO 23 ─────── MOSI (DIN)
GPIO 19 ─────── MISO (unused)
GPIO 3 ─────── CS (Chip Select)
GPIO 17 ─────── DC (Data/Command)
GPIO 16 ─────── RST (Reset)
GPIO 4 ─────── BUSY
Power Monitoring
GPIO 34 ─────── Battery voltage (built-in ADC divider)
Status
GPIO 2 ─────── Built-in LED

Wiring Diagram

 ┌─────────────────────────────────────┐
│ FireBeetle ESP32-E │
│ │
INA228 │ │ Spectra 6 Display
┌──────────┐ │ │ ┌──────────────┐
│ VCC ─────┼────────┤ GPIO 13 │ │ │
│ GND ─────┼────────┤ GND GPIO 18 ├────┤ CLK │
│ SDA ─────┼────────┤ GPIO 21 GPIO 23 ├────┤ DIN (MOSI) │
│ SCL ─────┼────────┤ GPIO 22 GPIO 3 ├────┤ CS │
│ │ │ GPIO 17 ├────┤ DC │
│ VIN+ ────┼─ Solar+│ GPIO 16 ├────┤ RST │
│ VIN- ────┼─ Solar-│ GPIO 4 ├────┤ BUSY │
└──────────┘ │ 3.3V ├────┤ VCC │
│ GND ├────┤ GND │
Solar Panel │ │ └──────────────┘
┌──────────┐ │ │
│ + ───────┼─ VIN+ on INA228 │
│ - ───────┼─ VIN- on INA228 ─── Battery charge circuit │
└──────────┘ │ │
└─────────────────────────────────────┘

INA228 Connection Details

The INA228 measures current through its shunt resistor. Connect:

  • VIN+: Solar panel positive
  • VIN-: To battery charging circuit (through shunt)
  • VCC: Connected to GPIO 13 for switchable power
  • GND: Common ground
  • SDA/SCL: I2C bus (GPIO 21/22)

I2C Address: Default 0x40 (A0=GND, A1=GND)

Software Setup

1. Install PlatformIO

Install PlatformIO IDE for VS Code.

2. Clone Project

cd~/Documents/PlatformIO/Projects
git clone https://github.com/Dreadmond/PhotonFrame.git
cd PhotonFrame

3. Configure Secrets

cp secrets.example.h secrets.h

Edit secrets.h with your credentials:

  • WiFi SSID and password
  • MQTT broker details
  • Nextcloud WebDAV URL and credentials
  • Arduino OTA password

4. Build and Upload

pio run -t upload
pio device monitor

Intelligent Refresh Algorithm

PhotonFrame uses a hybrid power-aware refresh algorithm:

BatterySolar PowerRefresh IntervalPower State
≥3.9V>100µW30 minutesabundant
≥3.9V<100µW2 hoursneutral
3.6-3.9V>100µW1 hourneutral
3.6-3.9V<100µW4 hoursconserving
3.4-3.6V>50µW4 hoursconserving
3.4-3.6V<50µW8 hourslow
<3.4Vany24 hoursemergency

Power Budget

  • Display refresh: ~0.4mAh per cycle (15-20s @ 80mA)
  • WiFi + MQTT: ~0.1mAh per wake cycle
  • Deep sleep: ~10µA
  • Indoor solar: ~50-200µW typical

OTA Updates

Arduino OTA (Development)

The device listens for Arduino OTA connections for 10 seconds after each boot.

# Upload via OTA (after initial USB flash)
pio run -t upload --upload-port photonframe.local

Credentials:

  • Hostname: photonframe.local
  • Password: Set in secrets.h (default: photonframe123)

To keep the device awake for OTA, send MQTT command:

{"action": "ota_mode"}

Device stays awake for 5 minutes with blinking LED.

Nextcloud OTA (Production)

  1. Build: pio run
  2. Upload .pio/build/firebeetle32/firmware.bin to: /Shared/firmware/photonframe/firmware.bin
  3. Device auto-updates on next boot
  4. File deleted after successful update

GitHub OTA (Fallback)

  1. Create GitHub release with tag v1.x.x
  2. Attach firmware.bin to release
  3. Device checks GitHub if no Nextcloud update

MQTT Integration

Topics

TopicDirectionDescription
photonframe/stateOutAll sensor data (JSON, retained)
photonframe/availabilityOutonline/offline (LWT)
photonframe/commandInCommands (JSON)
photonframe/statusOutStatus messages

State Payload

{
"battery_percentage": 85,
"battery_voltage": 3.92,
"solar_voltage_mV": 450.2,
"solar_current_uA": 125.3,
"solar_power_uW": 56.4,
"ina_temperature": 24.5,
"power_state": "neutral",
"next_refresh_seconds": 7200,
"firmware_version": "1.0.0",
"wifi_rssi": -52,
"free_heap": 180000,
"boot_count": 42,
"successful_updates": 38,
"error_count": 0,
"last_error": "none",
"ip_address": "192.168.1.123"
}

Commands

Send JSON to photonframe/command:

CommandDescription
{"action": "update_display"}Force image refresh on next boot
{"action": "reboot"}Restart device immediately
{"action": "check_ota"}Check Nextcloud/GitHub for updates
{"action": "ota_mode"}Stay awake 5 min for Arduino OTA
{"action": "clear_errors"}Reset error counters
{"action": "force_ha_discovery"}Republish HA discovery configs

Home Assistant Auto-Discovery

PhotonFrame registers sensors automatically via MQTT discovery:

  • Battery percentage & voltage
  • Solar voltage, current, power
  • INA228 temperature
  • Power state
  • Next refresh time
  • WiFi signal strength
  • Boot count, errors, firmware version

Sensors use expire_after: 90000 (25 hours) for deep-sleep compatibility.

Nextcloud Image Setup

Image Requirements

  • Format: PNG (8-bit RGB)
  • Resolution: 800x480 (or will be scaled to fit)
  • Max size: 500KB
  • Colors: Automatically quantized to 7-color palette

Folder Structure

Nextcloud/
├── Photos/EPaper/
│ ├── display.png # Primary image
│ └── fallback.png # Backup image
└── Shared/firmware/photonframe/
└── firmware.bin # OTA updates

Troubleshooting

Excessive Battery Drain / Negative Solar Current

If you observe:

  • Battery voltage steadily declining with no recovery
  • Solar current readings frequently negative (e.g., -50 to -150 µA)
  • Short sleep intervals despite low/no sunlight

Cause: Prior to v1.0.2, a bug in readPower() used abs(current) instead of max(0, current). This made negative (reverse) current appear as positive power, triggering shorter sleep intervals even when the solar panel wasn't actually charging the battery.

Solution: Update to firmware v1.0.2 or later. The fix ensures negative current = 0 usable power, which correctly triggers longer sleep intervals (8-24 hours) when solar isn't providing charge.

Understanding negative current: The INA228 on the high side of the solar panel measures current flowing INTO the energy harvester. Negative readings indicate reverse current flow (e.g., input capacitor discharge through the solar panel's internal resistance during low-light conditions). This is normal behavior but should not be counted as usable solar power.

INA228 Not Detected

Check serial output for:

Powering on INA228...
Initializing I2C on SDA=21, SCL=22
Scanning for INA228 at address 0x40...
Manufacturer ID read: 0x5449 <- Should be 0x5449

If ID is 0x0000 or 0xFFFF:

  1. Verify GPIO 13 is connected to INA228 VCC
  2. Check SDA (GPIO 21) and SCL (GPIO 22) connections
  3. Verify INA228 address matches (A0/A1 pins)

Display Not Updating

  1. Check SPI wiring, especially BUSY pin (GPIO 4)
  2. Verify display VCC is 3.3V (not 5V!)
  3. Check serial output for PNG decode errors
  4. Ensure image is valid PNG, under 500KB

MQTT Unavailable in Home Assistant

  1. Device uses expire_after instead of availability topic
  2. Sensors stay valid for 25 hours during deep sleep
  3. Send {"action": "force_ha_discovery"} to republish configs

WiFi Connection Issues

  1. Device creates PhotonFrame-Setup AP if WiFi fails
  2. Connect to AP, configure via captive portal
  3. Check credentials in secrets.h

Configuration Reference

secrets.h

#defineSECRET_WIFI_SSID"YourWiFi"
#defineSECRET_WIFI_PASSWORD"YourPassword"
#defineSECRET_MQTT_BROKER"192.168.1.100"
#defineSECRET_MQTT_PORT1883
#defineSECRET_MQTT_USERNAME"mqtt_user"
#defineSECRET_MQTT_PASSWORD"mqtt_pass"
#defineSECRET_MQTT_CLIENT_ID"photonframe_001"
#defineSECRET_NEXTCLOUD_URL"https://cloud.example.com/remote.php/dav/files/User/Photos/EPaper/"
#defineSECRET_NEXTCLOUD_USER"username"
#defineSECRET_NEXTCLOUD_PASS"app-password"
#defineSECRET_NEXTCLOUD_PHOTO"display.png"
#defineSECRET_NEXTCLOUD_FALLBACK"fallback.png"
#defineOTA_HOSTNAME"photonframe"
#defineOTA_PASSWORD"photonframe123"

config.h Defaults

ConstantValueDescription
INA228_I2C_ADDR0x40I2C address
INA228_SHUNT_RESISTOR0.01515mΩ shunt
VOLTAGE_CRITICAL3.4VEmergency threshold
VOLTAGE_LOW3.6VLow battery threshold
VOLTAGE_MEDIUM3.9VMedium battery threshold
POWER_ABUNDANT_UW100µW for "abundant" state
POWER_LOW_UW50µW for "low" state

Project Structure

PhotonFrame/
├── platformio.ini # Build configuration
├── secrets.h # Credentials (gitignored)
├── secrets.example.h # Template
├── README.md
├── src/
│ ├── main.cpp # Main firmware
│ ├── config.h # Pin definitions, constants
│ ├── display.h/cpp # Spectra 6 e-paper driver
│ ├── power.h/cpp # INA228 driver, sleep logic
│ └── ota.h # Nextcloud/GitHub OTA
└── docs/
└── PINOUT.md # Detailed wiring reference

Changelog

v1.0.2

  • Fixed: Power calculation bug that caused excessive battery drain
    • readPower() now uses max(0, current) instead of abs(current)
    • Negative solar current (reverse flow) correctly results in 0 power
    • Longer sleep intervals now properly trigger when solar isn't charging

v1.0.1

  • Fixed PNG display rendering
  • Fixed indoor solar power measurement for low-current scenarios

v1.0.0

  • Initial release

License

MIT License

Credits

About

Indoor solar-powered e-paper display with INA228 power monitoring and intelligent refresh scheduling

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PhotonFrame

An indoor solar-powered e-paper photo frame with intelligent power management.

Overview

PhotonFrame is a FireBeetle ESP32-E powered 7.3" Spectra 6 (7-color) e-paper display that:

  • Fetches images from Nextcloud WebDAV
  • Monitors solar panel power via INA228 sensor
  • Adjusts refresh intervals based on available power + battery voltage
  • Reports telemetry to Home Assistant via MQTT
  • Supports OTA updates (Arduino OTA, Nextcloud, GitHub)

Hardware Requirements

ComponentModelNotes
MicrocontrollerDFRobot FireBeetle ESP32-EAny ESP32 with sufficient GPIO
DisplayGood Display GDEP073E017.3" 800x480 Spectra 6 (7-color)
Power SensorINA228 moduleAdafruit or generic, 15mΩ shunt
Solar Panel5V indoor panel~100-200mW for indoor light
Battery3.7V LiPo1000-2000mAh recommended

Wiring

Pinout Summary

FireBeetle ESP32-E Pin Assignments
==================================
I2C (INA228 Power Sensor)
GPIO 21 ─────── SDA
GPIO 22 ─────── SCL
GPIO 13 ─────── INA228 VCC (switched power)
SPI (Spectra 6 Display)
GPIO 18 ─────── SCK (Clock)
GPIO 23 ─────── MOSI (DIN)
GPIO 19 ─────── MISO (unused)
GPIO 3 ─────── CS (Chip Select)
GPIO 17 ─────── DC (Data/Command)
GPIO 16 ─────── RST (Reset)
GPIO 4 ─────── BUSY
Power Monitoring
GPIO 34 ─────── Battery voltage (built-in ADC divider)
Status
GPIO 2 ─────── Built-in LED

Wiring Diagram

 ┌─────────────────────────────────────┐
│ FireBeetle ESP32-E │
│ │
INA228 │ │ Spectra 6 Display
┌──────────┐ │ │ ┌──────────────┐
│ VCC ─────┼────────┤ GPIO 13 │ │ │
│ GND ─────┼────────┤ GND GPIO 18 ├────┤ CLK │
│ SDA ─────┼────────┤ GPIO 21 GPIO 23 ├────┤ DIN (MOSI) │
│ SCL ─────┼────────┤ GPIO 22 GPIO 3 ├────┤ CS │
│ │ │ GPIO 17 ├────┤ DC │
│ VIN+ ────┼─ Solar+│ GPIO 16 ├────┤ RST │
│ VIN- ────┼─ Solar-│ GPIO 4 ├────┤ BUSY │
└──────────┘ │ 3.3V ├────┤ VCC │
│ GND ├────┤ GND │
Solar Panel │ │ └──────────────┘
┌──────────┐ │ │
│ + ───────┼─ VIN+ on INA228 │
│ - ───────┼─ VIN- on INA228 ─── Battery charge circuit │
└──────────┘ │ │
└─────────────────────────────────────┘

INA228 Connection Details

The INA228 measures current through its shunt resistor. Connect:

  • VIN+: Solar panel positive
  • VIN-: To battery charging circuit (through shunt)
  • VCC: Connected to GPIO 13 for switchable power
  • GND: Common ground
  • SDA/SCL: I2C bus (GPIO 21/22)

I2C Address: Default 0x40 (A0=GND, A1=GND)

Software Setup

1. Install PlatformIO

Install PlatformIO IDE for VS Code.

2. Clone Project

cd~/Documents/PlatformIO/Projects
git clone https://github.com/Dreadmond/PhotonFrame.git
cd PhotonFrame

3. Configure Secrets

cp secrets.example.h secrets.h

Edit secrets.h with your credentials:

  • WiFi SSID and password
  • MQTT broker details
  • Nextcloud WebDAV URL and credentials
  • Arduino OTA password

4. Build and Upload

pio run -t upload
pio device monitor

Intelligent Refresh Algorithm

PhotonFrame uses a hybrid power-aware refresh algorithm:

BatterySolar PowerRefresh IntervalPower State
≥3.9V>100µW30 minutesabundant
≥3.9V<100µW2 hoursneutral
3.6-3.9V>100µW1 hourneutral
3.6-3.9V<100µW4 hoursconserving
3.4-3.6V>50µW4 hoursconserving
3.4-3.6V<50µW8 hourslow
<3.4Vany24 hoursemergency

Power Budget

  • Display refresh: ~0.4mAh per cycle (15-20s @ 80mA)
  • WiFi + MQTT: ~0.1mAh per wake cycle
  • Deep sleep: ~10µA
  • Indoor solar: ~50-200µW typical

OTA Updates

Arduino OTA (Development)

The device listens for Arduino OTA connections for 10 seconds after each boot.

# Upload via OTA (after initial USB flash)
pio run -t upload --upload-port photonframe.local

Credentials:

  • Hostname: photonframe.local
  • Password: Set in secrets.h (default: photonframe123)

To keep the device awake for OTA, send MQTT command:

{"action": "ota_mode"}

Device stays awake for 5 minutes with blinking LED.

Nextcloud OTA (Production)

  1. Build: pio run
  2. Upload .pio/build/firebeetle32/firmware.bin to: /Shared/firmware/photonframe/firmware.bin
  3. Device auto-updates on next boot
  4. File deleted after successful update

GitHub OTA (Fallback)

  1. Create GitHub release with tag v1.x.x
  2. Attach firmware.bin to release
  3. Device checks GitHub if no Nextcloud update

MQTT Integration

Topics

TopicDirectionDescription
photonframe/stateOutAll sensor data (JSON, retained)
photonframe/availabilityOutonline/offline (LWT)
photonframe/commandInCommands (JSON)
photonframe/statusOutStatus messages

State Payload

{
"battery_percentage": 85,
"battery_voltage": 3.92,
"solar_voltage_mV": 450.2,
"solar_current_uA": 125.3,
"solar_power_uW": 56.4,
"ina_temperature": 24.5,
"power_state": "neutral",
"next_refresh_seconds": 7200,
"firmware_version": "1.0.0",
"wifi_rssi": -52,
"free_heap": 180000,
"boot_count": 42,
"successful_updates": 38,
"error_count": 0,
"last_error": "none",
"ip_address": "192.168.1.123"
}

Commands

Send JSON to photonframe/command:

CommandDescription
{"action": "update_display"}Force image refresh on next boot
{"action": "reboot"}Restart device immediately
{"action": "check_ota"}Check Nextcloud/GitHub for updates
{"action": "ota_mode"}Stay awake 5 min for Arduino OTA
{"action": "clear_errors"}Reset error counters
{"action": "force_ha_discovery"}Republish HA discovery configs

Home Assistant Auto-Discovery

PhotonFrame registers sensors automatically via MQTT discovery:

  • Battery percentage & voltage
  • Solar voltage, current, power
  • INA228 temperature
  • Power state
  • Next refresh time
  • WiFi signal strength
  • Boot count, errors, firmware version

Sensors use expire_after: 90000 (25 hours) for deep-sleep compatibility.

Nextcloud Image Setup

Image Requirements

  • Format: PNG (8-bit RGB)
  • Resolution: 800x480 (or will be scaled to fit)
  • Max size: 500KB
  • Colors: Automatically quantized to 7-color palette

Folder Structure

Nextcloud/
├── Photos/EPaper/
│ ├── display.png # Primary image
│ └── fallback.png # Backup image
└── Shared/firmware/photonframe/
└── firmware.bin # OTA updates

Troubleshooting

Excessive Battery Drain / Negative Solar Current

If you observe:

  • Battery voltage steadily declining with no recovery
  • Solar current readings frequently negative (e.g., -50 to -150 µA)
  • Short sleep intervals despite low/no sunlight

Cause: Prior to v1.0.2, a bug in readPower() used abs(current) instead of max(0, current). This made negative (reverse) current appear as positive power, triggering shorter sleep intervals even when the solar panel wasn't actually charging the battery.

Solution: Update to firmware v1.0.2 or later. The fix ensures negative current = 0 usable power, which correctly triggers longer sleep intervals (8-24 hours) when solar isn't providing charge.

Understanding negative current: The INA228 on the high side of the solar panel measures current flowing INTO the energy harvester. Negative readings indicate reverse current flow (e.g., input capacitor discharge through the solar panel's internal resistance during low-light conditions). This is normal behavior but should not be counted as usable solar power.

INA228 Not Detected

Check serial output for:

Powering on INA228...
Initializing I2C on SDA=21, SCL=22
Scanning for INA228 at address 0x40...
Manufacturer ID read: 0x5449 <- Should be 0x5449

If ID is 0x0000 or 0xFFFF:

  1. Verify GPIO 13 is connected to INA228 VCC
  2. Check SDA (GPIO 21) and SCL (GPIO 22) connections
  3. Verify INA228 address matches (A0/A1 pins)

Display Not Updating

  1. Check SPI wiring, especially BUSY pin (GPIO 4)
  2. Verify display VCC is 3.3V (not 5V!)
  3. Check serial output for PNG decode errors
  4. Ensure image is valid PNG, under 500KB

MQTT Unavailable in Home Assistant

  1. Device uses expire_after instead of availability topic
  2. Sensors stay valid for 25 hours during deep sleep
  3. Send {"action": "force_ha_discovery"} to republish configs

WiFi Connection Issues

  1. Device creates PhotonFrame-Setup AP if WiFi fails
  2. Connect to AP, configure via captive portal
  3. Check credentials in secrets.h

Configuration Reference

secrets.h

#defineSECRET_WIFI_SSID"YourWiFi"
#defineSECRET_WIFI_PASSWORD"YourPassword"
#defineSECRET_MQTT_BROKER"192.168.1.100"
#defineSECRET_MQTT_PORT1883
#defineSECRET_MQTT_USERNAME"mqtt_user"
#defineSECRET_MQTT_PASSWORD"mqtt_pass"
#defineSECRET_MQTT_CLIENT_ID"photonframe_001"
#defineSECRET_NEXTCLOUD_URL"https://cloud.example.com/remote.php/dav/files/User/Photos/EPaper/"
#defineSECRET_NEXTCLOUD_USER"username"
#defineSECRET_NEXTCLOUD_PASS"app-password"
#defineSECRET_NEXTCLOUD_PHOTO"display.png"
#defineSECRET_NEXTCLOUD_FALLBACK"fallback.png"
#defineOTA_HOSTNAME"photonframe"
#defineOTA_PASSWORD"photonframe123"

config.h Defaults

ConstantValueDescription
INA228_I2C_ADDR0x40I2C address
INA228_SHUNT_RESISTOR0.01515mΩ shunt
VOLTAGE_CRITICAL3.4VEmergency threshold
VOLTAGE_LOW3.6VLow battery threshold
VOLTAGE_MEDIUM3.9VMedium battery threshold
POWER_ABUNDANT_UW100µW for "abundant" state
POWER_LOW_UW50µW for "low" state

Project Structure

PhotonFrame/
├── platformio.ini # Build configuration
├── secrets.h # Credentials (gitignored)
├── secrets.example.h # Template
├── README.md
├── src/
│ ├── main.cpp # Main firmware
│ ├── config.h # Pin definitions, constants
│ ├── display.h/cpp # Spectra 6 e-paper driver
│ ├── power.h/cpp # INA228 driver, sleep logic
│ └── ota.h # Nextcloud/GitHub OTA
└── docs/
└── PINOUT.md # Detailed wiring reference

Changelog

v1.0.2

  • Fixed: Power calculation bug that caused excessive battery drain
    • readPower() now uses max(0, current) instead of abs(current)
    • Negative solar current (reverse flow) correctly results in 0 power
    • Longer sleep intervals now properly trigger when solar isn't charging

v1.0.1

  • Fixed PNG display rendering
  • Fixed indoor solar power measurement for low-current scenarios

v1.0.0

  • Initial release

License

MIT License

Credits

About

Indoor solar-powered e-paper display with INA228 power monitoring and intelligent refresh scheduling

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PhotonFrame

An indoor solar-powered e-paper photo frame with intelligent power management.

Overview

PhotonFrame is a FireBeetle ESP32-E powered 7.3" Spectra 6 (7-color) e-paper display that:

  • Fetches images from Nextcloud WebDAV
  • Monitors solar panel power via INA228 sensor
  • Adjusts refresh intervals based on available power + battery voltage
  • Reports telemetry to Home Assistant via MQTT
  • Supports OTA updates (Arduino OTA, Nextcloud, GitHub)

Hardware Requirements

ComponentModelNotes
MicrocontrollerDFRobot FireBeetle ESP32-EAny ESP32 with sufficient GPIO
DisplayGood Display GDEP073E017.3" 800x480 Spectra 6 (7-color)
Power SensorINA228 moduleAdafruit or generic, 15mΩ shunt
Solar Panel5V indoor panel~100-200mW for indoor light
Battery3.7V LiPo1000-2000mAh recommended

Wiring

Pinout Summary

FireBeetle ESP32-E Pin Assignments
==================================
I2C (INA228 Power Sensor)
GPIO 21 ─────── SDA
GPIO 22 ─────── SCL
GPIO 13 ─────── INA228 VCC (switched power)
SPI (Spectra 6 Display)
GPIO 18 ─────── SCK (Clock)
GPIO 23 ─────── MOSI (DIN)
GPIO 19 ─────── MISO (unused)
GPIO 3 ─────── CS (Chip Select)
GPIO 17 ─────── DC (Data/Command)
GPIO 16 ─────── RST (Reset)
GPIO 4 ─────── BUSY
Power Monitoring
GPIO 34 ─────── Battery voltage (built-in ADC divider)
Status
GPIO 2 ─────── Built-in LED

Wiring Diagram

 ┌─────────────────────────────────────┐
│ FireBeetle ESP32-E │
│ │
INA228 │ │ Spectra 6 Display
┌──────────┐ │ │ ┌──────────────┐
│ VCC ─────┼────────┤ GPIO 13 │ │ │
│ GND ─────┼────────┤ GND GPIO 18 ├────┤ CLK │
│ SDA ─────┼────────┤ GPIO 21 GPIO 23 ├────┤ DIN (MOSI) │
│ SCL ─────┼────────┤ GPIO 22 GPIO 3 ├────┤ CS │
│ │ │ GPIO 17 ├────┤ DC │
│ VIN+ ────┼─ Solar+│ GPIO 16 ├────┤ RST │
│ VIN- ────┼─ Solar-│ GPIO 4 ├────┤ BUSY │
└──────────┘ │ 3.3V ├────┤ VCC │
│ GND ├────┤ GND │
Solar Panel │ │ └──────────────┘
┌──────────┐ │ │
│ + ───────┼─ VIN+ on INA228 │
│ - ───────┼─ VIN- on INA228 ─── Battery charge circuit │
└──────────┘ │ │
└─────────────────────────────────────┘

INA228 Connection Details

The INA228 measures current through its shunt resistor. Connect:

  • VIN+: Solar panel positive
  • VIN-: To battery charging circuit (through shunt)
  • VCC: Connected to GPIO 13 for switchable power
  • GND: Common ground
  • SDA/SCL: I2C bus (GPIO 21/22)

I2C Address: Default 0x40 (A0=GND, A1=GND)

Software Setup

1. Install PlatformIO

Install PlatformIO IDE for VS Code.

2. Clone Project

cd~/Documents/PlatformIO/Projects
git clone https://github.com/Dreadmond/PhotonFrame.git
cd PhotonFrame

3. Configure Secrets

cp secrets.example.h secrets.h

Edit secrets.h with your credentials:

  • WiFi SSID and password
  • MQTT broker details
  • Nextcloud WebDAV URL and credentials
  • Arduino OTA password

4. Build and Upload

pio run -t upload
pio device monitor

Intelligent Refresh Algorithm

PhotonFrame uses a hybrid power-aware refresh algorithm:

BatterySolar PowerRefresh IntervalPower State
≥3.9V>100µW30 minutesabundant
≥3.9V<100µW2 hoursneutral
3.6-3.9V>100µW1 hourneutral
3.6-3.9V<100µW4 hoursconserving
3.4-3.6V>50µW4 hoursconserving
3.4-3.6V<50µW8 hourslow
<3.4Vany24 hoursemergency

Power Budget

  • Display refresh: ~0.4mAh per cycle (15-20s @ 80mA)
  • WiFi + MQTT: ~0.1mAh per wake cycle
  • Deep sleep: ~10µA
  • Indoor solar: ~50-200µW typical

OTA Updates

Arduino OTA (Development)

The device listens for Arduino OTA connections for 10 seconds after each boot.

# Upload via OTA (after initial USB flash)
pio run -t upload --upload-port photonframe.local

Credentials:

  • Hostname: photonframe.local
  • Password: Set in secrets.h (default: photonframe123)

To keep the device awake for OTA, send MQTT command:

{"action": "ota_mode"}

Device stays awake for 5 minutes with blinking LED.

Nextcloud OTA (Production)

  1. Build: pio run
  2. Upload .pio/build/firebeetle32/firmware.bin to: /Shared/firmware/photonframe/firmware.bin
  3. Device auto-updates on next boot
  4. File deleted after successful update

GitHub OTA (Fallback)

  1. Create GitHub release with tag v1.x.x
  2. Attach firmware.bin to release
  3. Device checks GitHub if no Nextcloud update

MQTT Integration

Topics

TopicDirectionDescription
photonframe/stateOutAll sensor data (JSON, retained)
photonframe/availabilityOutonline/offline (LWT)
photonframe/commandInCommands (JSON)
photonframe/statusOutStatus messages

State Payload

{
"battery_percentage": 85,
"battery_voltage": 3.92,
"solar_voltage_mV": 450.2,
"solar_current_uA": 125.3,
"solar_power_uW": 56.4,
"ina_temperature": 24.5,
"power_state": "neutral",
"next_refresh_seconds": 7200,
"firmware_version": "1.0.0",
"wifi_rssi": -52,
"free_heap": 180000,
"boot_count": 42,
"successful_updates": 38,
"error_count": 0,
"last_error": "none",
"ip_address": "192.168.1.123"
}

Commands

Send JSON to photonframe/command:

CommandDescription
{"action": "update_display"}Force image refresh on next boot
{"action": "reboot"}Restart device immediately
{"action": "check_ota"}Check Nextcloud/GitHub for updates
{"action": "ota_mode"}Stay awake 5 min for Arduino OTA
{"action": "clear_errors"}Reset error counters
{"action": "force_ha_discovery"}Republish HA discovery configs

Home Assistant Auto-Discovery

PhotonFrame registers sensors automatically via MQTT discovery:

  • Battery percentage & voltage
  • Solar voltage, current, power
  • INA228 temperature
  • Power state
  • Next refresh time
  • WiFi signal strength
  • Boot count, errors, firmware version

Sensors use expire_after: 90000 (25 hours) for deep-sleep compatibility.

Nextcloud Image Setup

Image Requirements

  • Format: PNG (8-bit RGB)
  • Resolution: 800x480 (or will be scaled to fit)
  • Max size: 500KB
  • Colors: Automatically quantized to 7-color palette

Folder Structure

Nextcloud/
├── Photos/EPaper/
│ ├── display.png # Primary image
│ └── fallback.png # Backup image
└── Shared/firmware/photonframe/
└── firmware.bin # OTA updates

Troubleshooting

Excessive Battery Drain / Negative Solar Current

If you observe:

  • Battery voltage steadily declining with no recovery
  • Solar current readings frequently negative (e.g., -50 to -150 µA)
  • Short sleep intervals despite low/no sunlight

Cause: Prior to v1.0.2, a bug in readPower() used abs(current) instead of max(0, current). This made negative (reverse) current appear as positive power, triggering shorter sleep intervals even when the solar panel wasn't actually charging the battery.

Solution: Update to firmware v1.0.2 or later. The fix ensures negative current = 0 usable power, which correctly triggers longer sleep intervals (8-24 hours) when solar isn't providing charge.

Understanding negative current: The INA228 on the high side of the solar panel measures current flowing INTO the energy harvester. Negative readings indicate reverse current flow (e.g., input capacitor discharge through the solar panel's internal resistance during low-light conditions). This is normal behavior but should not be counted as usable solar power.

INA228 Not Detected

Check serial output for:

Powering on INA228...
Initializing I2C on SDA=21, SCL=22
Scanning for INA228 at address 0x40...
Manufacturer ID read: 0x5449 <- Should be 0x5449

If ID is 0x0000 or 0xFFFF:

  1. Verify GPIO 13 is connected to INA228 VCC
  2. Check SDA (GPIO 21) and SCL (GPIO 22) connections
  3. Verify INA228 address matches (A0/A1 pins)

Display Not Updating

  1. Check SPI wiring, especially BUSY pin (GPIO 4)
  2. Verify display VCC is 3.3V (not 5V!)
  3. Check serial output for PNG decode errors
  4. Ensure image is valid PNG, under 500KB

MQTT Unavailable in Home Assistant

  1. Device uses expire_after instead of availability topic
  2. Sensors stay valid for 25 hours during deep sleep
  3. Send {"action": "force_ha_discovery"} to republish configs

WiFi Connection Issues

  1. Device creates PhotonFrame-Setup AP if WiFi fails
  2. Connect to AP, configure via captive portal
  3. Check credentials in secrets.h

Configuration Reference

secrets.h

#defineSECRET_WIFI_SSID"YourWiFi"
#defineSECRET_WIFI_PASSWORD"YourPassword"
#defineSECRET_MQTT_BROKER"192.168.1.100"
#defineSECRET_MQTT_PORT1883
#defineSECRET_MQTT_USERNAME"mqtt_user"
#defineSECRET_MQTT_PASSWORD"mqtt_pass"
#defineSECRET_MQTT_CLIENT_ID"photonframe_001"
#defineSECRET_NEXTCLOUD_URL"https://cloud.example.com/remote.php/dav/files/User/Photos/EPaper/"
#defineSECRET_NEXTCLOUD_USER"username"
#defineSECRET_NEXTCLOUD_PASS"app-password"
#defineSECRET_NEXTCLOUD_PHOTO"display.png"
#defineSECRET_NEXTCLOUD_FALLBACK"fallback.png"
#defineOTA_HOSTNAME"photonframe"
#defineOTA_PASSWORD"photonframe123"

config.h Defaults

ConstantValueDescription
INA228_I2C_ADDR0x40I2C address
INA228_SHUNT_RESISTOR0.01515mΩ shunt
VOLTAGE_CRITICAL3.4VEmergency threshold
VOLTAGE_LOW3.6VLow battery threshold
VOLTAGE_MEDIUM3.9VMedium battery threshold
POWER_ABUNDANT_UW100µW for "abundant" state
POWER_LOW_UW50µW for "low" state

Project Structure

PhotonFrame/
├── platformio.ini # Build configuration
├── secrets.h # Credentials (gitignored)
├── secrets.example.h # Template
├── README.md
├── src/
│ ├── main.cpp # Main firmware
│ ├── config.h # Pin definitions, constants
│ ├── display.h/cpp # Spectra 6 e-paper driver
│ ├── power.h/cpp # INA228 driver, sleep logic
│ └── ota.h # Nextcloud/GitHub OTA
└── docs/
└── PINOUT.md # Detailed wiring reference

Changelog

v1.0.2

  • Fixed: Power calculation bug that caused excessive battery drain
    • readPower() now uses max(0, current) instead of abs(current)
    • Negative solar current (reverse flow) correctly results in 0 power
    • Longer sleep intervals now properly trigger when solar isn't charging

v1.0.1

  • Fixed PNG display rendering
  • Fixed indoor solar power measurement for low-current scenarios

v1.0.0

  • Initial release

License

MIT License

Credits

About

Indoor solar-powered e-paper display with INA228 power monitoring and intelligent refresh scheduling

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages