That screen really is too bright sometimes

Since we have chronic light sensitivity as part of our ME/CFS, I decided to investigate whether there is any software way to make the screen of our phone running SailfishOS consistently darker without disabling automatic brightness control.

Unfortunately the minimum allowed by the “Brightness Base Level” slider under SettingsLook and FeelDisplay is still someway too high after all, so there is no user-friendly way to lower it further, but there did turn out to be a hackey way to force a lower brightness response by editing system files which in turn requires enabling root access if you don’t already have that…

Changing the system brightness profile

Note that some of these steps may need to be repeated after a system upgrade.

Getting root access

  1. Enable “Developer Mode” under SettingsSystemDeveloper tools
  2. Set an SSH password, this will be also your root password
  3. Either:
  4. Also enable SSH by checking “Remote connection”, then running ssh defaultuser@<ADDRESS> (where <ADDRESS> is the “WLAN IP address” listed) on your computer’s terminal window and entering the set password (recommended if you have SSH on a computer as using the “Terminal” app is pretty annoying)
  5. Close settings and launch the “Terminal” from the app drawer
  6. Enter devel-su on the computer with SSH logged in or in the “Terminal” app
  7. Type in the password set in step 2
  8. Make sure nano is installed by typing in pkcon install nano and pressing enter

There is also an official help guide with pictures for this at https://docs.sailfishos.org/Support/Help_Articles/Enabling_Developer_Mode/

Replacing brightness profiles

System brightness profiles are stored at /etc/mce/20als-defaults.ini on the device (ALS = ambient light sensor). Since you probably already selected the lowest profile in system settings and its still too bright, we have to update the Ambient Light level to screen brightness mapping to be even less sensitive.

  1. Run nano /etc/mce/20als-defaults.ini to edit the MCE Ambient Light Sensor (ALS) preset/defaults file.
  2. Use the arrow keys to navigate to the end of the [BrightnessDisplay] section (just above [BrightnessLed]).
  3. Remove all the existing lines starting with LimitsProfile… and LevelsProfile… by going to the start of the line and repeatedly pressing Ctrl+K (cut line)
  4. Copy and paste the following lines containing the updated profiles:

    LimitsProfile0=1;2;4;6;11;19;34;61;109;195;350;511;608;724;862;1026;1221;1454;1731;2060;2453
    LevelsProfile0=1;1;1;1;1;1;1;1;1;1;1;10;20;30;40;50;60;70;80;90;100
    
    LimitsProfile1=1;2;4;6;11;19;34;61;109;195;350;490;584;695;827;985;1172;1396;1662;1978;2355
    LevelsProfile1=1;1;1;1;1;2;2;2;3;3;4;14;24;33;43;52;62;72;81;91;100
    
    LimitsProfile2=1;2;4;6;11;19;34;61;109;195;350;471;560;667;794;945;1125;1340;1595;1899;2261
    LevelsProfile2=1;1;2;2;3;4;5;5;6;7;8;18;27;36;45;54;64;73;82;91;100
    
    LimitsProfile3=1;2;4;6;11;19;34;61;109;195;350;452;538;640;762;908;1080;1286;1531;1823;2170
    LevelsProfile3=1;2;3;4;5;6;7;8;9;10;12;21;30;39;48;56;65;74;83;92;100
    
    LimitsProfile4=1;2;4;6;11;19;34;61;109;195;350;434;516;615;732;871;1037;1235;1470;1750;2083
    LevelsProfile4=1;2;4;5;7;8;10;11;13;14;16;25;33;42;50;58;67;75;84;92;100
    
    LimitsProfile5=1;2;4;6;11;19;34;61;109;195;350;416;496;590;703;836;996;1185;1411;1680;2000
    LevelsProfile5=1;3;5;7;9;10;12;14;16;18;20;28;36;44;52;60;68;76;84;92;100
    
    LimitsProfile6=1;2;4;6;11;19;34;61;109;195;350;400;476;567;674;803;956;1138;1355;1613;1920
    LevelsProfile6=2;4;6;8;10;13;15;17;19;21;24;32;40;47;55;62;70;78;85;93;100
    
    LimitsProfile7=1;2;4;6;11;19;34;61;109;195;350;384;457;544;648;771;918;1092;1301;1548;1843
    LevelsProfile7=2;5;7;10;12;15;17;20;22;25;28;36;43;50;57;64;72;79;86;93;100
    
    LimitsProfile8=1;2;4;6;11;19;34;61;109;195;350;368;439;522;622;740;881;1049;1249;1486;1769
    LevelsProfile8=2;5;8;11;14;17;20;23;26;29;32;39;46;53;60;66;73;80;87;94;100
    
    LimitsProfile9=1;2;4;6;11;19;34;61;109;195;350;354;421;501;597;710;846;1007;1199;1427;1699
    LevelsProfile9=3;6;9;13;16;19;22;26;29;32;36;43;49;56;62;68;75;81;88;94;100
    
    LimitsProfile10=1;2;4;6;11;19;34;61;109;195;350;340;404;481;573;682;812;967;1151;1370;1631
    LevelsProfile10=3;7;10;14;18;21;25;29;32;36;40;46;52;58;64;70;76;82;88;94;100
    
    LimitsProfile11=1;2;4;6;11;19;34;61;109;195;350;326;388;462;550;655;779;928;1105;1315;1566
    LevelsProfile11=4;8;12;16;20;24;28;32;36;40;44;50;56;61;67;72;78;84;89;95;100
    
    LimitsProfile12=1;2;4;6;11;19;34;61;109;195;350;313;373;443;528;629;748;891;1060;1262;1503
    LevelsProfile12=4;8;13;17;21;26;30;34;39;43;48;54;59;64;69;74;80;85;90;95;100
    
    LimitsProfile13=1;2;4;6;11;19;34;61;109;195;350;300;358;426;507;603;718;855;1018;1212;1443
    LevelsProfile13=4;9;14;18;23;28;33;37;42;47;52;57;62;67;72;76;81;86;91;96;100
    
    LimitsProfile14=1;2;4;6;11;19;34;61;109;195;350;288;343;409;487;579;690;821;977;1163;1385
    LevelsProfile14=5;10;15;20;25;30;35;40;45;50;56;61;65;70;74;78;83;87;92;96;100
    
    LimitsProfile15=1;2;4;6;11;19;34;61;109;195;350;277;330;392;467;556;662;788;938;1117;1330
    LevelsProfile15=5;10;16;21;27;32;38;43;49;54;59;64;68;72;76;80;84;88;92;96;100
    
    LimitsProfile16=1;2;4;6;11;19;34;61;109;195;350;266;316;377;448;534;636;757;901;1072;1276
    LevelsProfile16=5;11;17;23;29;34;40;46;52;58;64;68;72;75;79;82;86;90;93;97;100
    
    LimitsProfile17=1;2;4;6;11;19;34;61;109;195;350;255;304;362;430;512;610;726;865;1029;1225
    LevelsProfile17=6;12;18;24;30;37;43;49;55;61;68;72;75;78;81;84;88;91;94;97;100
    
    LimitsProfile18=1;2;4;6;11;19;34;61;109;195;350;245;292;347;413;492;586;697;830;988;1176
    LevelsProfile18=6;13;19;26;32;39;45;52;58;65;72;75;78;81;84;86;89;92;95;98;100
    
    LimitsProfile19=1;2;4;6;11;19;34;61;109;195;350;235;280;333;397;472;562;669;797;949;1129
    LevelsProfile19=6;13;20;27;34;41;48;55;62;69;76;79;81;84;86;88;91;93;96;98;100
    
    LimitsProfile20=1;2;4;6;11;19;34;61;109;195;350;226;269;320;381;453;540;643;765;911;1084
    LevelsProfile20=7;14;21;29;36;43;50;58;65;72;80;82;84;86;88;90;92;94;96;98;100
    
  5. Press Ctrl+X, then Y, then Enter to save and close the editor.

  6. Enter systemctl restart mce or just restart your device to load the new values.

This replaces the default profiles with ones that starting from about the same maximum brightness curves will go continously lower with all settings below about 24% in setting selecting curves that were previously unreachable.

Background

How I found about this

TL;DR: Some educated guesswork, intimiate Linux system knowledge and a little help from the MCE source code.

To find out which SailfishOS system component was responsible for managing the backlight, I knew I had find which program writes to /sys/class/backlight/<DEVNAME> (the Linux kernel interface for controlling backlight devices). So after looking up how to find our which process writes to a file, I compiled fanotify-example on the device using gcc (GNU C Compiler), then ran it as ./fanotify-example /sys/class/backlight/panel0-backlight. This revealed that the brightness value of the main panel is controlled by /usr/sbin/mce (MCE).

Knowing that it is customary for Linux system services to have configuration files in /etc, I then checked the directory /etc/mce and found a file named 20als-defaults.ini in there, which I correctly guessed would stand for Ambient Light Sensor defaults.

Given that that file is both documented and rather obvious I than started looking for a way to select a different display backlight profile. For this I played around a bit with the mce command and found a debug logging switch that revealed that there are apparently some “GConf” values influencing its operation in addition to the files at /etc/mce and that it reads the file /var/lib/mce/builtin-gconf.values to find these values. The name GConf indeed turned out related to the old GNOME 2.x configuration system, but GConf isn’t actually used anymore. It’s just that MCE contains a GConf compatible configuration parser, which made assume that nothing in the system apparently ever writes that format anymore – this turned out to be a wrong assumption as that file is actually rewritten by system settings when you update any system-wide values.

Since I assumed that that file is shipped by SailfishOS during installation, I looked for information on what it does and learned from the manpage at https://github.com/sailfishos/mce/blob/91198feec76f1069ed7b38f519e9a0f7852ed358/man/mce.8#L89 (that isn’t actually shipped in SailfishOS itself) that the “GConf key” /system/osso/dsm/display/display_brightness controls the current brightness profile used, which let me to believe I just need to change that value to use a different brightness curve – while one can indeed change that file to load a different curve, one can also just do so in system settings.

Finally, I had to figure out how to generate dimmer curves since even the default 0 curve is still too bright. This involved playing around with the existing values in a Python shell and trying different binary operator on them to hopefully try and spot the pattern. Plotting the existing curves was quite helpful for this once I decided to finally do that, since you can see discontinuities, trends and repetitions much better than when just starting at the numbers.

How I generated the extra curve values

Based on analysis I wrote the following Python code to generate some additional dimmer curves, but latter realized that it was impossible to load them:

limprf = lambda max: f"1;2;4;6;11;19;34;61;109;195;350;{';'.join(str(round(max * 0.84**p)) for p in range(9, -1, -1))}"
lvlprf = lambda mid: f"{';'.join(str(max(floor(mid / 11 * i), 1)) for i in range(1, 11 + 1))};{';'.join(str(ceil(((100 - mid) / 10 * i) + mid)) for i in range(1, 10 + 1))}"
def gen_curves(n):
    for i in range(1, n + 1):
        print()
        print(f"LimitsProfile{20 + i}={limprf(2000 / (0.966 ** i))}")
        print(f"LevelsProfile{20 + i}={lvlprf(20 - (3 * i))}")
gen_curves(9)

Once they didn’t load, I begrudinginly dug through the available source code and found that the system is actually limited to 21 curves (0–20), so instead I generated some modified curves (as showcased in the main part of the article) that start lower and end at the same brightness level:

limprf = lambda max: f"1;2;4;6;11;19;34;61;109;195;350;{';'.join(str(round(max * 0.84**p)) for p in range(9, -1, -1))}"
lvlprf = lambda mid: f"{';'.join(str(max(floor(mid / 11 * i), 1)) for i in range(1, 11 + 1))};{';'.join(str(ceil(((100 - mid) / 10 * i) + mid)) for i in range(1, 10 + 1))}"
for i in range(0, 20 + 1):
    print()
    print(f"LimitsProfile{i}={limprf(2000 * (0.96 ** (i - 5)))}")
    print(f"LevelsProfile{i}={lvlprf(20 + (4 * (i - 5)))}")

In any case, these just extrapolates the same trends found in the pre-existing curves, so I cannot really explain why exactly those values, but apparently they work. Oh well. 🤷🏽