Skip to content

Latest commit

 

History

History
318 lines (228 loc) · 14.4 KB

API.md

File metadata and controls

318 lines (228 loc) · 14.4 KB

LG Local Remote — HTTP API

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.

GET / — dev control UI

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.

GET /health

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.

GET /device

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).

GET /screenshot

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 (or image/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://..." }

Examples

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

Caveats

  • 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.

GET /input

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": {...} }.

POST /input

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": {...} }.

GET /view

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 }

POST /view

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 | ..." }

Typical flow — "screenshot HDMI3"

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/view

This 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.

GET /display

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.

POST /display

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/display

200: { "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; leave for_minutes out 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.

GET /picture

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).

POST /picture

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 applies backlight 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 409 no_saved_picture and 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 detail names the failing key and what was already applied.
  • 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… } }

POST /kill (dev only)

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.

Error taxonomy

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)

Platform notes

  • 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 via luna://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}.