nekr0z / matebook-applet

System tray applet/control app for Huawei Matebook
GNU General Public License v3.0
142 stars 9 forks source link

Support MacOS #26

Open nekr0z opened 3 years ago

nekr0z commented 3 years ago

People are running MacOS on Matebooks, and it looks like matebook-applet would be welcome there.

It should be relatively easy to add MacOS-specific endpoints and have them probed when running the applet on MacOS. All our dependencies support MacOS, so there shouldn't be a problem.

Two things prevent me from implementing this straight away:

  1. Their current way of doing things doesn't allow to set arbitrary thresholds. Setting arbitrary thresholds is more versatile and is our preferred way of interacting with endpoints. It can be worked around, but I'd prefer not to if there's a way.
  2. It's unclear how to get the currently set values on MacOS.
profzei commented 3 years ago

Here I am. As I said, I am very happy to collaborate with you. Thank you very much for your willingness!

Their current way of doing things doesn't allow to set arbitrary thresholds. Setting arbitrary thresholds is more versatile and is our preferred way of interacting with endpoints. It can be worked around, but I'd prefer not to if there's a way.

I have to do some tests on the script created by @ldan93 but, in my opinion, it should be possible to set arbitrary thresholds.

It's unclear how to get the currently set values on MacOS.

@ldan93 uses the following steps:

  1. loading (i.e. injecting during boot phase) a sort of library/driver (created mainly for debugging purpouses) called kext on macOS i.e. ACPI_Debug.kext
  2. loading a SSDT-RMDT.aml file during boot phase which is a file with user-defined methods in ACPI language for interacting with ACPI_Debug.kext
  3. using ioio utility for calling those user-defined methods declared in SSDT-RMDT.aml file

I think it should be possible pass some arbitrary values to ioio utility -> user-defined methods in SSDT-RMDT.aml file

nekr0z commented 3 years ago

in my opinion, it should be possible to set arbitrary thresholds.

From what I have understood, there's no straightforward way (using that script) to set, for example, 35-75 or 56-71 (i.e. totally arbitrary values for both high and low as long as low<high). Having such a possibility would be ideal, and make the applet adaptation much less of a hassle.

it should be possible pass some arbitrary values to ioio utility -> user-defined methods in SSDT-RMDT.aml file

I need to read into all of this more, I'm totally unfamiliar with the whole ioio deal, or even with how MacOS talks to ACPI in the first place.

Basically, here's what I have in mind as an algorithm:

  1. The applet issues a command that returns the current battery limit values. This can be basically a command you'd put into a terminal to read the results from the same terminal. It can be complicated, if needed, involve more than one step and such. This command is the first thing that needs to be figured out.
  2. The applet parses what it got from that command, and either figures out the current settings (and can display it for the user) or decides that what it received doesn't make sense and maybe the user didn't install all the necessary utilities or hasn't loaded the necessary modules at boot-time. In the latter case the applet can throw a error message and advise the user to go read some manuals and install the required things.
  3. At the user's command, the applet sets the required limits, and goes back to 1. to check that the limits have been set as asked.

Actually, this is how it currently works on Linux. Figuring out the command (set of commands) for 1. is the first step (and half the job, really).

ldan93 commented 3 years ago

Hello @nekr0z and @profzei Porting matebook-applet to MacOs would be very very nice, thank you for your implication ! The current implementation is very hacky but I'm sure there are some workarounds. Here are some thoughts :

  1. About setting arbitrary thresholds : there is a way. We can write an ACPI method that takes the thresholds as an argument and then sets them. The only limitation is that you can only pass one argument to an ACPI method with ioio. The most convenient way would be to directly send the whole argument used by the \SBTT method (ie. XXYY0000 where XX is the max value and YY the min value coded in hexadecimal)
  2. About getting the current values : it's quite tricky. Currently, what my basic script does is : request the values through an ACPI call, and then the values are written in the system logs of MacOS. Theoretically, we might be able to extract the values from the terminal by parsing the system log file, but this is not ideal. I'm sure there is a more conventional way of getting EC/ACPI values in MacOS, but I don't know how... We might ask for help in the ACPI-Debug's thread on Tonymacx86...
nekr0z commented 3 years ago

send the whole argument used by the \SBTT method (ie. XXYY0000 where XX is the max value and YY the min value coded in hexadecimal)

Perfect, we'll do that.

we might be able to extract the values from the terminal by parsing the system log file, but this is not ideal.

Yep, I've already toyed with this idea. Doable, but too many moving parts, I'd prefer some other alternative.

ldan93 commented 3 years ago

So, I've written a basic ACPI Method (DGB5) in SSDT-RMDT.aml

It takes the XXYY0000 argument as an input. In terminal, you must type in : ./ioio -s org_rehabman_ACPIDebug dbg5 ARG Where ARG is XXYY0000 converted to a decimal number (because ioio expects a decimal number) Ex : if you want to set the thresholds to 60-80 (so 3C-50 in hex) --> the SBTT method needs to receive "0x503C0000" --> this is 1346109440 in decimal : ./ioio -s org_rehabman_ACPIDebug dbg5 1346109440

nekr0z commented 3 years ago

./ioio -s org_rehabman_ACPIDebug dbg5 ARG

Is there a reasonable expectation of where ioio is on the system? ./ requires cd to that directory, which is not straightforward to do for a GUI app ;-)

ldan93 commented 3 years ago

Well, currently I put ioio in /Applications/Utilities. But I'm thinking it might be better to put it in the /usr/local/bin folder. So, this way the ioio command could be invoked system-wide no matter the current active directory. What do you think ?

nekr0z commented 3 years ago

I think if we can't really rely on ioio being accessible from some standard place (or PATH, or whatever), I'd better figure out how to talk to kext directly. ;-)

ldan93 commented 3 years ago

Well, ioio is basically a binary that must be downloaded and manually copied somewhere by the user. It's not installed through a standardised package like in a Linux environment. But that's not really a problem, since we can tell users where to put the binary in @profzei documentation. Or maybe you could package ioio with matebook applet ?

About avoiding using ioio : well, after some research, I truly think this utility is the most convenient way of calling ACPI method from user-space, and more generally it is precisely designed to interact with kernel extensions. But if you find a better option, that's perfect !

ldan93 commented 3 years ago

About getting the current thresholds, I've investigated the process of parsing system logs. Even if it's not the best path, this is achievable with this script I wrote :

#!/bin/sh
#  threshold.sh
log stream --predicate 'senderImagePath contains "ACPIDebug"' | sed -n 's/.*XXYYZZ", \(.*\), "ZZYYXX.*/\1/p' &
sleep 0.2
ioio -s org_rehabman_ACPIDebug dbg4 0
sleep 0.2
killall log
nekr0z commented 3 years ago

OK, I'm getting close to hacking up a crude prototype, but my lack of MacOS knowledge starts showing. Please enlighten me, because I'm kinda lost here.

  1. They do have PATH in OS X, don't they? And I can reasonably expect ioio to be in PATH, right?
  2. OS X is basically BSD and is supposed to have ioreg that knows everything about the current state of hardware-related variables. Could we get thresholds from ioreg?
profzei commented 3 years ago

They do have PATH in OS X, don't they? And I can reasonably expect ioio to be in PATH, right?

Positive answer! (we can put ioio into PATH)

Could we get thresholds from ioreg?

I tried to look at into my ioreg but I didn't find any useful info about battery thresholds maybe because I didn't look for the "right entry"

nekr0z commented 3 years ago

Positive answer! (we can put ioio into PATH)

That's good. Makes it actually feasible to use ioio, at least for a start. I've found a library that supposedly allows talking to KEXT directly, but since I know very little about KEXT and can't really debug things without running OS X on my Matebook (which I'm not planning to do), the debugging would be extremely hard.

I tried to look at into my ioreg but I didn't find any useful info about battery thresholds

Would you mind patching your ioreg output to a text file (i.e. ioreg > ioreg.txt) and letting me have a look? It will only give one of the planes by default, but the ACPI devices are likely to be shown...

In other news: I've been able to compile matebook-applet on OS X and make it run. Cross-compiling graphical applications turns out to be unfeasible, so I requisitioned my wife's old MacBook Air. Unfortunately, it runs very ancient OS X (10.8 or something), which is probably why I'm getting compilation errors and applet mode crashes. The good thing is, windowed mode does run, so I can do some debugging (as much as possible without the actual hardware, that is).

If someone has Go installed, I'd appreciate if you tried to clone this repository, check out the darwin branch, go run build.go, and try to run ./matebook-applet -vv to confirm it indeed does run and puts the icon in the tray (it doesn't do much more on OS X yet) instead of crashing like on the machine I have. :) If, however, you don't have Go installed, don't bother: we'll have a working prototype soon, we'll test it in the windowed mode, and then merge it to master branch and have Travis build it on a decent enough OS X version…

ldan93 commented 3 years ago

On my computer, with macOS 10.15, it builds but it doesn't run/put the icon in the tray... I have attached the build logs and the output of ./matebook-applet -vv : logs.txt.zip

nekr0z commented 3 years ago

On my computer, with macOS 10.15, it builds but it doesn't run/put the icon in the tray... I have attached the build logs and the output of ./matebook-applet -vv : logs.txt.zip

Thank you so very much! It's awesome that you have Go set up, your help in debugging is priceless.

Looks like we're hitting a known issue with the systray library. It only manifests on OS X. Since your behaviour is the same as mine, I can further debug this one by myself.

But first let's get at least something working. I've pushed some new code to the darwin branch. It would be awesome if you could build it (the build log will spit some warnings, don't mind), run ./matebook-applet -vv -w, and show me the output. It would be absolutely great if you used the SSDT-RMDT that you posted before and not the one you've customized for the script: it's the exact output that I'm interested in.

What's supposed to happen is for the applet to do all the steps from threshold.sh up to the point of getting the relevant log line, then clean up by killing the log process. The actual parsing is not yet coded, so the applet will assume battery protection is off and draw the window accordingly.

ldan93 commented 3 years ago

You're welcome ! So I've built the new code and run it with my previous custom SSDT-RMDT (the one without the XXYYZZ stuff). Here's the output : logs2.txt.zip The window is drawn :

Capture d’écran 2021-01-23 à 18 26 49

I've been thinking about something else : I used your applet in Linux before switching to MacOs. I've just remembered that there was an option to tweak Fn-Lock. I definitely think I could quite easily add relevant ACPI methods in the SSDT-RMDT to get and set Fn-Lock state, so we could also implement this later on.

nekr0z commented 3 years ago

@ldan93 thanks again! We're closing in.

I've been trying to nail that systray issue, but with no luck yet. I still have a couple of ideas to try, so not all is lost (yet). For now I've simply disabled the applet mode for OS X (i.e. it's always -w, even it you don't specify it).

I've pushed more code, and would appreciate a final test. If I got it right, it should parse the thresholds, paint a window and even allow setting the thresholds. If I made a mistake somewhere (which is totally possible and actually likely) and something doesn't work, I'd appreciate the -vv output. ;)

nekr0z commented 3 years ago

I definitely think I could quite easily add relevant ACPI methods in the SSDT-RMDT to get and set Fn-Lock state, so we could also implement this later on.

Totally. If battery thresholds work, implementing FnLock control will be a piece of cake. All I need is:

nekr0z commented 3 years ago

Ok, I was finally able to make the applet mode work. Unfortunately, with these libraries on OS X there seems to be no way to have a window drawn and the systray at the same time, so no custom threshold settings for the applet mode, sorry. Should work in the windowed mode, though.

ldan93 commented 3 years ago

Thank you so much !! Seeing this project materialising at such a great pace is so cool !

So, the applet mode is drawn as expected and the applet is able to set the thresholds. Here are some observations :

logs3.txt.zip

nekr0z commented 3 years ago
* still reporting that Battery protection is OFF even though it's not the case

Oops, missed that one. Should be working with the latest code.

* Is this the expected behaviour ?

Not really. :) I've put a 500 ms delay in the latest commit, hope it does the trick. We can always increase it if that's not enough.

Try again?

ldan93 commented 3 years ago
nekr0z commented 3 years ago
  • delay is slightly not enough : the logs are now accurate... 2/3 of the time.

Actually, there used to be the same issue with the linux driver, and we even have code to mitigate it. That has long been obsolete, as the linux driver was fixed by Ayman, but the code is still there. I activated it for OS X, should to the trick.

  • Is there a way of running the applet without having a Terminal window opened ? (Launching the binary from the file explorer or putting it in the startup programs always open a Terminal in the background)

I wouldn't really know, MacOS is not really my realm. ;) From what I gathered so far, I need to make something called an App Bundle, which should be relatively straightforward to do. I was planning on figuring this out as soon as we have the applet really working, so it looks like that's what I'll be doing next.

The plan is to have a build script option to generate that App Bundle thing when building for MacOS.

ldan93 commented 3 years ago

There is a small issue that I forgot to report :

When I wrote my first hacky script, I actually noticed the same strange behaviour : setting thresholds to 0-100 and then reading them would report an unreliable output...

Apart from that, everything is working really nice, thanks again for your wonderful work 👍

nekr0z commented 3 years ago

@ldan93 The applet uses the log trick to report thresholds (as you might have already guessed). Since the log doesn't show the protection is off, the applet can't guess it.

On Linux the driver always reports the thresholds as 0-100 or 0-0 when battery protection is off, and that's how the applet figures it out. We either need to set the thresholds to those values as part of switching the protection off, or else we need some command or a log trick for the applet to probe whether the protection is off.

Currently for setting the thresholds the applet uses your DBG5 method, except for actually for switching it off, which is done by calling your DBG0 method. Looks like the DBG0 method, while turning the protection off, fails to change the thresholds that later get reported in the log. I noticed that it has \SBTT (0x64000000) inside, so it does try to set 0-100. I wonder if trying to set 0-0 instead would do the trick…

ldan93 commented 3 years ago

Setting 0-0 doesn't do the trick...

About the DBG0 method : actually, we should not use this one, this was a very bad implementation I wrote... Because of the "Sleep" function in the ACPI code, it makes the Matebook freeze for one second. Just using the DBG5 method with 0 - 100 values (so the argument would be 1677721600) disables battery protection in a more acceptable way. Would you mind using this way ?

Then, I've done some reverse engineering on the EC registry. I think we've got a way of knowing whether battery protection is OFF or ON :

nekr0z commented 3 years ago
* From my understanding : `0x80` means battery protection if OFF, and `0xc0` means it's ON (I'm not 100% sure, but this has been consistent across many tries)

Nope. Well, maybe your model is an exemption, but on all MateBooks I've seen, including mine, that one is for "charging allowed/disallowed". If your protection is on, it will be 0xc0 when the required level is reached (so that the laptop doesn't charge the battery anymore), and turn into 0x80 as soon as the battery charge drops below the desired level.

ldan93 commented 3 years ago

OK then, this was a bad guess, never mind...

nekr0z commented 3 years ago

Setting 0-0 doesn't do the trick...

You did it manually via DBG5, right? Not via the applet? Because the applet wouldn't really do that for now.

we should not use this one

OK, will change.

ldan93 commented 3 years ago

You did it manually via DBG5, right? Not via the applet? Because the applet wouldn't really do that for now.

Yep ! Setting it to 0 strangely resets the thresholds to 40-70...

nekr0z commented 3 years ago

Yep ! Setting it to 0 strangely resets the thresholds to 40-70...

That's not really strange (for pre-2020 MateBooks), it was a long shot anyway.

I've pushed new code. It doesn't do DBG0 anymore. Please test and confirm it works and turns the protection off still.

Also, the new code allows you to go run build.go -m, which should give you a neat matebook-applet.app bundle. If you open the directory in your Finder, it shows it without the .app extension, but you can tell it from the binary by the icon, hopefully. This bundle can be run without terminal by simply double-clicking, and I suppose you could make it autostart, too (I don't know how it's done in MacOS, though).

ldan93 commented 3 years ago

The new code does turn off the protection as expected.

About the matebook-applet.app : it launches, but the options are missing

Capture d’écran 2021-01-25 à 00 35 14

I've tried to launch the matebook-applet binary bundled inside the .app : it works as expected.

nekr0z commented 3 years ago

The new code does turn off the protection as expected.

But still the applet doesn't know it's turned off, right?

it launches, but the options are missing

Oops, I did not expect this. It looks like the app being launched from within the bundle can't access the log executable. Some quick googling told me that this is indeed expected, a bundled app isn't allowed to execute commands outside of its bundle. This essentially means that unless we find a way to talk to the kext directly, without log and ioio, the bundle will be useless. Which is a shame, because I spent quite some time figuring it all out. Well, there's no such thing as useless knowledge, is there? ;)

For now, you MacOS crowd will need to figure out the way to run the applet without the terminal for yourselves. Googling brings up AppleScript as a possible solution, I don't know how feasible that is.

nekr0z commented 3 years ago

There's actually a dirty hack I came across while googling this matter last night, but it's quite old and I don't know if it can trick the modern system. The idea is to make a symlink to the required application inside the bundle (presumably, it should go in the Content/Resources subdir, but I'm not sure). The applet needs ioio and log, so these are the binaries that need to be symlinked.

ldan93 commented 3 years ago

I've tried the symlink trick inside Content/Ressources and also tried to copy paste the full binaries in this folder : still not working... I don't have a clue about what we could do about this. I will try to investigate...

But still the applet doesn't know it's turned off, right?

I've just checked : it displays an error in the applet (but the battery protection is effectively turned off) :

Capture d’écran 2021-01-25 à 17 54 07

It's possible to set a threshold again despite this error. But, if matebook-applet is switched off without setting a new battery protection mode, at the next launch of the applet it's not possible to use it anymore :

Capture d’écran 2021-01-25 à 00 35 14

logs5.txt.zip

nekr0z commented 3 years ago

I have toyed with the idea of talking directly to kext via the library I've mentioned. I think I could (given enough time and your debugging effort) rewrite ioio in Go to include as part of the applet's driver, but that doesn't solve the log problem.

at the next launch of the applet it's not possible to use it anymore

There are two issues here. First is a bug: the applet wouldn't read a singe-digit 0x6 from the log (you can see in the trace that the value got ignored. That one I've fixed, new code is pushed. The second issue is the fact that your system reports insane values (i.e. max threshold is lower that min). I think the applet (now that I fixed the bug) will still complain, but should work. Give it a try.

nekr0z commented 3 years ago

I will try to investigate...

I can't debug this, unfortunately, but you can try (with symlinks, copying binaries shouldn't work) giving explicit paths to binaries. I'm not really sure how they should be called from within a bundle. I coded simply "ioio" and "log" (so they get executed if they are in PATH), but it could be that from the bundle it should be "Resources/ioio" or "../Resources/ioio", or some such.

The actual calls are in driver-darwin.go file, lines 80 and 83; you may try changing these and rebuilding the bundle. You need to get both commands right for it to matter. Obviously, this would break the standalone executable, but we can statr working around that if you find a working setup.

ldan93 commented 3 years ago

There are two issues here. First is a bug: the applet wouldn't read a singe-digit 0x6 from the log (you can see in the trace that the value got ignored. That one I've fixed, new code is pushed.

It is fixed with the new code 👍

The second issue is the fact that your system reports insane values (i.e. max threshold is lower than min).

I think there is a bug in the implementation of setting a very low threshold : if I try to set a custom threshold in windowed mode, it doesn't work with threshold<16% (same insane values in logs) This would explain the 0-100 problem (which would only be a specific case of this broader bug)

When I manually set the value with DBG5 (ex : 0-100% = 0x64000000 = ioio -s org_rehabman_ACPIDebug dbg5 1677721600, or 10-15% = 0x0F0A0000 =ioio -s org_rehabman_ACPIDebug dbg5 252313600) and then launch the applet, it doesn't report anything weird...

Maybe another problem with single digit hex numbers ?

nekr0z commented 3 years ago

Maybe another problem with single digit hex numbers ?

Great catch! Indeed it was. Fixed, pushed, please test ;)

ldan93 commented 3 years ago

Great catch! Indeed it was. Fixed, pushed, please test ;)

Fixed !

I think I finally found a way of having battery protection OFF reported as 0-100, as in Linux :

From my tests, after using this command, matebook-applet is already able to tell that BP is OFF.

Could you please implement this command ?

And well, after that, apart from the bundled app thing, I guess we've got a 100% working app !! 😃

nekr0z commented 3 years ago

It bypasses SBTT and manually sets the thresholds to 0-100

Hmm... Did you consider rewriting DBG5 so that it bypasses SBTT and sets arbitrary thresholds? ;-) I mean, implementing DBG6 is easy, but I like the code without the corner cases much better.

after that, apart from the bundled app thing, I guess we've got a 100% working app !!

FnLock?

ldan93 commented 3 years ago

Hmm... Did you consider rewriting DBG5 so that it bypasses SBTT and sets arbitrary thresholds? ;-) I mean, implementing DBG6 is easy, but I like the code without the corner cases much better.

You're right, it makes much more sens. I've updated the DBG5 method : switching BP OFF is now functional.

FnLock?

About this : would you like Fn-Lock reading to be done in DBG5 or in a dedicated method ?

nekr0z commented 3 years ago

would you like Fn-Lock reading to be done in DBG5 or in a dedicated method ?

I hope you meant DBG4 ;-)

I can live with either option, but since FnLock is a totally separate thing, it would be more logical to have totally separate methods for it. IMHO.

ldan93 commented 3 years ago

OK, so I've implemented a DBG6 method to get the Fn-Lock state : it returns 0 (default behaviour = priority to brightness/sound/... keys) or 1 (priority to F1-F12 keys) :

        Method (DBG6, 1, Serialized)
        {
            Local0 = \_SB.PCI0.LPCB.EC0.RRAM (0x03E6)
            \RMDT.P2 ("Reading Fn-Lock state :", Local0)
        }

The DBG7 method implements setting Fn-Lock state quite like DBG5 : the argument expected by the \SFRS method inside is a hex in a 0xY0000 format, where : Y = 2 enables Fn-Lock state (priority to brightness/sound/...) and Y = 1 switches it off, back to default behaviour. So that's 0x20000 and 0x10000, so 131072 and 65536 in the ioio command.

        Method (DBG7, 1, Serialized)
        {
            Local0 = Arg0
            \RMDT.P2 ("Value : ", Local0)
            \SFRS (Local0)
            \RMDT.P1 ("Fn-Lock state set")
        }

Please find the relevant logs from log stream.

nekr0z commented 3 years ago

Implemented and pushed. Please test, and please also test that I didn't break the battery functionality while refactoring for this implementation ;-)

ldan93 commented 3 years ago

Everything seems to be perfectly working ! This is so cool :) I will keep testing the applet during the coming days, to see if there are some edge-cases/issues that I didn't spot. In the meantime, I'm still investigating the app bundle stuff, but since I have absolutely zero knowledge about this, don't expect solutions very soon.

ldan93 commented 3 years ago

(What I'm going to say might be total nonsense since I don't really know what I'm talking about :) )

From what I understand, accessing external binaries from a .app ressource should work. It's only in the specific case of a sandboxed app (ex: apps submitted to the AppStore) that external binaries are supposed to be unreachable.

I've done some tests with Automator : If I package matebook-applet binary in an app bundle with Automator, it doesn't work (it launches but can't call external commands). But if I first declare the path variable in Automator's script , then the resulting .app works as expected. Here's my very simple Automator code :

PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
'/Users/ldan93/matebook-applet/matebook-applet'

Do you have the option to explicitly declare the path folder the way I did ?

ldan93 commented 3 years ago

I can also report that launching the .app from Terminal with an explicit var_path option also works : ENV_PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin open -a matebook-applet.app

nekr0z commented 3 years ago

Do you have the option to explicitly declare the path folder the way I did ?

Interesting! I need to dig deeper into this. Not really something that is sensible to do from within the app, but I feel there must be an option in Info.plist for that...

ldan93 commented 3 years ago

I actually tried to set a custom PATH in the .plist, with LSEnvironment ( cf. https://apple.stackexchange.com/a/79845 ) but I had no luck... Maybe there's a parameter to set at compilation time ?