Skip to content
alavrardPublic

About

HTTP API for picture settings (backlight, picture mode, light sensor) on MediaTek Fire TV smart TVs. No root. Tested on Hisense 55U65QF.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PQ Bridge

HTTP API for picture settings (backlight, picture mode, light sensor, …) on MediaTek-based Fire TV smart TVs.

A tiny sideloaded Android app that binds to the TV's own picture-quality service — the same one the Settings UI talks to — and exposes it as JSON over HTTP on port 8765. No root, no ADB at runtime, no UI automation.

$ curl http://192.168.1.50:8765/backlight/30
{"ok":true,"key":"Backlight","requested":"30","before":"51","after":"30"}

Important

Verified on one TV only: Hisense 55" U6 Mini-LED, model 55U65QF (2025), Fire OS 8 (Android 11), MediaTek board codename coyote-ch.

Other Fire TV smart TVs built on MediaTek boards may ship the same service, possibly under different names or transaction codes. If it doesn't work on yours, How this was found walks through finding the right values. Please open an issue with your model either way.


Contents


Quick check: is your TV compatible?

With ADB connected (see below):

adb shell pm list packages | grep customerpq
# package:com.mediatek.customerpq          <- good sign

adb shell getprop ro.product.device
# coyote-ch                                <- identical to the tested set

If com.mediatek.customerpq isn't there, this app won't work as-is, but the investigation steps may still find the equivalent on your set.

Install

1. Enable ADB on a Fire TV smart TV

This differs from Fire TV sticks and most online guides:

  1. The TV must be registered to an Amazon account and online. If it's offline or not signed in, the settings tree renders incomplete and Developer Options never appears, no matter how many times you press. Let it update and reboot first.
  2. Go to Settings → Device & Software → About. On sticks and cubes it's "My Fire TV" instead, which isn't on the TVs.
  3. Highlight the top row (the device name, e.g. "Your TV") and press Select about 7 times, quickly. Slow presses reset the counter.
  4. Press Back once. Developer Options is now in the menu. Turn on ADB debugging.
  5. Note the TV's IP (Settings → Network) and give it a DHCP reservation on your router.

Since early 2024 Fire OS blocks apps on the TV itself from opening local ADB connections, so you need a computer on the same network. After installation, PQ Bridge doesn't use ADB at all.

2. Install the APK

Download pqbridge-<version>.apk from Releases, then:

adb connect <tv-ip>:5555        # accept the prompt on the TV the first time
adb install -r pqbridge-<version>.apk
adb shell am start -n com.pqbridge.app/.MainActivity

The last command is needed once. Android won't deliver the boot broadcast to an app that has never been opened. It also starts the service right away.

From then on it starts on every boot. From a cold power-on it takes about 60–90 s to respond (most of that is Fire OS booting).

curl http://<tv-ip>:8765/health     # {"bound":true}
Uninstall / restart / logs
adb uninstall com.pqbridge.app
adb shell am start-foreground-service com.pqbridge.app/.BridgeService   # restart without the activity
adb logcat -s PqBridge/Service PqBridge/PqClient PqBridge/Http

If you switch between a release APK and your own build, uninstall first: adb install -r refuses to replace an app signed with a different key.

API

All endpoints are GET and return JSON.

Endpoint Does
/ List endpoints
/health {"bound":true} if the binder connection to the PQ service is alive
/state Backlight, Picture_Mode, Light_Sensor, LD_MODE, Color_Temperature, Gamma, Peaking_Brightness, Low_Latency
/get?key=K Read any key
/set?key=K&value=V Write any key, then read it back
/backlight Current backlight (0–100)
/backlight/N or /backlight?value=N Set backlight
/applyall Copy current input's settings to all inputs (see caveats)
curl http://<tv-ip>:8765/state
curl "http://<tv-ip>:8765/set?key=Light_Sensor&value=Off"
curl "http://<tv-ip>:8765/get?key=Picture_Mode"

Always check ok on a set. The TV silently ignores unknown keys and invalid values (logcat: invalid <...> setting, ABORT!), so the bridge reads the value back and reports "ok": false if it didn't take. A 200 alone doesn't mean it worked.

Keys

Case-sensitive, identical to the TV's internal names. Seen on the 55U65QF:

Backlight (0–100), Picture_Mode, Light_Sensor (On/Off), LD_MODE, Gamma, Color_Temperature, Peaking_Brightness, Low_Latency, BFI, Live_Tone, DV_IQ_LS, Color_Temperature_{Red,Green,Blue}_{Gain,Offset}, 11_Point_N_{Red,Green,Blue}.

To see every key on your set, watch logcat and move any picture slider:

adb logcat | grep onNotifyPqSettingInfo

Caveats

  • No authentication. Anyone on your LAN can change picture settings. For a home display this is probably fine. If it isn't, block port 8765 at your router, or build from source with TOKEN set in HttpServer.java so every request needs ?token=….
  • Settings are per-context. The TV stores each value separately per <input, SDR/HDR10/Dolby Vision, picture mode, colour temperature>, e.g. <HDMI4, SDR, Filmmaker, Warm>. A set only affects the context that's active right now. When a Dolby Vision title starts, you see a different stored backlight. /applyall copies across inputs only, not across HDR types or picture modes. Values persist across power cycles.
  • Turn the light sensor off, or it will override your backlight: /set?key=Light_Sensor&value=Off. (Untested whether that setting is global or per-context.)
  • Unofficial interface. This uses an internal MediaTek service, not a public API. Menu redesigns won't break it, but a firmware update that renames the service or reorders its methods would. The names and transaction codes are constants at the top of PqClient.java.
  • Fire OS may kill it under memory pressure despite the foreground service. If /health stops answering, restart the service (see above) or power-cycle the TV.

Home Assistant

examples/home-assistant/tv_backlight.yaml is a complete package that creates a number.tv_backlight slider:

  • A rest: sensor polls /state every 60 s. The slider greys out (unavailable) when the TV is off.
  • The slider updates immediately when moved. A trigger-based template sensor takes the value from a tv_backlight_set event fired at set time, and the next poll reconciles it. (homeassistant.update_entity on a rest: sensor is throttled, so polling alone feels laggy.)
  • Sets are debounced (400 ms, mode: restart) and turn Light_Sensor off first if it's on.

Replace 192.168.1.50 with your TV's IP. Example dashboard card that hides while the TV is off:

type: tile
entity: number.tv_backlight
features:
  - type: numeric-input
    style: slider
visibility:
  - condition: state
    entity: number.tv_backlight
    state_not: unavailable

The rest: integration doesn't create entities until its first successful fetch. If HA starts while the TV is unplugged, the entities show up a minute or so after the TV comes back.


How it works

Amazon Settings UI  (com.amazon.tv.settings.mediatek)
   │  IPqService.setPqParameter
   ▼
com.mediatek.customerpq / CustomerPqService          ◄── PQ Bridge binds here
   │  ICustomerPqService.setToPqCache(key, value)
   ▼
PQ cache + SQLite ─► vendor.mediatek.hardware.pq@1.0::IPq (HIDL HAL) ─► panel
   │
   └─► side effect: Settings.System.screen_brightness   ◄── a mirror nobody reads

CustomerPqService is declared android:exported="true" with no android:permission, so any installed app may bind to it. setToPqCache validates the key, stores the value for the current context, and pushes it to the HAL. That's exactly what moving the slider does.

PqClient.java sends raw Parcels to the binder using the transaction codes from the decompiled stub, instead of reconstructing the AIDL file (which would drag in SettingItem/ColorItem parcelables we don't need):

Package / class com.mediatek.customerpq / .CustomerPqService
Intent action CustomerPqService:remote
Interface descriptor com.mediatek.extservice.pqutils.ICustomerPqService
tx 1 String getFromPqCache(String key)
tx 2 void setToPqCache(String key, String value)
tx 3 void applyToAllSourcesToPQCache()
tx 4 void resetPqCache(List<String> keys) (unused)
tx 5–8 updateSettingItems / updateColorItems / register / unregister callback (unused)

The rest is a foreground service holding the binder, a hand-rolled HTTP/1.0 server, and a boot receiver. About 300 lines of Java with zero dependencies.

How this was found

This is the part that carries over to other models. If your TV has different package names or transaction codes, these are the steps to find them.

1. The obvious approach, and why it silently fails

Diff system settings before and after moving the Backlight slider:

adb shell settings list system > before.txt
# move the Backlight slider on the TV
adb shell settings list system > after.txt
diff before.txt after.txt
< screen_brightness=130
> screen_brightness=143
< screen_brightness_float=0.507874
> screen_brightness_float=0.5590551

So you try adb shell settings put system screen_brightness 50. The value sticks (settings get reads it back), and the panel doesn't change at all.

Logcat while moving the real slider shows why:

adb logcat -c && adb logcat | grep -iE 'backlight|pqcache|brightness'
PqCache: set(Backlight, 51), get lock
PqCache: <HDMI4, SDR, Filmmaker, Warm> set(Backlight, 51)
PqCache: set backlight with Settings(130/255)          <-- the PQ service WRITES the setting
MTSPQ-OEMService: defaultSetCall, successfully set : Backlight : 51
hwcomposer: setDisplayBrightness success, brightness = 0.503346

The picture-quality service writes screen_brightness as a side effect (round(backlight × 2.55), float = (int − 1) / 254), and nothing reads it back. It's a mirror, not a control. This is the first thing everyone tries, which is why it's worth spelling out.

The same log gives you the names to follow: PqCache, MTSPQ-OEMService, and the per-context tuple.

2. Ruling out the other obvious routes

Tried Result
adb shell cmd display set-brightness 0.2 No effect. dumpsys display shows mScreenBrightness=1.0 regardless; DisplayManager isn't in the chain.
adb shell service list | grep -i pq Nothing. The PQ components aren't registered with servicemanager, so service call can't reach them.
adb shell lshal | grep -i pq vendor.mediatek.hardware.pq@1.0::IPq/default. A HIDL HAL in hwservicemanager, which service call can't reach either.
MediaTek ContentProviders (com.mediatek.tis/*SettingsProvider) Tuner/input settings (HDMI, composite, analog), not picture quality.
Hisense RemoteNow MQTT (port 36669), hisensetv libs, HA Vidaa integrations Only exist on VIDAA / Hisense-Android firmware. Fire OS TVs don't run that broker.
input keyevent UI automation Works (quick settings = keyevent 176), but is brittle. Kept as a last resort only.

3. Finding the app that owns the PQ cache

adb shell pm list packages -f | grep -iE 'pq|picture|mediatek'
# package:/product/app/CustomerPqService_coyote-ch/…apk=com.mediatek.customerpq

adb shell dumpsys package com.mediatek.customerpq | grep -A3 -i 'service'
# shows CustomerPqService and its intent action "CustomerPqService:remote"

Pull the APK, along with the Settings app that calls it, for decompiling:

adb pull "$(adb shell pm path com.mediatek.customerpq | cut -d: -f2 | tr -d '\r')" customerpq.apk
adb pull "$(adb shell pm path com.amazon.tv.settings.mediatek | cut -d: -f2 | tr -d '\r')" tvsettings.apk

4. Decompiling with jadx

Open the APK in jadx (brew install jadx, then jadx-gui customerpq.apk).

  1. AndroidManifest.xml: find the <service> entry. What matters is android:exported="true" with no android:permission attribute. If a permission is set, a sideloaded app can't bind to it and this approach is dead on your firmware.
  2. Search for Stub: the AIDL-generated I…Service$Stub class holds everything a raw binder client needs:
    • DESCRIPTOR = "com.mediatek.extservice.pqutils.ICustomerPqService"
    • TRANSACTION_getFromPqCache = 1, TRANSACTION_setToPqCache = 2, …
    • the onTransact switch, which shows the argument order and types for each code (readString() ×2 for set, etc.)
  3. In the Settings APK, search for the descriptor or setToPqCache to confirm this really is the path the UI takes, and to see which key strings it uses.

5. Adapting PQ Bridge to another model

Update the constants at the top of PqClient.java (DESCRIPTOR, PKG, CLS, ACTION, TX_*) and the package name in the <queries> block of AndroidManifest.xml. Package visibility rules require it before you can bind to another app's service. Build, install, and test /get?key=Backlight first, since a wrong read can't hurt anything.

If bindService returns false or /health stays {"bound":false}, check adb logcat -s PqBridge/PqClient and look for ActivityManager errors about the bind.


Building from source

Android Studio (easiest): File → Open this folder, wait for the Gradle sync, then Build → Build App Bundle(s) / APK(s) → Build APK(s). Studio bundles the JDK and SDK. Output: app/build/outputs/apk/debug/app-debug.apk.

Command line: needs a JDK 17–21 and the Android SDK (platform 34). Point JAVA_HOME at the JDK and either set ANDROID_HOME or let Android Studio write local.properties.

./gradlew assembleDebug

The first run downloads Gradle 8.7 and the Android Gradle Plugin via the wrapper; nothing else needs installing.

Project layout, for non-Android people

settings.gradle.kts         declares the project and its one module, :app
build.gradle.kts            pins the Android Gradle Plugin version (8.5.2)
gradle/wrapper/, gradlew    the Gradle wrapper: fetches the right Gradle on first run
gradle.properties           JVM/Gradle flags
app/build.gradle.kts        SDK versions, app id, signing, build types
app/src/main/
  AndroidManifest.xml       permissions, components, boot receiver, <queries>
  java/com/pqbridge/app/
    PqClient.java           raw-Parcel binder client
    HttpServer.java         GET-only HTTP/1.0 server
    BridgeService.java      foreground service owning both
    BootReceiver.java       starts the service on BOOT_COMPLETED
    MainActivity.java       launch once so Android allows boot start
  res/values/strings.xml    app name

targetSdk is deliberately 30 (Android 11, matching Fire OS 8). Targeting 34 would require foregroundServiceType declarations and permissions that don't help on this device. compileSdk 34 only controls which APIs the compiler knows about.

Maintainers: see RELEASING.md.

License

MIT. Not affiliated with Hisense, Amazon, or MediaTek. Changing picture settings through an internal interface is at your own risk.

About

HTTP API for picture settings (backlight, picture mode, light sensor) on MediaTek Fire TV smart TVs. No root. Tested on Hisense 55U65QF.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages