LAN-bound HTTP API served by the com.lg.app.signage.dev.remote JS service
running inside the com.lg.app.signage.dev IPK on a webOS Signage panel.
- Bind:
0.0.0.0:9999(LAN-reachable from any host on the same network). - Auth: none in v1. Rely on LAN isolation. Do not expose beyond trusted networks.
- Content-Type: JSON for everything except
/screenshot(image/jpeg or image/png). - Error shape:
{ "ok": false, "error": "<code>", ...detail }. HTTP status ≥ 400.
Returns a small single-file HTML control page (service/index.html, read once at startup) that drives the rest of this API from a browser. Same wire format as any static file — Content-Type: text/html.
The page exposes:
- input switcher (
POST /input) — buttons per input, with signal-detection dots - view switcher (
POST /view) — render an HDMI input inside the SI app - display controls (
POST /display) — Blank / Unblank, optional "restore after N min" - picture controls (
POST /picture) — backlight / brightness / contrast sliders + Apply, Night / Day preset buttons - screenshot button (
GET /screenshot) — FHD PNG default, configurable W/H/format, inline preview + save link - collapsible device info (
GET /device) - dev-only kill button (
POST /kill)
No polling — the page loads status on open and after each action. Solo-dev tool: no auth, no responsive layout.
GET /index.html returns the same content.
Liveness + uptime. Safe to poll.
Request: none.
200:
{
"ok": true,
"service": "com.lg.app.signage.dev.remote",
"version": "0.0.1",
"uptimeSeconds": 42,
"httpReady": true,
"lastCapture": { "at": "2026-04-22T12:04:12.345Z", "bytes": 120543, "uri": "file://internal/...", "format": "JPEG" },
"lastInput": { "at": "2026-04-22T12:04:09.111Z", "type": "HDMI", "index": 0 },
"view": null,
"display": { "mode": "on", "raw": "active", "restoreAt": null },
"picture": { "values": { "backlight": 80, "brightness": 50, "contrast": 85 }, "preset": null, "saved": null }
}lastCapture is null until GET /screenshot has been called at least once.
lastInput is the last input change: it stays null until POST /input switches the input, and a plain GET /input does not update it.
display and picture are read live from the panel (same shape as GET /display / GET /picture) with a 3 s timeout each. If a read fails, that field carries { "error": … } instead and /health still returns 200.
Static platform facts pulled from IDCAP configuration/property/get. Useful for fleet inventory and as a canary that IDCAP-from-service is alive.
Request: none.
200:
{
"ok": true,
"props": {
"model_name": "43UH5Q-EQ.BEUGLJP",
"serial_number": "XXXXXXXXXXXX",
"firmware_version": "03.34.12",
"platform_version": "9.0.0-146",
"webos_version": "9.0.0",
"idpn": 410,
"idcap_js_extension_version": "1.1.1"
}
}Any individual property that fails resolves to { "_error": { ... } } in its slot but doesn't fail the whole response. Exact property names come from the IDCAP "Properties" mapping table (see vendor docs).
Captures the panel's current framebuffer via IDCAP (idcap://utility/screen/capture), reads the resulting file (idcap://storage/file/read), and streams the bytes as the HTTP body.
Query parameters (all optional):
| Param | Type | Range | Default | Notes |
|---|---|---|---|---|
format |
JPEG | PNG |
— | JPEG |
Response content-type matches |
width |
integer | 128–1920 | 1280 | |
height |
integer | 72–1080 | 720 |
200: Raw image bytes. Response headers:
Content-Type: image/jpeg(orimage/png)Content-Length: <bytes>X-Capture-Uri: file://...(IDCAP URI of the saved file, for debugging)
502:
{ "ok": false, "error": "capture_failed", "detail": { "errorMessage": "..." }, "captureUri": "file://..." }curl -s http://<panel-ip>:9999/screenshot -o shot.jpg
curl -s "http://<panel-ip>:9999/screenshot?format=PNG&width=1920&height=1080" -o shot.png- HDCP-protected content cannot be captured. External HDMI from a protected source returns an IDCAP error.
- FHD capture + JPEG read round-trip takes ~1–3 seconds in practice.
- For images larger than ~1 MB, file/read slows noticeably. If this becomes a problem we'll switch to fetching via
http://127.0.0.1:9080/<path>from inside the service.
Returns the current audio/video input plus the full list of available inputs and their signal-detection state.
Request: none.
200:
{
"ok": true,
"current": { "type": "HDMI", "index": 0 },
"currentInputPort": "ext://hdmi:1",
"count": 4,
"inputs": [
{ "inputPort": "ext://hdmi:1", "signalDetection": true, "type": "HDMI", "index": 0 },
{ "inputPort": "ext://hdmi:2", "signalDetection": false, "type": "HDMI", "index": 1 },
{ "inputPort": "ext://dp:1", "signalDetection": false, "type": "RGB", "index": 0 },
{ "inputPort": "ext://ops:1", "signalDetection": false, "type": "OTHERS","index": 0 }
]
}inputs is null and inputListError is populated when the inputlist/get call fails (the current input is returned either way).
502: { "ok": false, "error": "get_input_failed", "detail": {...} }.
Switch the active input. Accepts either the native IDCAP shape or a shorter URI form.
Request body (JSON, pick one form):
{ "type": "HDMI", "index": 0 }or
{ "src": "ext://hdmi:1" }The URI form maps hdmi → HDMI, dp|dvi|rgb → RGB, ops|others → OTHERS, svideo/component/composite/scart similarly. Port number N in ext://.../:N maps to IDCAP index: N-1.
200: { "ok": true, "set": { "type": "HDMI", "index": 0 } }.
400:
{ "ok": false, "error": "invalid_json" }— request body isn't valid JSON{ "ok": false, "error": "missing_input", "example": { "type": "HDMI", "index": 0 } }{ "ok": false, "error": "bad_src", "expected": "ext://hdmi:1 | ext://dp:1 | ..." }{ "ok": false, "error": "unknown_src_scheme", "scheme": "..." }
502: { "ok": false, "error": "set_input_failed", "requested": {...}, "detail": {...} }.
Returns the current "view layer" state. When view is non-null, the web app is rendering that HDMI source as a fullscreen <video> overlay instead of the status UI — and /screenshot will capture that video output.
200:
{ "ok": true, "view": { "src": "ext://hdmi:3", "setAt": "2026-04-22T13:04:21Z" } }or when inactive:
{ "ok": true, "view": null }Switch the web app between "status UI" and "rendering an external HDMI input fullscreen". The panel stays in SI mode throughout — our app and service keep running, so /screenshot continues to work.
Request body (JSON):
{ "src": "ext://hdmi:3" }Shorthand also accepted: {"src": "hdmi:3"} / {"src": "hdmi3"}. To clear back to status UI: {"src": null}, {"src": "app"}, {"src": "none"}, or {}.
Switchover latency: up to ~1 s (web app polls ping every 1 s and applies the change on next tick).
200:
{ "ok": true, "view": { "src": "ext://hdmi:3", "setAt": "2026-04-22T13:04:21Z" } }400:
{ "ok": false, "error": "invalid_json" }{ "ok": false, "error": "bad_src", "expected": "ext://hdmi:1 | ext://dp:1 | ..." }
curl -sS -X POST -H 'content-type: application/json' -d '{"src":"ext://hdmi:3"}' http://<panel>:9999/view
sleep 2 # let the video element come up and sync
curl -sS "http://<panel>:9999/screenshot?format=JPEG" -o hdmi3.jpg
curl -sS -X POST -H 'content-type: application/json' -d '{"src":null}' http://<panel>:9999/viewThis keeps the panel in SI mode the whole time — service never dies, no relaunching needed.
Note: HDCP-protected HDMI content still can't be captured (browser and IDCAP both refuse). For dev-time debugging against sources you control, this is fine; against protected content (e.g. some streaming devices), the video tag will render black in the capture.
Whether the panel is blanked. Reads idcap://system/display/mute/get.
200:
{ "ok": true, "display": { "mode": "off", "raw": "screen off", "restoreAt": "2026-10-07T05:00:00.000Z" } }mode is "on" / "off" (raw is the IDCAP value: "active" / "screen off"). restoreAt is the pending auto-restore time, or null.
Blank or unblank the panel with main power on via idcap://system/display/mute (the IDCAP equivalent of SCAP Power.setDisplayMode). Only the panel goes dark: the app, the service, the HTTP API and the selected input are unaffected. No power-off, DPM, auto-standby or 15-min-off setting is touched.
Body:
| Field | Type | Notes |
|---|---|---|
mode |
"off" | "on" |
Required |
for_minutes |
number, 0 < n ≤ 1440 |
Only with "off". Restores automatically after N minutes. |
curl -sS -X POST -H 'content-type: application/json' -d '{"mode":"off"}' http://$PANEL:9999/display
curl -sS -X POST -H 'content-type: application/json' -d '{"mode":"off","for_minutes":600}' http://$PANEL:9999/display
curl -sS -X POST -H 'content-type: application/json' -d '{"mode":"on"}' http://$PANEL:9999/display200: { "ok": true, "display": { "mode": "off", "raw": "screen off", "restoreAt": "…" | null } }
{"mode":"on"}is unconditional: it sends"active"without reading state first, so it works right after a service restart. It also cancels any pending auto-restore.- A new
{"mode":"off"}replaces the pending auto-restore; leavefor_minutesout to cancel it. - The auto-restore deadline is kept in
$TMPDIR/lglr-display-state.json, so it survives a service restart: on startup the timer is re-armed, or fires at once if the deadline has passed. It does not survive a panel reboot. If the restore call fails, it is retried every 60 s.
Current backlight, brightness, contrast (0–100), read via idcap://configuration/property/get. Values belong to the current picture mode.
200:
{ "ok": true, "picture": { "values": { "backlight": 80, "brightness": 50, "contrast": 85 }, "preset": null, "saved": null } }preset is "night" while a night snapshot is held; saved is that snapshot (what "day" will restore).
Set any subset of backlight, brightness, contrast (integers 0–100) via idcap://configuration/property/set (IDCAP equivalent of SCAP Configuration.setPictureProperty), or apply a preset.
curl -sS -X POST -H 'content-type: application/json' -d '{"backlight":20}' http://$PANEL:9999/picture
curl -sS -X POST -H 'content-type: application/json' -d '{"preset":"night"}' http://$PANEL:9999/picture
curl -sS -X POST -H 'content-type: application/json' -d '{"preset":"night","backlight":10}' http://$PANEL:9999/picture
curl -sS -X POST -H 'content-type: application/json' -d '{"preset":"day"}' http://$PANEL:9999/picture"night"saves the current three values, then appliesbacklight 0, brightness 0, contrast 0. Any field sent alongside overrides that default. If a snapshot is already held (a second"night"), it is kept, so day values are never overwritten with night values."day"restores the snapshot (fields sent alongside override it) and then clears it. With no snapshot it returns 409no_saved_pictureand changes nothing.- The snapshot lives in the same state file as the display auto-restore (survives a service restart, not a panel reboot).
- Keys are set one at a time. On failure the 502
detailnames the failingkeyand what was alreadyapplied. - If Smart Energy Saving or an automatic brightness-control mode is active, the panel may override the backlight value. This endpoint does not change those settings.
200: { "ok": true, "applied": { "backlight": 0, … }, "picture": { …same as GET… } }
Exits the service process so the next ares-launch picks up new code. Used by scripts/deploy.sh as a hot-reload mechanism since webOS JS services holding a TCP listener don't idle-time-out.
200: { "ok": true, "bye": true } (service exits ~200ms after responding).
No authentication. Exposed on the same LAN port as the rest of the API. Remove before any production/customer-facing deployment.
| HTTP | error |
Meaning |
|---|---|---|
| 400 | invalid_json |
Request body wasn't JSON |
| 400 | missing_input |
POST /input had neither {type,index} nor {src} |
| 400 | bad_src |
src didn't match ext://scheme:N |
| 400 | unknown_src_scheme |
Scheme part of src isn't recognized |
| 404 | not_found |
No endpoint matches the request |
| 502 | capture_failed |
IDCAP capture or file/read error |
| 502 | no_uri |
IDCAP capture returned no uri field |
| 502 | no_data / empty_data / decode_failed |
IDCAP file/read returned nothing usable |
| 502 | get_input_failed |
IDCAP externalinput/get error |
| 502 | set_input_failed |
IDCAP externalinput/set error |
| 400 | bad_mode |
POST /display mode wasn't "off" / "on" |
| 400 | bad_for_minutes / for_minutes_only_with_off |
Invalid for_minutes, or sent with "on" |
| 400 | bad_fields / missing_fields / bad_preset |
POST /picture: unknown or out-of-range field, nothing to set, or unknown preset |
| 409 | no_saved_picture |
{"preset":"day"} with no night snapshot held |
| 502 | get_display_failed / set_display_failed |
IDCAP system/display/mute error |
| 502 | get_picture_failed / set_picture_failed |
IDCAP configuration/property error (snapshot_failed inside detail if "night" couldn't read current values) |
| 504 | (not emitted — IDCAP calls have an internal 15s timeout surfaced via 502 idcap_timeout) |
- Target hardware: LG UH5Q signage. Verified on 43UH5Q-EQ.BEUGLJP, webOS 9.0.0-146, IDPN 4xx. Should work on any webOS Signage 6.0+ panel where IDCAP is available.
- IDCAP middleware on the device:
com.webos.service.commercial.scapadapter. All calls route vialuna://com.webos.service.idcapmw.mwcommand/callidcap. - Source IDCAP reference:
idcap://utility/screen/capture,idcap://externalinput/{get,set},idcap://externalinput/inputlist/get,idcap://storage/file/read,idcap://system/display/mute{,/get},idcap://configuration/property/{get,set}.