Commands, settings, SDK functions, anything in the docs.

Build apps

Quickstart

Create an app, see it on a simulated panel as you edit, and render it to a file.

On this page

An app is a small TypeScript module that draws what a display shows. It runs on LEDABLE's servers, not on the display: when a display asks for the app, the platform runs it, turns the frames it returns into an image and sends the image to the display, which plays it.

You need the ledable command, installed as in Install and sign in. Nothing on this page needs an account or a display.

Create a project

Terminal
ledable developer init hello-led
cd hello-led

developer init takes one argument, the app's id, which also names the new directory: lowercase letters, digits and dashes. It refuses a directory that already exists. Into the new directory it writes:

FileWhat it is
ledable.jsonThe app's id, version, store listing and the hosts it may reach
index.tsxThe app: a white square whose size is a setting
README.mdThe text of the app's page in the store
package.jsonThe SDK as a dependency, the CLI as a development dependency, and check, dev and build scripts
tsconfig.jsonCompiler settings, taken from what the CLI generates
.gitignoreThe files that stay on your machine

Then it runs npm install and type-checks the project once, so a new project already builds. The project explains every file.

See it as you work

Terminal
ledable developer dev

developer dev builds the app and serves a preview page on your machine, on port 4400 unless you pass --port. It prints the address to open:

JSON
{"preview":"http://127.0.0.1:4400/"}

Open that address in a browser. The page runs the build as the platform runs it and shows what a display would show, on a simulated 64×32 panel, with the store preview below it and a form with the app's settings beside it. The command keeps running until you stop it with Ctrl-C.

Change it

Open index.tsx, change the colour of the square, and save. developer dev rebuilds on every save and the page draws the new image. A type error shows above the panel while the last good image stays; an error your code throws shows under the panel with the line it came from. Local preview covers everything the page and the command do.

This is a complete app with two settings, a message and a colour. It scrolls the message across the panel, and its preview draws the picture the store shows:

scrolling-message/index.tsx
import { colorRgb, defineApp, marquee, string } from "@ledable/sdk/app";
import {
  Frame, Text, measureText, renderFrame, type BitmapFont,
} from "@ledable/sdk/graphics";
import { MODERNDOS, loadFont } from "@ledable/sdk/fonts";

const WIDTH = 64;
const HEIGHT = 32;

/** Every frame of the message, from entering on the right to gone. */
function scroll(font: BitmapFont, message: string, color: string) {
  const width = Math.ceil(measureText(font, message).width);
  const distance = width + WIDTH;
  // 30 ms a pixel at 25 frames a second: smooth on the panel, and a
  // few hundred kilobytes at most.
  const motion = marquee({
    distancePx: distance,
    durationMs: distance * 30,
    fps: 25,
  });
  const image = Array.from({ length: motion.frameCount }, (_, frame) =>
    renderFrame(
      <Frame width={WIDTH} height={HEIGHT} background="#000000">
        <Text
          x={WIDTH - motion.offsetAt(frame)}
          y={HEIGHT / 2}
          anchor="lm"
          text={message}
          font={font}
          color={color}
        />
      </Frame>,
    ),
  );
  return { image, frameMs: motion.frameMs };
}

type Rgb = readonly [number, number, number];

function hex(rgb: Rgb): string {
  const parts = rgb.map(part => part.toString(16).padStart(2, "0"));
  return `#${parts.join("")}`;
}

export default defineApp({
  settings: () => ({
    message: string({
      name: "Message",
      description: "What scrolls across the panel",
      default: "Hello, LEDABLE",
      max_length: 80,
    }),
    color: colorRgb({
      name: "Colour",
      description: "The colour of the letters",
      default: [246, 205, 109],
    }),
  }),

  async render(ctx, params) {
    const font = await loadFont(ctx.assets, MODERNDOS);
    return scroll(font, params.message, hex(params.color));
  },

  async preview(ctx) {
    const font = await loadFont(ctx.assets, MODERNDOS);
    return scroll(font, "Hello, LEDABLE", "#f6cd6d");
  },
});
The scrolling-message example on a LEDABLE panel

Three things to notice:

  • Settings are declared in code with builders such as string and colorRgb, and render receives their values typed: params.message is a string. See Settings.
  • What an app cannot do for itself comes through ctx: reading its own files, such as a font with loadFont(ctx.assets, …), the time, and the display's time zone. See How apps work.
  • A frame is JSX drawn with renderFrame, pixel for pixel. See Drawing.

Render it once

To get the image a display would get, without a preview page:

Terminal
ledable developer render --out frame.webp

It builds the app, renders it with its default settings and writes the WebP. It prints where the file went and how the display is told to play it:

JSON
{
  "output": "/Users/you/hello-led/frame.webp",
  "bytes": 800,
  "behavior": "loop",
  "realtime": false,
  "softTtlMs": 300000,
  "hardTtlMs": 600000,
  "cacheId": null
}

--preview writes the store preview instead, and --values takes settings from a file; see Local preview.

Next