Skip to content

android: add iReader Neo 3 Ultra EPD, lights, and HWSurfaceView - #624

Open
CynricXu wants to merge 2 commits into
koreader:masterfrom
CynricXu:feature/ireader-neo3-ultra
Open

CynricXu wants to merge 2 commits into
koreader:masterfrom
CynricXu:feature/ireader-neo3-ultra

Conversation

@CynricXu

@CynricXu CynricXu commented Sep 24, 2026 •

Copy link
Copy Markdown

Add first-party support for the iReader Neo 3 Ultra (RM06L).

This is the launcher half of the device port. A follow-up koreader PR will bump the submodule and add the Lua page-turn menu after this merges.

Device

  • Brand: iReader
  • Model: Neo 3 Ultra
  • Product/device: RM06L*

Changes

  • Identify the device in DeviceInfo
  • EPD controller: official water-ripple via EPDCDevice.nativePostCommand
  • Lights controller: warm/cool channels through sysfs + SystemUI ContentProvider
  • HWSurfaceView so refreshes go through the official Surface path
  • einkPrepareRipple FFI hook in android.lua

Keeping MainActivity device-agnostic

MainActivity is shared by every device, so it must not reference one model's controller directly. The iReader-specific surface and ripple behaviour is exposed as optional EPDInterface methods with neutral defaults:

  • createSurface(context): SurfaceView? → null (falls back to NativeSurfaceView)
  • usesOpaquePixelFormat(): Boolean → false
  • prepareRipple(effect: Int) → no-op

IReaderNeo3EPDController overrides the three, and MainActivity only talks to device.epd. The defaults are exactly what the previous per-device guards returned, so every other controller keeps its behaviour.

Tested on a Neo 3 Ultra (RM06L-CKDW). Frontlight, warmth, and PAGE_H ripple work in portrait and landscape.

This does not change other devices.


This change is?Reviewable

@CynricXu

Copy link
Copy Markdown
Author

KOReader half is koreader/koreader#16114 (Draft). Please merge this launcher PR first; the main-repo PR will bump the submodule to the official merge commit afterwards.

|| MODEL == STR_TOLINO && (DEVICE == "tolino_vision2" || DEVICE == STR_NTX)
-> Id.TOLINO

// iReader Neo 3 Ultra

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sort alphabetically.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. The Id enum is already in the right place; I'll move the detection branch to sit after InkPalm Plus and before JDRead.

Comment on lines +323 to +332
if (DeviceInfo.ID == DeviceInfo.Id.IREADER_NEO3_ULTRA) {
try {
val cls = Class.forName("android.eink.view.HWSurfaceView")
val created = cls.getConstructor(Context::class.java).newInstance(this) as SurfaceView
Log.i(TAG_SURFACE, "Using official HWSurfaceView")
return created
} catch (t: Throwable) {
Log.w(TAG_SURFACE, "HWSurfaceView unavailable, fallback NativeSurfaceView", t)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't think this belongs in this file but could you briefly explain what it's for first?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this device the official PAGE_H ripple is posted with EPDCDevice.nativePostCommand against android.eink.view.HWSurfaceView. KOReader's default NativeSurfaceView is a normal SurfaceView, so the same command never hits the OEM surface and the animation does not run.

That is why the window has to be created as HWSurfaceView (with a fallback if the class is missing). I put the Class.forName call in MainActivity only because that is where the SurfaceView is constructed today.

I agree it should not live as an IREADER_NEO3_ULTRA branch in this file. I'll move construction to IReaderNeo3EPDController (or similar) and have MainActivity call that helper.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, in that case please add that in a comment when you do. It can be very brief, just something like "HWSurfaceView is needed for EPDCDevice.nativePostCommand PAGE_H ripple"

@CynricXu
CynricXu force-pushed the feature/ireader-neo3-ultra branch from 1063dfe to ed5c899 Compare September 27, 2026 02:08
// The following two lines brings SurfaceView to "top" in order for NGL4 refresh to work, should be compatible with other controllers
view?.setZOrderOnTop(true)
view?.holder?.setFormat(PixelFormat.TRANSPARENT)
if (DeviceInfo.ID == DeviceInfo.Id.IREADER_NEO3_ULTRA) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If many devices require new views, maybe instead of a device.needsView flag, imho it would be better to have something like device.viewType = {0: no view, 1: transparent, 2: opaque, and so forth} and change the interface for all the drivers. @Frenzie

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, sounds good. I'm not sure if any other devices would need it though?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically, the new reader product from iReader can reuse this logic if the hardware remains the same. However, I suggest limiting the submission to the Neo3Ultra this time, as I cannot test other devices or know their specific features, nor can I guarantee the effectiveness. If other similar products are added later, only minor modifications for adaptation will be needed. What do you think?

- Detect RM06L as IREADER_NEO3_ULTRA (alphabetical order)
- EPD controller using the OEM HWSurfaceView so the PAGE_H water-ripple
  posted via EPDCDevice.nativePostCommand hits the right surface
- Dual-channel LM3630A frontlight controller (A = warm, B = cool) with
  SystemUI reading-light provider sync
- Expose einkPrepareRipple to Lua
@CynricXu
CynricXu force-pushed the feature/ireader-neo3-ultra branch from ed5c899 to 63c655e Compare September 27, 2026 18:32
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import org.koreader.launcher.device.Device
import org.koreader.launcher.device.epd.IReaderNeo3EPDController

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better not to do it this way as it could lead to poor code style.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think so too. Let me make some adjustments.

MainActivity is shared by every device, so it must not reference a single
model's EPD controller directly. Move createSurface/usesOpaquePixelFormat/
prepareRipple into EPDInterface as optional methods with neutral defaults,
override them in IReaderNeo3EPDController, and call them through device.epd.
The redundant DeviceInfo check inside the controller is dropped: EPDFactory
only instantiates it for the matching device id.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants