pgaskin / NickelMenu

The easiest way to launch scripts, change settings, and run actions on Kobo e-readers.
https://pgaskin.net/NickelMenu
MIT License
591 stars 32 forks source link

Frontlight action #3

Closed pgaskin closed 4 years ago

pgaskin commented 4 years ago

MR:post-3983396

pgaskin commented 4 years ago

There are two options for this action. Firstly, I could use PowerSettings, which will save the state, and is more future-proof, as it will notify anything which needs to know the brightness, and it will work with the automatic lighting. Secondly, I could use FrontLight itself, but that would be the equivalent of changing it behind Nickel's back.

I'll probably go with the first option, but I haven't decided if it should go into the nickel_setting action or if it should go in a separate frontlight one, although I'm leaning towards the second to provide more room for expansion in the future.

Update: It seems I was slightly mistaken about the PowerSettings. I need to use FrontLight with that too.

pgaskin commented 4 years ago

This is now becoming harder to implement than I originally thought due to the fact I'm now having to work around both C++ and Qt in addition to Nickel to try and initialize a FrontLight object.

Update: Oh, and this just made it even more annoying (the FrontLight constructor connects a bunch of signals, and I just segfaulted):

Note: If a QObject has no thread affinity (that is, if thread() returns zero), or if it lives in a thread that has no running event loop, then it cannot receive queued signals or posted events.

But, it may have actually made it slightly easier, as I might be able to (ab)use the main loop's object as a parent.

NiLuJe commented 4 years ago

Yeah, given the number of issues over the years with keeping frontlight status in sync across a bunch of stuff (all because the kernel did not expose a way to read the fl status until Mk.7, only set it), it doesn't surprise me all that much... Good luck ;).

pgaskin commented 4 years ago

I've finally got some of the QObject trickery working:

    QObject *parent = new QObject(nullptr); // we can't just use another object directly, as FrontLight will replace the vtable
    parent->moveToThread(QApplication::instance()->thread()); // the QObject doesn't have a thread to begin with
    NM_ASSERT(parent->thread(), "our parent QObject doesn't have a thread for FrontLight to attach signals to");
    FrontLight *fl = FrontLight_FrontLight(parent); // this will replace the vtable and attach some signals
    NM_ASSERT(fl, "frontlight didn't return anything"); // it's more likely to just segfault in the constructor

It doesn't segfault anymore, but I have to see if it can actually call stuff (although, the destructors working properly is a good sign)...

pgaskin commented 4 years ago

Good luck ;).

Yeah. The only reason I'm still going through with this is because figuring this out will be useful for when I need to call other similar objects (and because it's fun). If it wasn't for that, I'd just defer this feature indefinitely until I have some other need for figuring this sort of thing out.

pgaskin commented 4 years ago

OK, so it seems that does at least work for simple getters, which means I haven't messed up the memory layout:

    NM_RETURN_OK(nm_action_result_toast("test: %d", (reinterpret_cast<int(*)(FrontLight*)>(dlsym(RTLD_DEFAULT, "_ZNK10FrontLight11temperatureEv"))(fl))));

Now I just have to figure out which int param is which (and the temperature bool param) for _ZN10FrontLight14setTemperatureEibi and _ZNK10FrontLight13setBrightnessEii.

Update: It seems to find the correct ChannelMixer in its own vtable, then forward the function call to it. One of the params (I'll have to do some math with the ChannelMixer assembly to figure it out) is for the animation.

Update: I haven't tested it yet, but the second int parameter seems to be for the animation duration (or 0 for making it instant).

pgaskin commented 4 years ago

Oh, and I don't remember if I've mentioned it before, but for if anyone wants to figure out the vtable addresses for themselves, you need to do the pointer math on the LDR instruction and the ones before it, then resolve that against the relocations and the dynstr table (you can use symdump from kobopatch for that, or you can use readelf).

pgaskin commented 4 years ago

I've managed to get the frontlight changing working. Here's the relevant code (not including the FrontLight init from above):

    if (!strcmp(thingy, "level")) {
        char *end;
        long pct = strtol(value, &end, 10);
        NM_ASSERT(*thingy && !*end && pct >= 0 && pct <= 100, "level (%%) invalid or out of range 0-100: '%s'", value);

        bool (*Device__supportsLight)(Device*);
        reinterpret_cast<void*&>(Device__supportsLight) = dlsym(RTLD_DEFAULT, "_ZNK6Device13supportsLightEv");
        if (Device__supportsLight)
            NM_ASSERT(Device__supportsLight(dev), "device does not have a light");

        FrontLight_setBrightness(fl, (int)(pct), 0);

        void (*PowerSettings__setFrontLightState)(Settings*, bool);
        reinterpret_cast<void*&>(PowerSettings__setFrontLightState) = dlsym(RTLD_DEFAULT, "_ZN13PowerSettings18setFrontLightStateEb");
        NM_ASSERT(PowerSettings__setFrontLightState, "could not dlsym PowerSettings::setFrontLightState");

        void (*PowerSettings__setFrontLightLevel)(Settings*, int);
        reinterpret_cast<void*&>(PowerSettings__setFrontLightLevel) = dlsym(RTLD_DEFAULT, "_ZN13PowerSettings18setFrontLightLevelEi");
        NM_ASSERT(PowerSettings__setFrontLightLevel, "could not dlsym PowerSettings::setFrontLightLevel");

        PowerSettings__setFrontLightState(settings, pct != 0);
        vtable_ptr(settings) = vtable_target(PowerSettings_vtable);

        if (pct) {
            PowerSettings__setFrontLightLevel(settings, (int)(pct));
            vtable_ptr(settings) = vtable_target(PowerSettings_vtable);
        }
    }

Update: I have to fix the case when the level is 0, as it segfaults.

Update: The segfault is caused by a race condition from me calling FrontLight destructor at the end of the action, as it needs to remain valid for the duration of the animation (which still happens when the duration is 0) so stop() can be called at the end. I'll have to not destroy the object, but I don't know how I'll clean it up then ... I can't do it from another thread due to QObject limitations. I guess I'll just not destroy it for now, as it doesn't do any cleanup except deallocating the memory used by the FrontLight QObject and the QTimer (which isn't all that much). Unfortunately, this means we'll have the first unbounded (but small, it won't make a difference in practice unless someone uses this action tens of thousands of times at least) memory leak :disappointed: until I figure out a way to deal with this (which won't be until after v0.0.2).

May  7 13:05:18 hindenburg: OH THE HUMANITY!
May  7 13:05:18 hindenburg: pid: 711, tid: 711 (nickel), rev: 8bb853a72a7c209047baedb2ef5b3c38eebf45e5   >>> /usr/local/Kobo/nickel <<<
May  7 13:05:18 hindenburg: signal 7 (SIGBUS), code 1 (BUS_ADRALN), fault addr 6574696d
May  7 13:05:18 hindenburg:  r0 0177b860  r1 2eb1dda8  r2 00000000  r3 00000001
May  7 13:05:18 hindenburg:  r4 6574696d  r5 0155cfd0  r6 0177b860  r7 7ee24a58
May  7 13:05:18 hindenburg:  r8 69666974  r9 4141413d  10 72655320  fp 2b979e39
May  7 13:05:18 hindenburg:  ip 2e7b9309  sp 7ee24a58  lr 2e7b933f  pc 2e82b962  cpsr 600e0030
May  7 13:05:19 hindenburg:     #00 sp: 0x7ee24a58 ip: 0x2e82b962  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtCore.so.4.6.2: _ZN15QtSharedPointer20ExternalRefCountData9getAndRefEPK7QObject+0x15
May  7 13:05:19 hindenburg:     #01 sp: 0x7ee24a68 ip: 0x2e7b933f  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtCore.so.4.6.2: _ZN18QAbstractAnimation4stopEv+0x36
May  7 13:05:19 hindenburg:     #02 sp: 0x7ee24a98 ip: 0x2b9771ef  /usr/local/Kobo/libnickel.so.1.0.0: _ZN12ChannelMixer23makeBrightnessAnimationEiii+0x26
May  7 13:05:19 hindenburg:     #03 sp: 0x7ee24b20 ip: 0x2b977def  /usr/local/Kobo/libnickel.so.1.0.0: _ZN12ChannelMixer7turnOffEb+0x52
May  7 13:05:19 hindenburg:     #04 sp: 0x7ee24b88 ip: 0x300563e5  /usr/local/Kobo/imageformats/libnm.so: nm_action_frontlight+0x520
May  7 13:05:19 hindenburg:     #05 sp: 0x7ee24c68 ip: 0x30053b87  /usr/local/Kobo/imageformats/libnm.so: _ZNSt17_Function_handlerIFvbEZ13_nm_menu_hookEUlbE_E9_M_invokeERKSt9_Any_datab+0xfa
May  7 13:05:19 hindenburg:     #06 sp: 0x7ee24cc8 ip: 0x300547bb  /usr/local/Kobo/imageformats/libnm.so: _ZN9QtPrivate18QFunctorSlotObjectISt8functionIFvbEELi1ENS_4ListIIbEEEvE4implEiPNS_15QSlotObjectBaseEP7QObjectPPvPb+0x52
May  7 13:05:19 hindenburg:     #07 sp: 0x7ee24ce8 ip: 0x2e965535  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtCore.so.4.6.2: _ZN11QMetaObject8activateEP7QObjectiiPPv+0x1dc
May  7 13:05:19 hindenburg:     #08 sp: 0x7ee24d98 ip: 0x2dcfbfd1  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN7QAction9triggeredEb+0x24
May  7 13:05:19 hindenburg:     #09 sp: 0x7ee24dc0 ip: 0x2dcfde0b  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN7QAction8activateENS_11ActionEventE+0x9a
May  7 13:05:19 hindenburg:     #10 sp: 0x7ee24dd8 ip: 0x2b71b84f  /usr/local/Kobo/libnickel.so.1.0.0: _ZN22AbstractMenuController10tapGestureEP15GestureReceiverP10TapGesture+0xd6
May  7 13:05:19 hindenburg:     #11 sp: 0x7ee24e18 ip: 0x2b6f40c3  /usr/local/Kobo/libnickel.so.1.0.0: _ZN15GestureReceiver21sendGestureToDelegateEP13QGestureEventP15GestureDelegate+0xd6
May  7 13:05:19 hindenburg:     #12 sp: 0x7ee24e38 ip: 0x2dd00b63  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN19QApplicationPrivate13notify_helperEP7QObjectP6QEvent+0x56
May  7 13:05:19 hindenburg:     #13 sp: 0x7ee24e50 ip: 0x2dd06569  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN12QApplication6notifyEP7QObjectP6QEvent+0xff0
May  7 13:05:19 hindenburg:     #14 sp: 0x7ee24fb0 ip: 0x2b987f87  /usr/local/Kobo/libnickel.so.1.0.0: _ZN18Nickel3Application6notifyEP7QObjectP6QEvent+0x32
May  7 13:05:19 hindenburg:     #15 sp: 0x7ee24fe0 ip: 0x2e944729  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtCore.so.4.6.2: _ZN16QCoreApplication14notifyInternalEP7QObjectP6QEvent+0x84
May  7 13:05:19 hindenburg:     #16 sp: 0x7ee25010 ip: 0x2dd3c3b5  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN18QGestureRecognizer20unregisterRecognizerEN2Qt11GestureTypeE+0x3224
May  7 13:05:19 hindenburg:     #17 sp: 0x7ee250e0 ip: 0x2dd3d1f7  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN18QGestureRecognizer20unregisterRecognizerEN2Qt11GestureTypeE+0x4066
May  7 13:05:19 hindenburg:     #18 sp: 0x7ee251a8 ip: 0x2dd3dad5  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN18QGestureRecognizer20unregisterRecognizerEN2Qt11GestureTypeE+0x4944
May  7 13:05:19 hindenburg:     #19 sp: 0x7ee251f0 ip: 0x2dd05619  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN12QApplication6notifyEP7QObjectP6QEvent+0xa0
May  7 13:05:19 hindenburg:     #20 sp: 0x7ee25350 ip: 0x2b987f87  /usr/local/Kobo/libnickel.so.1.0.0: _ZN18Nickel3Application6notifyEP7QObjectP6QEvent+0x32
May  7 13:05:19 hindenburg:     #21 sp: 0x7ee25380 ip: 0x2e944729  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtCore.so.4.6.2: _ZN16QCoreApplication14notifyInternalEP7QObjectP6QEvent+0x84
May  7 13:05:19 hindenburg:     #22 sp: 0x7ee253b0 ip: 0x2dd0799f  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN19QApplicationPrivate22translateRawTouchEventEP7QWidgetP12QTouchDeviceRK5QListIN11QTouchEvent10TouchPointEEm+0x996
May  7 13:05:19 hindenburg:     #23 sp: 0x7ee25470 ip: 0x2dd4b751  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZThn8_NK7QWidget6metricEN12QPaintDevice17PaintDeviceMetricE+0x2e38
May  7 13:05:19 hindenburg:     #24 sp: 0x7ee254f0 ip: 0x2dd00b63  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN19QApplicationPrivate13notify_helperEP7QObjectP6QEvent+0x56
May  7 13:05:19 hindenburg:     #25 sp: 0x7ee25508 ip: 0x2dd0599b  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtWidgets.so.4.6.2: _ZN12QApplication6notifyEP7QObjectP6QEvent+0x422
May  7 13:05:19 hindenburg:     #26 sp: 0x7ee25668 ip: 0x2b987f87  /usr/local/Kobo/libnickel.so.1.0.0: _ZN18Nickel3Application6notifyEP7QObjectP6QEvent+0x32
May  7 13:05:19 hindenburg:     #27 sp: 0x7ee25698 ip: 0x2e944729  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtCore.so.4.6.2: _ZN16QCoreApplication14notifyInternalEP7QObjectP6QEvent+0x84
May  7 13:05:19 hindenburg:     #28 sp: 0x7ee256c8 ip: 0x2e4cd4cb  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtGui.so.4.6.2: _ZN22QGuiApplicationPrivate17processTouchEventEPN29QWindowSystemInterfacePrivate10TouchEventE+0xc9e
May  7 13:05:19 hindenburg:     #29 sp: 0x7ee25830 ip: 0x2e4cf149  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtGui.so.4.6.2: _ZN22QGuiApplicationPrivate24processWindowSystemEventEPN29QWindowSystemInterfacePrivate17WindowSystemEventE+0x18c
May  7 13:05:19 hindenburg:     #30 sp: 0x7ee25868 ip: 0x2e4bd8df  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtGui.so.4.6.2: _ZN22QWindowSystemInterface22sendWindowSystemEventsE6QFlagsIN10QEventLoop17ProcessEventsFlagEE+0x1e
May  7 13:05:19 hindenburg:     #31 sp: 0x7ee25878 ip: 0x2fea6717  /usr/local/Kobo/platforms/libkobo.so: _ZN13QFontEngineFT19alphaRGBMapForGlyphEj6QFixedRK10QTransform+0xf3a
May  7 13:05:19 hindenburg:     #32 sp: 0x7ee25888 ip: 0x2e94306b  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtCore.so.4.6.2: _ZN10QEventLoop4execE6QFlagsINS_17ProcessEventsFlagEE+0xe6
May  7 13:05:19 hindenburg:     #33 sp: 0x7ee258c0 ip: 0x2e947f49  /usr/local/Trolltech/QtEmbedded-4.6.2-arm/lib/libQtCore.so.4.6.2: _ZN16QCoreApplication4execEv+0x58
May  7 13:05:19 hindenburg:     #34 sp: 0x7ee258f0 ip: 0x0001e01b  /usr/local/Kobo/nickel: main+0xcae
May  7 13:05:19 hindenburg:     #35 sp: 0x7ee25ab0 ip: 0x2fb75335  /lib/libc-2.11.1.so: __libc_start_main+0x9c
May  7 13:05:19 hindenburg:     #36 sp: 0x7ee25bf8 ip: 0x0001e595  /usr/local/Kobo/nickel: _start+0x20
NiLuJe commented 4 years ago

Stupid workaround idea: spinning up a new detached thread and doing all of it in there, which would possibly allow you to wait as long as the needed animation timeout before calling the destructor?

pgaskin commented 4 years ago

I just found an unusual issue with setTemperature. Because of something to do with how I'm calling it, it works, but seems to corrupt something and segfault after a short while (the stack trace is useless).

Update: This log line appears right before it segfaults: May 7 14:15:21 nickel: ( 268.586 @ 0x30de308 / frontlight.Popup.debug) void FrontLightPopup::onFrontLightChanged() front light is changed to 100 , temperature: 6400, which implies that the event handler wants something I'm not giving it. This at least gives me something to go off. Interestingly, it doesn't happen with the normal frontlight change.

Update: I've narrowed down the issue further, and it only seems to happen when something triggers the popup to receive the update, and only after a temperature change. I still don't know why it happens. It might be related to the fact that the slider only automatically updates when changing the brightness (it reads it every time the popup is opened), but not for the temperature (I'll have to see where it gets it from).

Update: This snippet from the event may be relevant (note that r8 >= r0 is just an optimized version of r8 && !r0 where both are bools):

    Device::getCurrentDevice();
    Settings::Settings();
    r4 = *0x112e55c;
    *(r7 + 0x14) = r4 + 0x8;
    r8 = PowerSettings::autoColorEnabled();
    *(r7 + 0x14) = r4 + 0x8;
    Settings::~Settings();
    r0 = ValueDisplaySlider::held();
    if (r8 >= r0) {
            ValueDisplaySlider::slider();
            FrontLight::temperature();
            r0 = QAbstractSlider::setValue();
    }
    return r0;

Update: the temperature is stored as a field in the class, rather than being read from the hardware.

Stupid workaround idea: spinning up a new detached thread and doing all of it in there, which would possibly allow you to wait as long as the needed animation timeout before calling the destructor?

That's a good idea! I think that might work. The only possible issue I see is that it makes it hard to do the user-visible errors, but I can probably work around that by passing it a struct of working function pointers.

pgaskin commented 4 years ago

Update: this segfault actually happens for the brightness too ... I've decided not to implement this for now, and not for the next while (unless I suddenly realize why), as it's too intermittently buggy, it will be the most complex action, and I don't think it's worth the hassle. In addition, even if I did manage to get it working, I wouldn't be confident that it works completely, much less that it works on all firmware versions supported by NickelMenu.

pgaskin commented 4 years ago

I'm going to close this issue for now, as I don't see myself working on this in the foreseeable future.