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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ so the SD manager itself stays device-agnostic.
|---|---|---|---|---|
| **Xteink X4** | ESP32-C3 | SSD1677 | 800×480 | B/W + 4-level grayscale |
| **Xteink X3** | ESP32-C3 | UC8253 | 792×528 | B/W + 4-level grayscale; BQ27220 I²C battery gauge; shares the C3 binary with X4 |
| **OnePage** | ESP32-C61 | SSD1677 | 800×480 | B/W + 4-level grayscale, 4-key front resistor ADC ladder + 3 side keys, shared SPI MicroSD with power gating, Wi-Fi 6 + BLE 5.4 wireless page-turner remote host |
| **de-link** | ESP32-S3 | SSD1677 | 800×480 | B/W + grayscale, PWM frontlight, native 4-bit SDMMC SD |
| **M5Stack PaperColor** | ESP32-S3 | ED2208 | 400×600 Spectra-6 color | native interrupted-refresh driver, optional M5GFX backend, built-in speaker (ES8311 codec + AW8737A amp), 2x RGB LEDs |
| **Murphy M3** | ESP32-S3 | UC8253 | 240×416 | B/W (90°-rotated framebuffer, full/fast LUTs), CHSC6x touch, PWM frontlight |
Expand All @@ -145,8 +146,8 @@ found so the caller can `display.setDisplayX3()`. Call it before
profile. In builds without an Xteink profile the helpers compile to no-ops that
return false without touching any pins, so an unconditional call is safe on
every device. Devices on a different MCU build their own binary, selected with a
`-DFREEINK_DEVICE_*` flag. A build targets exactly one of the three MCU families — ESP32-C3 (X3/X4),
ESP32-S3 (de-link/PaperColor/Murphy/LilyGo/Sticky/X4 Pro/Paper Mono/PaperS3), or classic ESP32 (M5Paper);
`-DFREEINK_DEVICE_*` flag. A build targets exactly one of the four MCU families — ESP32-C3 (X3/X4),
ESP32-C61 (OnePage), ESP32-S3 (de-link/PaperColor/Murphy/LilyGo/Sticky/X4 Pro/Paper Mono/PaperS3), or classic ESP32 (M5Paper);
`BoardConfig` rejects mixing families at compile time.

#### Per-batch panel controllers (`applyXteinkDisplayController()`)
Expand All @@ -160,9 +161,7 @@ batch. `XteinkDetect` resolves which silicon a unit carries at boot:
```cpp
#include <XteinkDetect.h>

// Before FreeInkDisplay::begin() — promotes BoardConfig::ACTIVE.displayController
// to the UltraChip sibling when this unit carries it, so begin() selects the
// matching driver. No-op (returns false) on builds without a probe-capable device.
// Detect and apply the display controller for this unit's batch:
freeink::applyXteinkDisplayController();
display.begin();
```
Expand Down Expand Up @@ -295,7 +294,18 @@ wasTouchReleased/getTouchPoint`; it delivers coordinates raw-panel-oriented and
app owns display-orientation mapping. GT911 additionally provides allocation-free
multi-contact snapshots, completed 2-4 finger translation gestures, and completed
two-finger rotations with a signed angle, center, and duration. The GT911 boards set
their `TouchConfig` in the board profile (e.g. `BoardConfig::LILYGO_T5_PRO_GT911`).
### OnePage Reader board support (ESP32-C61)

The OnePage Reader is an open-hardware DIY e-reader built around the **ESP32-C61**
RISC-V wireless SoC. Complete hardware specifications, pinout, power gating, and
board profile details are documented in [docs/onepage-c61-support.md](docs/onepage-c61-support.md).

Key characteristics:
- **Display**: SSD1677 800×480 on SPI @ 20MHz.
- **MicroSD**: SPI mode sharing the bus with EPD, powered via GPIO27 power rail.
- **Input**: 4-key front resistor ADC ladder on GPIO4 (`OnePageAdcLadder`) + 3 discrete active-low side keys (`UP=6, DOWN=9, POWER=2`).
- **Battery / Power**: ADC sampling on GPIO5 with charge-pause control on GPIO10 (`BAT_CHG_EN`).
- **Bluetooth**: Wi-Fi 6 + BLE 5.4 with BLE HID Central / page-turner remote host support.

## Build composition — devices × capabilities

Expand All @@ -310,6 +320,7 @@ MCU (a C3-vs-S3 mix is a compile error):
|---|---|
| `-DFREEINK_DEVICE_X4` | X4 only — links just SSD1677 (tightest) |
| `-DFREEINK_DEVICE_X3 -DFREEINK_DEVICE_X4` | X3 **and** X4 in one C3 binary, runtime-selected via `setDisplayX3()` |
| `-DFREEINK_DEVICE_ONEPAGE` | OnePage (C61, SSD1677 800×480 + 4-key ADC ladder + 3 side keys + shared SD) |
| `-DFREEINK_DEVICE_DELINK` | de-link (S3, SSD1677 + frontlight) |
| `-DFREEINK_DEVICE_M5` | M5 PaperColor (S3, ED2208 + color) |
| `-DFREEINK_DEVICE_MURPHY` | Murphy M3 (S3, UC8253 + touch + frontlight) |
Expand Down
82 changes: 82 additions & 0 deletions docs/onepage-c61-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# OnePage ESP32-C61 Board Support

## Overview

**OnePage Reader** is an open-hardware DIY E-reader built around the **ESP32-C61** RISC-V wireless SoC.
Main ecosystem repo: [MoveCall/onepage-reader](https://github.com/MoveCall/onepage-reader)

This document describes the FreeInk SDK board support profile for OnePage (`FREEINK_DEVICE_ONEPAGE`).

## Hardware Specifications

| Subsystem | Specification / Pinout |
|:---|:---|
| **SoC** | ESP32-C61 (ESP32-C61HR2, 160MHz RISC-V, Wi-Fi 6, BLE 5.4, 16MB Flash, 2MB PSRAM @ 40MHz) |
| **Display** | 4.26" E-Ink Display (GDEQ0426T82), **SSD1677** controller, **800 × 480** resolution, 4-level grayscale |
| **Display SPI** | SCLK: `GPIO22`, MOSI: `GPIO23`, CS: `GPIO25`, DC: `GPIO8`, RST: `GPIO27`, BUSY: `GPIO29` · **20 MHz** |
| **MicroSD Card** | SPI mode (shared SPI bus with EPD): SCLK: `GPIO22`, MISO: `GPIO24`, MOSI: `GPIO23`, CS: `GPIO26` · **20 MHz** |
| **Buttons (Front 4-Key)** | Single resistor ADC ladder on `GPIO4` (ADC1_CH2):<br>• `BACK`: 2400..2800 mV (~2592 mV)<br>• `LEFT`: 1780..2140 mV (~1956 mV)<br>• `RIGHT`: 1140..1500 mV (~1316 mV)<br>• `CONFIRM` (ENTER): 0..250 mV (~0 mV) |
| **Buttons (Side 3-Key)** | Active-low GPIO buttons with internal pull-up:<br>• `UP` (PREV): `GPIO6`<br>• `DOWN` (NEXT): `GPIO9`<br>• `POWER` (WAKE): `GPIO2` (Deep-sleep wakeup source) |
| **Battery & Charging** | • Battery ADC: `GPIO5` (ADC1_CH3, multiplier ×2.0)<br>• USB / Charge Detect: `GPIO11` (UART0_RXD, LM66200 ST open-drain, low = USB present)<br>• Charge Pause: `GPIO10` (`BAT_CHG_EN`, drive LOW during ADC sample to avoid charger offset) |
| **Power Architecture** | `GPIO27` serves as both EPD RST and shared SD power rail enable. Silenced before deep sleep. |
| **Bluetooth Remote** | BLE HID Central / Host support for wireless page turners |

## FreeInk SDK Profile

In FreeInk SDK (`BoardConfig.h`):

```cpp
constexpr BoardProfile ONEPAGE = {
Board::OnePage,
"onepage",
InputStyle::OnePageAdcLadder,
DisplayController::SSD1677,
800,
480,
{22, 23, 25, 8, 27, 29, PIN_UNASSIGNED}, // display SPI
20000000, // 20 MHz
{22, 24, 23, 26, PIN_UNASSIGNED, false, 20000000, true}, // microSD
{PIN_UNASSIGNED, PIN_UNASSIGNED, PIN_UNASSIGNED, PIN_UNASSIGNED, 6, 9, 2, false, 4}, // input
5, // batteryAdc (GPIO5)
11, // batteryChargeStatus (GPIO11)
2.0f, // batteryDividerMultiplier
11, // usbDetect (GPIO11)
NO_TOUCH,
NO_FRONTLIGHT,
NO_AUDIO,
NO_LEDS,
NO_FLIP,
NO_SDMMC,
NO_GAUGE,
NO_MIC,
NO_SENSORS,
1.0f, // uiScale
{PIN_UNASSIGNED, PIN_UNASSIGNED, 10, true},// chargeEnable (GPIO10)
0,
{9, 3, 3, 3},
false // batteryChargeStatusActiveHigh: false
};
```

## Build Configuration

Add the following to your project's `platformio.ini`:

```ini
[env:onepage]
extends = base
board = onepage-c61
board_build.mcu = esp32c61
custom_sdkconfig =
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_QUAD=y
CONFIG_SPIRAM_SPEED_40M=y
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_BOOT_INIT=y
CONFIG_SPIRAM_BOOT_HW_INIT=y
CONFIG_BT_LE_SLEEP_ENABLE=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
build_flags =
${base.build_flags}
-DFREEINK_DEVICE_ONEPAGE=1
```
15 changes: 14 additions & 1 deletion libs/hardware/BatteryMonitor/src/BatteryMonitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,20 @@ uint16_t BatteryMonitor::readMillivolts() const {
const uint16_t mv = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
#else
// ESP-IDF 5.x has analogReadMilliVolts
const uint16_t mv = analogReadMilliVolts(_adcPin);
// OnePage only: pause charging around the read. Other boards with a
// chargeEnable pin (e.g. Sticky) must NOT have it glitched during battery reads.
uint16_t mv = 0;
if (BoardConfig::isOnePage() && BoardConfig::ACTIVE.power.chargeEnable >= 0) {
const int8_t ce = BoardConfig::ACTIVE.power.chargeEnable;
const bool activeHigh = BoardConfig::ACTIVE.power.chargeEnableActiveHigh;
pinMode(ce, OUTPUT);
digitalWrite(ce, activeHigh ? LOW : HIGH); // pause charging
delay(5);
mv = analogReadMilliVolts(_adcPin);
digitalWrite(ce, activeHigh ? HIGH : LOW); // resume charging
} else {
mv = analogReadMilliVolts(_adcPin);
}
#endif

return static_cast<uint16_t>(mv * _dividerMultiplier);
Expand Down
74 changes: 64 additions & 10 deletions libs/hardware/BoardConfig/include/BoardConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,39 +72,43 @@
#ifndef FREEINK_DEVICE_EEGO_A4
#define FREEINK_DEVICE_EEGO_A4 0
#endif
#ifndef FREEINK_DEVICE_ONEPAGE
#define FREEINK_DEVICE_ONEPAGE 0
#endif

// --- 2) Coherence: exactly one MCU family, at least one device ---------------
#if !(FREEINK_DEVICE_X4 || FREEINK_DEVICE_X3 || FREEINK_DEVICE_X4PRO || FREEINK_DEVICE_X4CLASSIC || FREEINK_DEVICE_M5 || \
FREEINK_DEVICE_MURPHY || FREEINK_DEVICE_DELINK || FREEINK_DEVICE_LILYGO || FREEINK_DEVICE_M5PAPER || \
FREEINK_DEVICE_STICKY || FREEINK_DEVICE_PAPERMONO || FREEINK_DEVICE_PAPERS3 || FREEINK_DEVICE_MURPHY_M4 || \
FREEINK_DEVICE_EEGO_A4)
FREEINK_DEVICE_EEGO_A4 || FREEINK_DEVICE_ONEPAGE)
#error \
"FreeInk: no device selected. Pass at least one -DFREEINK_DEVICE_<NAME> (X4, X3, X4PRO, X4CLASSIC, M5, MURPHY, DELINK, LILYGO, M5PAPER, STICKY, PAPERMONO, PAPERS3, MURPHY_M4, EEGO_A4) in your build env — see platformio.sample.ini."
"FreeInk: no device selected. Pass at least one -DFREEINK_DEVICE_<NAME> (X4, X3, X4PRO, X4CLASSIC, M5, MURPHY, DELINK, LILYGO, M5PAPER, STICKY, PAPERMONO, PAPERS3, MURPHY_M4, EEGO_A4, ONEPAGE) in your build env — see platformio.sample.ini."
#endif
// Each device belongs to one MCU family; a binary targets exactly one. X3/X4 are
// ESP32-C3; M5 PaperColor/Murphy/de-link/LilyGo are ESP32-S3; M5Paper v1.1 is the
// classic ESP32 (ESP32-D0WDQ6). The three families differ in deep-sleep wakeup,
// SPI peripheral count, and toolchain, so they never share a binary.
// classic ESP32 (ESP32-D0WDQ6); OnePage is ESP32-C61. The families differ in
// deep-sleep wakeup, SPI peripheral count, and toolchain, so they never share a binary.
#define FREEINK_MCU_C3 (FREEINK_DEVICE_X3 || FREEINK_DEVICE_X4)
#define FREEINK_MCU_C61 (FREEINK_DEVICE_ONEPAGE)
#define FREEINK_MCU_S3 \
(FREEINK_DEVICE_M5 || FREEINK_DEVICE_MURPHY || FREEINK_DEVICE_DELINK || FREEINK_DEVICE_LILYGO || \
FREEINK_DEVICE_STICKY || FREEINK_DEVICE_X4PRO || FREEINK_DEVICE_X4CLASSIC || FREEINK_DEVICE_PAPERMONO || \
FREEINK_DEVICE_PAPERS3 || FREEINK_DEVICE_MURPHY_M4 || FREEINK_DEVICE_EEGO_A4)
#define FREEINK_MCU_ESP32 (FREEINK_DEVICE_M5PAPER)
#if (FREEINK_MCU_C3 + FREEINK_MCU_S3 + FREEINK_MCU_ESP32) != 1
#if (FREEINK_MCU_C3 + FREEINK_MCU_C61 + FREEINK_MCU_S3 + FREEINK_MCU_ESP32) != 1
#error \
"FreeInk: all selected devices must share one MCU family — ESP32-C3 (X3/X4), ESP32-S3 (M5/Murphy/de-link/LilyGo/Sticky/X4Pro), or ESP32 (M5Paper). Build one binary per family."
"FreeInk: all selected devices must share one MCU family — ESP32-C3 (X3/X4), ESP32-C61 (OnePage), ESP32-S3 (M5/Murphy/de-link/LilyGo/Sticky/X4Pro), or ESP32 (M5Paper). Build one binary per family."
#endif

// --- 3) Derive panel drivers from the device set -----------------------------
// Sticky reuses SSD1677: its 800x480 panel rides a 24-pin FPC whose GDR/RESE/BS1
// + dual VSH1/VSH2 + external VGH/VGL/VSL/VCOM charge pump is the SSD1677
// application circuit (same controller + resolution as X4 / de-link).
// application circuit (same controller + resolution as X4 / de-link / OnePage).
// X4 Pro is a distinct ESP32-S3 device (NOT the C3 X4): its 800x480 panel may
// use SSD1677, UC8179, or UC8279, recovered from OEM firmware and hardware
// references — see docs/xteink-x4pro-support.md.
#if FREEINK_DEVICE_X4 || FREEINK_DEVICE_DELINK || FREEINK_DEVICE_STICKY || FREEINK_DEVICE_X4PRO || \
FREEINK_DEVICE_X4CLASSIC || FREEINK_DEVICE_MURPHY_M4
FREEINK_DEVICE_X4CLASSIC || FREEINK_DEVICE_MURPHY_M4 || FREEINK_DEVICE_ONEPAGE
#define FREEINK_DRIVER_SSD1677 1
#else
#define FREEINK_DRIVER_SSD1677 0
Expand Down Expand Up @@ -376,6 +380,7 @@ enum class Board : uint8_t {
PaperMono,
M5PaperS3, // ESP32-S3 sibling of M5Paper v1.1: same ED047TC1 glass, no IT8951 — raw parallel via LovyanGFX
EegoA4, // EEGO Reader A4: ESP32-S3, UC8279C 768x552 SPI panel, GSLX680 touch, PCF8563 RTC
OnePage, // OnePage: ESP32-C61, SSD1677 800x480 SPI panel, 4-key ADC ladder + 3 side keys
};

// How the board reports button presses.
Expand All @@ -386,6 +391,7 @@ enum class InputStyle : uint8_t {
DigitalConfirmPowerHold, // confirm click, power hold on a shared GPIO
DigitalFiveKey, // 3 physical GPIO keys + synthesized events (Murphy M3)
DigitalTwoButton, // short up/down; holds synthesize back/confirm/power
OnePageAdcLadder, // OnePage: 4 front keys on GPIO4 ADC ladder + 3 side GPIO keys
};

// Panel controller silicon. Drivers are selected from this at begin().
Expand Down Expand Up @@ -498,6 +504,7 @@ struct InputPins {
int8_t down;
int8_t power;
bool powerActiveHigh; // true = pressed reads HIGH (INPUT_PULLDOWN); false = active-LOW (INPUT_PULLUP)
int8_t adcLadderPin = PIN_UNASSIGNED; // ADC pin for single resistor ladder (e.g. OnePage GPIO4)
};

// Capacitive touch panel description (TouchController::None disables it).
Expand Down Expand Up @@ -1698,6 +1705,44 @@ constexpr BoardProfile XTEINK_X4_CLASSIC = {
{9, 7, 3, 7}, // bezel insets: carried from X4 Pro (same glass), pending measurement
true}; // batteryChargeStatusActiveHigh: GPIO21 STAT driven HIGH while charging

constexpr BoardProfile ONEPAGE = {
Board::OnePage,
"onepage",
InputStyle::OnePageAdcLadder,
DisplayController::SSD1677,
800,
480,
// Display SPI: SCLK 22, MOSI 23, CS 25, DC 8, RST 27, BUSY 29, powerEnable PIN_UNASSIGNED
{22, 23, 25, 8, 27, 29, PIN_UNASSIGNED},
20000000, // displaySpiHz: 20MHz
// MicroSD (shared SPI bus): SCLK 22, MISO 24, MOSI 23, CS 26, powerEnable 27
{22, 24, 23, 26, 27, false, 20000000, true},
// Input: 4-key front ADC ladder on GPIO4 + 3 side GPIO keys (UP=6, DOWN=9, POWER=2)
// {back, confirm, left, right, up, down, power, powerActiveHigh, adcLadderPin}
{PIN_UNASSIGNED, PIN_UNASSIGNED, PIN_UNASSIGNED, PIN_UNASSIGNED, 6, 9, 2, false, 4},
5, // batteryAdc: GPIO5 (ADC1_CH3)
11, // batteryChargeStatus: GPIO11 (LM66200 ST open-drain, low=USB present)
2.0f, // batteryDividerMultiplier
11, // usbDetect: GPIO11 (LM66200 ST)
NO_TOUCH, // touch: no touch
NO_FRONTLIGHT, // frontlight: none
NO_AUDIO, // audio: none
NO_LEDS, // leds: none
NO_FLIP, // orientation
NO_SDMMC, // sdmmc: none (SPI)
NO_GAUGE, // batteryGauge: none (ADC)
NO_MIC, // mic
NO_SENSORS, // sensors
1.0f, // uiScale: 1.0
// Power: latch0, latch1, chargeEnable (GPIO10, active-high)
{PIN_UNASSIGNED, PIN_UNASSIGNED, 10, true},
0, // displayControllerVariant
{0, 0, 0, 0}, // viewableInsets: full 800x480 panel frame
false}; // batteryChargeStatusActiveHigh: false (low = USB present)

static_assert(ONEPAGE.displayWidth / 8 * ONEPAGE.displayHeight == 48000,
"OnePage framebuffer must be 48,000 bytes (800/8 x 480)");

// Largest framebuffer (bytes) over the devices compiled into this build, derived
// from the profiles above. The display facade sizes its static framebuffer to
// this so one binary holds whichever panel is runtime-selected; a single-device
Expand All @@ -1720,12 +1765,15 @@ constexpr uint32_t MAX_FRAMEBUFFER_BYTES = cmax(
FREEINK_DEVICE_PAPERMONO ? panelBytes(PAPER_MONO) : 0u),
cmax(cmax(FREEINK_DEVICE_PAPERS3 ? panelBytes(M5PAPER_S3) : 0u,
FREEINK_DEVICE_MURPHY_M4 ? panelBytes(MURPHY_M4) : 0u),
FREEINK_DEVICE_EEGO_A4 ? panelBytes(EEGO_A4) : 0u))));
cmax(FREEINK_DEVICE_EEGO_A4 ? panelBytes(EEGO_A4) : 0u,
FREEINK_DEVICE_ONEPAGE ? panelBytes(ONEPAGE) : 0u)))));

// Compile-time default device — the profile ACTIVE starts as. With a single
// device in the build this is the only device; with several same-MCU devices it
// is the boot default until the consumer calls selectDevice().
#if FREEINK_DEVICE_PAPERMONO
#if FREEINK_DEVICE_ONEPAGE
constexpr BoardProfile DEFAULT_DEVICE = ONEPAGE;
#elif FREEINK_DEVICE_PAPERMONO
constexpr BoardProfile DEFAULT_DEVICE = PAPER_MONO;
#elif FREEINK_DEVICE_EEGO_A4
constexpr BoardProfile DEFAULT_DEVICE = EEGO_A4;
Expand Down Expand Up @@ -1840,6 +1888,11 @@ inline bool selectDevice(Board which) {
case Board::EegoA4:
ACTIVE = EEGO_A4;
break;
#endif
#if FREEINK_DEVICE_ONEPAGE
case Board::OnePage:
ACTIVE = ONEPAGE;
break;
#endif
default:
return false;
Expand All @@ -1862,6 +1915,7 @@ inline bool isX4Pro() { return ACTIVE.board == Board::XteinkX4Pro; }
inline bool isX4Classic() { return ACTIVE.board == Board::XteinkX4Classic; }
inline bool isPaperMono() { return ACTIVE.board == Board::PaperMono; }
inline bool isEegoA4() { return ACTIVE.board == Board::EegoA4; }
inline bool isOnePage() { return ACTIVE.board == Board::OnePage; }
inline bool hasTouch() { return ACTIVE.touch.controller != TouchController::None; }
inline bool hasHomeKey() { return ACTIVE.touch.hasHomeKey; }
inline bool hasPwmFrontlight() { return ACTIVE.frontlight.gpio != PIN_UNASSIGNED || ACTIVE.frontlight.viaPm1Pwm; }
Expand Down
Loading