diff --git a/cli/src/snippets.ts b/cli/src/snippets.ts index 7dd05145d6..14eacbb2d6 100644 --- a/cli/src/snippets.ts +++ b/cli/src/snippets.ts @@ -55,7 +55,7 @@ export async function snippets(options: SnippetsOptions) { await writeFile(fullname, snip) imports += `import "./${mod}"\n` numsnip++ - if (numsnip % 150 == 0) { + if (numsnip % 100 == 0) { allImports.push(imports) imports = "" } diff --git a/compiler/src/specgen.ts b/compiler/src/specgen.ts index d24a7c4c30..13ea6ad663 100644 --- a/compiler/src/specgen.ts +++ b/compiler/src/specgen.ts @@ -460,8 +460,7 @@ This service is ${status} and may change in the future. ` : undefined, patchLinks(info.notes["short"]), - `- client for [${info.name} service](https://microsoft.github.io/jacdac-docs/services/${info.shortId}/)`, - baseclass ? `- inherits ${baseclass}` : undefined, + `\n{@import optional ../clients-custom/${info.shortId}-short.mdp}\n`, info.notes["long"] ? `## About diff --git a/jacdac-ts b/jacdac-ts index 79cc13796b..ad54417a22 160000 --- a/jacdac-ts +++ b/jacdac-ts @@ -1 +1 @@ -Subproject commit 79cc13796b09df362f051212a0af04d31b740eb6 +Subproject commit ad54417a2273bfd794cdbf3eb42f2a002b4dc29e diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5374d219a2..99a878bcdc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,6 @@ import "./array" import "./string" import "./events" import "./jacdac" -import "./led" import "./lightbulb" import "./rotaryencoder" import "./button" diff --git a/packages/core/src/led.ts b/packages/core/src/led.ts deleted file mode 100644 index c651ba39b1..0000000000 --- a/packages/core/src/led.ts +++ /dev/null @@ -1,54 +0,0 @@ -// This file contains implementation for actions/registers marked `client` in the spec, -// as well as additional functionality on different service clients (Roles) - -import * as ds from "@devicescript/core" - -declare module "@devicescript/core" { - interface Led { - /** - * Sets all the pixels to the given RGB color. - * @param rgb 24bit color number - */ - setAll(rgb: number): Promise - - /** - * Sets the brightness between 0 (off) and 1 (full). - * @param brightness - */ - setBrightness(brightness: number): Promise - - /** - * Turns off the LEDs. - */ - off(): Promise - } -} - -ds.Led.prototype.setAll = async function (rgb) { - const len = await this.numPixels.read() - const buflen = len * 3 - const buf = Buffer.alloc(buflen) - - let idx = 0 - const r = (rgb >> 16) & 0xff - const g = (rgb >> 8) & 0xff - const b = rgb & 0xff - while (idx < buflen) { - buf.setAt(idx, "u8", r) - buf.setAt(idx + 1, "u8", g) - buf.setAt(idx + 2, "u8", b) - idx = idx + 3 - } - await this.pixels.write(buf) -} - -ds.Led.prototype.setBrightness = async function (brightness) { - await this.intensity.write(brightness) -} - -ds.Led.prototype.off = async function () { - const len = await this.numPixels.read() - const buflen = len * 3 - const buf = Buffer.alloc(buflen) - await this.pixels.write(buf) -} diff --git a/packages/drivers/src/index.ts b/packages/drivers/src/index.ts index 352ee3834f..468b4d6c13 100644 --- a/packages/drivers/src/index.ts +++ b/packages/drivers/src/index.ts @@ -14,5 +14,6 @@ export * from "./dotmatrix" export * from "./st7735" export * from "./uc8151" export * from "./trafficlight" +export * from "./ledserver" configureHardware({ scanI2C: false }) diff --git a/packages/drivers/src/ledserver.ts b/packages/drivers/src/ledserver.ts new file mode 100644 index 0000000000..6a597912d5 --- /dev/null +++ b/packages/drivers/src/ledserver.ts @@ -0,0 +1,144 @@ +import * as ds from "@devicescript/core" +import { + PixelBuffer, + fillFade, + pixelBuffer, + correctGamma, +} from "@devicescript/runtime" +import { Server, ServerOptions, startServer } from "@devicescript/server" + +export interface LedServerOptions { + /** + * Number of LEDs + */ + length: number + /** + * Brightness applied to pixels before being rendered. + * This allocate twice the memory if less than 1 as an additional buffer is needed to compute the color. + * @default 1 + */ + intensity?: number + /** + * Number of columns of a LED matrix + */ + columns?: number + ledsPerPixel?: number + /** + * For monochrome LEDs, the LED wavelength + */ + waveLength?: number + /** + * The luminous power of the LEDs, is it very bright? + */ + luminousIntensity?: number + /** + * The shape and topology of the LEDs + */ + variant?: ds.LedVariant + /** + * Specify the amount of gamma correction + */ + gamma?: number +} + +class LedServer extends Server implements ds.LedServerSpec { + private _intensity: number + private _columns: number + private _ledPerPixels: number + private _waveLength: number + private _luminousIntensity: number + private _variant: ds.LedVariant + private _gamma: number + + readonly buffer: PixelBuffer + + constructor(options: LedServerOptions & ServerOptions) { + super(ds.Led.spec, options) + this.buffer = pixelBuffer(options.length) + this._intensity = options.intensity ?? 1 + this._columns = options.columns + this._ledPerPixels = options.ledsPerPixel + this._waveLength = options.waveLength + this._luminousIntensity = options.luminousIntensity + this._variant = options.variant + this._gamma = options.gamma + } + + pixels(): ds.Buffer { + if (this.buffer.length < 64) return this.buffer.buffer + else return Buffer.alloc(0) + } + set_pixels(value: ds.Buffer): void { + this.buffer.buffer.blitAt(0, value, 0, value.length) + } + intensity(): number { + return this._intensity + } + set_intensity(value: number): void { + this._intensity = Math.clamp(0, value, 1) + } + actualBrightness(): number { + return this._intensity + } + numPixels(): number { + return this.buffer.length + } + numColumns(): number { + return this._columns + } + ledsPerPixel(): number { + return this._ledPerPixels + } + waveLength(): number { + return this._waveLength || 0 + } + luminousIntensity(): number { + return this._luminousIntensity + } + variant(): ds.LedVariant { + return this._variant + } + + /** + * Display buffer on hardware + */ + async show(): Promise { + let b = this.buffer + // full brightness so we can use the buffer as is + if (this._intensity < 1 || this._gamma) { + const r = b.allocClone() + if (this._intensity < 1) fillFade(r, this._intensity) + if (this._gamma) correctGamma(r, this._gamma) + b = r + } + // TODO: render b to hardware + } +} + +/** + * Starts a programmable LED server. + * Simulation is supported for up to 64 LEDs; otherwise only the simulator + * will reflect the state of LEDs. + * @param options + * @returns + */ +export async function startLed( + options: LedServerOptions & ServerOptions +): Promise { + const { length } = options + const server = new LedServer(options) + const client = new ds.Led(startServer(server)) + + ;(client as any)._buffer = server.buffer + client.show = async function () { + await server.show() + if (length <= 64) await client.pixels.write(server.buffer.buffer) + else if (ds.isSimulator()) { + // the simulator handles brightness separately + const topic = `jd/${server.serviceIndex}/leds` + await ds._twinMessage(topic, server.buffer.buffer) + } + } + + return client +} diff --git a/packages/graphics/src/image.ts b/packages/graphics/src/image.ts index 6309cc6620..0ed09108eb 100644 --- a/packages/graphics/src/image.ts +++ b/packages/graphics/src/image.ts @@ -33,14 +33,14 @@ export declare class Image { buffer: Buffer /** - * Get a pixel color + * Return a copy of the current image */ - get(x: number, y: number): number + clone(): Image /** - * Return a copy of the current image + * Get a pixel color */ - clone(): Image + get(x: number, y: number): number /** * Set pixel color diff --git a/packages/graphics/src/palette.ts b/packages/graphics/src/palette.ts index d813d97887..463bc20d68 100644 --- a/packages/graphics/src/palette.ts +++ b/packages/graphics/src/palette.ts @@ -1,7 +1,14 @@ export class Palette { readonly buffer: Buffer - readonly numColors: number + /** + * Number of colors in the palette + */ + readonly length: number + /** + * Allocates a 4bpp palette similar to MakeCode Arcade + * @returns + */ static arcade() { return new Palette(hex` 000000 ffffff ff2121 ff93c4 ff8135 fff609 249ca3 78dc52 @@ -9,6 +16,10 @@ export class Palette { `) } + /** + * Allocates a 1bpp monochrome palette + * @returns + */ static monochrome() { return new Palette(hex`000000 ffffff`) } @@ -16,28 +27,41 @@ export class Palette { constructor(init: Buffer) { this.buffer = init.slice(0) this.buffer.set(init) - this.numColors = (this.buffer.length / 3) >> 0 + this.length = (this.buffer.length / 3) >> 0 } - color(idx: number) { - if (idx < 0 || idx >= this.numColors) return 0 + /** + * Returns the 24bit RGB color at the given index + * @param index + * @returns + */ + getAt(index: number) { + if (index < 0 || index >= this.length) return 0 return ( - (this.buffer[3 * idx + 0] << 16) | - (this.buffer[3 * idx + 1] << 8) | - (this.buffer[3 * idx + 2] << 0) + (this.buffer[3 * index] << 16) | + (this.buffer[3 * index + 1] << 8) | + (this.buffer[3 * index + 2] << 0) ) } - setColor(idx: number, color: number) { - this.buffer[3 * idx + 0] = color >> 16 - this.buffer[3 * idx + 1] = color >> 8 - this.buffer[3 * idx + 2] = color >> 0 + /** + * Sets a 24bit RGB color at the given index + * @param index + * @param color 24bit RGB color + */ + setAt(index: number, color: number) { + this.buffer[3 * index] = color >> 16 + this.buffer[3 * index + 1] = color >> 8 + this.buffer[3 * index + 2] = color >> 0 } - // r,g,b,padding + /** + * Packs palette for Jacdac packet + * @returns + */ packed(): Buffer { - const res: Buffer = Buffer.alloc(this.numColors << 2) - for (let i = 0; i < this.numColors; ++i) { + const res: Buffer = Buffer.alloc(this.length << 2) + for (let i = 0; i < this.length; ++i) { res[i * 4] = this.buffer[i * 3] res[i * 4 + 1] = this.buffer[i * 3 + 1] res[i * 4 + 2] = this.buffer[i * 3 + 2] @@ -45,11 +69,14 @@ export class Palette { return res } - // r,g,b,padding + /** + * Unpacks palette from Jacdac packet + * @param buffer + */ unpack(buffer: Buffer) { - if (buffer.length >> 2 !== this.numColors) + if (buffer.length >> 2 !== this.length) throw new RangeError("incorrect number of colors") - for (let i = 0; i < this.numColors; ++i) { + for (let i = 0; i < this.length; ++i) { this.buffer[i * 3] = buffer[i * 4] this.buffer[i * 3 + 1] = buffer[i * 4 + 1] this.buffer[i * 3 + 2] = buffer[i * 4 + 2] diff --git a/packages/runtime/src/colors.ts b/packages/runtime/src/colors.ts index 5cc8b4cbe2..4b126cceb2 100644 --- a/packages/runtime/src/colors.ts +++ b/packages/runtime/src/colors.ts @@ -1,5 +1,3 @@ -import * as ds from "@devicescript/core" - /** * Well known colors */ @@ -101,7 +99,7 @@ export function hsv(hue: number, sat: number = 255, val: number = 255): number { * @param brightness the amount of brightness to apply to the color between 0 and 1. */ export function fade(color: number, brightness: number): number { - brightness = Math.max(0, Math.min(0xff, brightness * 0xff)) + brightness = Math.max(0, Math.min(0xff, brightness << 8)) if (brightness < 0xff) { let red = (color >> 16) & 0xff let green = (color >> 8) & 0xff @@ -126,6 +124,13 @@ function unpackB(rgb: number): number { return (rgb >> 0) & 0xff } +/** + * Alpha blending of each color channel and returns a rgb 24bit color + * @param color + * @param alpha factor of blending, 0 for color, 1 for otherColor + * @param otherColor + * @returns + */ export function blend(color: number, alpha: number, otherColor: number) { alpha = Math.max(0, Math.min(0xff, alpha | 0)) const malpha = 0xff - alpha @@ -134,182 +139,3 @@ export function blend(color: number, alpha: number, otherColor: number) { const b = (unpackB(color) * malpha + unpackB(otherColor) * alpha) >> 8 return rgb(r, g, b) } - -/** - * A buffer of RGB colors - */ -export class PixelBuffer { - /** - * Number of pixels in the buffer - */ - readonly buffer: ds.Buffer - readonly start: number - readonly length: number - - constructor(buffer: ds.Buffer, start: number, length: number) { - ds.assert(buffer.length >= (start + length) * 3, "buffer too small") - this.buffer = buffer - this.start = start - this.length = length - } - - /** - * Set a pixel color in the buffer - * @param pixeloffset pixel offset. if negative starts from the end - * @param color RGB color - */ - setColor(pixeloffset: number, color: number) { - while (pixeloffset < 0) pixeloffset += this.length - const i = this.start + (pixeloffset << 0) - if (i < this.start || i >= this.start + this.length) return - const bi = i * 3 - this.buffer.setAt(bi, "u8", (color >> 16) & 0xff) - this.buffer.setAt(bi + 1, "u8", (color >> 8) & 0xff) - this.buffer.setAt(bi + 2, "u8", color & 0xff) - } - - /** - * Reads the pixel color at the given offsret - * @param pixeloffset pixel offset. if negative starts from the end - * @returns - */ - getColor(pixeloffset: number): number { - while (pixeloffset < 0) pixeloffset += this.length - const i = this.start + (pixeloffset << 0) - if (i < this.start || i >= this.start + this.length) return undefined - const bi = i * 3 - const r = this.buffer.getAt(bi, "u8") - const g = this.buffer.getAt(bi + 1, "u8") - const b = this.buffer.getAt(bi + 2, "u8") - - return rgb(r, g, b) - } - - /** - * Renders a bar grpah on the LEDs - * @param value - * @param high - * @returns - */ - setBarGraph( - value: number, - high: number, - options?: { - emptyRangeColor?: number - zeroColor?: number - } - ): void { - if (high <= 0) { - const emptyRangeColor = options?.emptyRangeColor - this.clear() - this.setColor( - 0, - isNaN(emptyRangeColor) ? 0xffff00 : emptyRangeColor - ) - return - } - - value = Math.abs(value) - const n = this.length - const n1 = n - 1 - let v = Math.idiv(value * n, high) - if (v === 0) { - const zeroColor = options?.zeroColor - this.setColor(0, isNaN(zeroColor) ? 0x666600 : zeroColor) - for (let i = 1; i < n; ++i) this.setColor(i, 0) - } else { - for (let i = 0; i < n; ++i) { - if (i <= v) { - const b = Math.idiv(i * 255, n1) - this.setColor(i, rgb(b, 0, 255 - b)) - } else this.setColor(i, 0) - } - } - } - - /** - * Writes a gradient between the two colors. - * @param startColor - * @param endColor - */ - setGradient( - startColor: number, - endColor: number, - start?: number, - end?: number - ): void { - // normalize range - if (start < 0) start += this.length - start = start || 0 - if (end < 0) end += this.length - end = end === undefined ? this.length - 1 : end - // check if any work needed - const steps = end - start - if (steps < 1) return - - this.setColor(start, startColor) - this.setColor(end, endColor) - for (let i = start + 1; i < end - 1; ++i) { - const alpha = Math.idiv(0xff * i, steps) - const c = blend(startColor, alpha, endColor) - this.setColor(i, c) - } - } - - /** - * Clears the buffer to #000000 - */ - clear() { - this.buffer.fillAt(this.start, this.length, 0) - } - - /** - * Creates a range view over the color buffer - * @param start start index - * @param length length of the range - * @returns a view of the color buffer - */ - range(start: number, length?: number): PixelBuffer { - const rangeStart = this.start + (start << 0) - const rangeLength = - length === undefined - ? this.length - start - : Math.min(length, this.length - start) - return new PixelBuffer(this.buffer, rangeStart, rangeLength) - } -} - -/** - * Create a color buffer that allows you to manipulate a range of pixels - * @param numPixels number of pixels - */ -export function pixelBuffer(numPixels: number) { - numPixels = numPixels << 0 - const buf = ds.Buffer.alloc(numPixels * 3) - return new PixelBuffer(buf, 0, numPixels) -} - -declare module "@devicescript/core" { - interface Led { - /** - * Allocates a pixel buffer for the LED string. - */ - allocateBuffer(): Promise - - /** - * Writes the pixels to the LED client - * @param led LED client - */ - write(pixels: PixelBuffer): Promise - } -} - -ds.Led.prototype.allocateBuffer = async function () { - const numPixels = await this.numPixels.read() - return pixelBuffer(numPixels) -} - -ds.Led.prototype.write = async function (pixels: PixelBuffer) { - if (!pixels) return - await this.pixels.write(pixels.buffer) -} diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 5d7064a525..25cea63c04 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -4,4 +4,7 @@ export * from "./schedule" export * from "./encodeURIComponent" export * from "./valuedashboard" export * from "./map" -export * from "./set" \ No newline at end of file +export * from "./set" +export * from "./pixelbuffer" +export * from "./led" +export * from "./leddisplay" \ No newline at end of file diff --git a/packages/runtime/src/led.ts b/packages/runtime/src/led.ts new file mode 100644 index 0000000000..adfc7cbd32 --- /dev/null +++ b/packages/runtime/src/led.ts @@ -0,0 +1,46 @@ +import * as ds from "@devicescript/core" +import { PixelBuffer, fillSolid, pixelBuffer } from "./pixelbuffer" + +interface LedWithBuffer { + _buffer: PixelBuffer +} + +declare module "@devicescript/core" { + interface Led { + /** + * Gets the pixel buffer to perform coloring operations. + * Call `show` to send the buffer to the LED strip. + */ + buffer(): Promise + + /** + * Sends the pixel buffer to the LED driver + */ + show(): Promise + + /** + * Sets all pixel color to the given color and renders the buffer + */ + showAll(c: number): Promise + } +} + +ds.Led.prototype.buffer = async function () { + let b = (this as any as LedWithBuffer)._buffer + if (!b) { + const n = await this.numPixels.read() + ;(this as any as LedWithBuffer)._buffer = b = pixelBuffer(n) + } + return b +} + +ds.Led.prototype.show = async function () { + const b = (this as any as LedWithBuffer)._buffer + if (b && b.length <= 64) await this.pixels.write(b.buffer) +} + +ds.Led.prototype.showAll = async function (c: number) { + const b = await this.buffer() + fillSolid(b, c) + await this.show() +} diff --git a/packages/runtime/src/leddisplay.ts b/packages/runtime/src/leddisplay.ts new file mode 100644 index 0000000000..77f12992fe --- /dev/null +++ b/packages/runtime/src/leddisplay.ts @@ -0,0 +1,49 @@ +import * as ds from "@devicescript/core" +import { Display, Image, Palette } from "@devicescript/graphics" + +/** + * Mounts a Display interface over a LED matrix to make it act as a screen. + * This function allocates one image. + * @param led + * @param palette + * @returns + */ +export async function startLedDisplay( + led: ds.Led, + palette?: Palette +): Promise { + if (!palette) { + const waveLength = await led.waveLength.read() + if (waveLength) palette = Palette.monochrome() + else palette = Palette.arcade() + } + + const buffer = await led.buffer() + + const width = (await led.numColumns.read()) || 1 + const height = (buffer.length / width) | 0 + + const bpp = palette.length === 2 ? 1 : 4 + + const image = Image.alloc(width, height, bpp) + + const init = async () => {} + + const show = async () => { + for (let x = 0; x < width; ++x) { + for (let y = 0; y < height; ++y) { + const ci = image.get(x, y) + const c = palette.getAt(ci) + buffer.setAt(y * width + x, c) + } + } + await led.show() + } + + return { + image, + palette, + init, + show, + } +} diff --git a/packages/runtime/src/main.ts b/packages/runtime/src/main.ts index 3d7020756f..416f9e545e 100644 --- a/packages/runtime/src/main.ts +++ b/packages/runtime/src/main.ts @@ -1,5 +1,16 @@ import { describe, expect, test } from "@devicescript/test" -import { encodeURIComponent, pixelBuffer, rgb, schedule, setStatusLight, uptime, Map, Set } from "." +import { + encodeURIComponent, + pixelBuffer, + rgb, + schedule, + setStatusLight, + uptime, + Map, + Set, + fillGradient, + fillBarGraph, +} from "." import { delay } from "@devicescript/core" describe("rgb", () => { @@ -20,22 +31,22 @@ describe("rgb", () => { describe("colorbuffer", () => { test("setpixelcolor", () => { const buf = pixelBuffer(3) - buf.setColor(1, 0x123456) - expect(buf.getColor(1)).toBe(0x123456) + buf.setAt(1, 0x123456) + expect(buf.at(1)).toBe(0x123456) }) test("setpixelcolor negative", () => { const buf = pixelBuffer(3) - buf.setColor(-1, 0x123456) - expect(buf.getColor(-1)).toBe(0x123456) + buf.setAt(-1, 0x123456) + expect(buf.at(-1)).toBe(0x123456) }) test("setbargraph", () => { const buf = pixelBuffer(4) - buf.setBarGraph(5, 10) + fillBarGraph(buf, 5, 10) console.log(buf.buffer) }) test("gradient", () => { const buf = pixelBuffer(4) - buf.setGradient(0xff0000, 0x00ff00) + fillGradient(buf, 0xff0000, 0x00ff00) console.log(buf.buffer) }) }) @@ -109,7 +120,7 @@ describe("encodeURIComponent tests", () => { }) }) -describe('Test Es Map Class', () => { +describe("Test Es Map Class", () => { function msg(m: string) { console.log(m) } @@ -144,15 +155,13 @@ describe('Test Es Map Class', () => { ]) msg("map test constructor") expect(map.size() === 3).toBe(true) - }) msg("Map tests completed") }) -describe('Test Es Set Class', () => { - +describe("Test Es Set Class", () => { test("add", () => { - let elements = new Set(); + let elements = new Set() expect(elements === elements.add(1)).toBe(true) expect(elements.size === 1).toBe(true) @@ -166,21 +175,20 @@ describe('Test Es Set Class', () => { expect(elements.size === 3).toBe(true) }) - test("clear", () => { - let elements = new Set(); - [1, 3, 1, 4, 5, 3].forEach(element => { + let elements = new Set() + ;[1, 3, 1, 4, 5, 3].forEach(element => { elements.add(element) }) expect(elements.size === 4).toBe(true) - elements.clear(); + elements.clear() expect(elements.size === 0).toBe(true) }) test("delete", () => { - let elements = new Set(); - ["a", "b", "e", "b", "d", "c", "a"].forEach(element => { + let elements = new Set() + ;["a", "b", "e", "b", "d", "c", "a"].forEach(element => { elements.add(element) }) @@ -196,10 +204,9 @@ describe('Test Es Set Class', () => { expect(elements.size === 4).toBe(true) }) - test("has", () => { - let elements = new Set(); - ["a", "d", "f", "d", "d", "a", "g"].forEach(element => { + let elements = new Set() + ;["a", "d", "f", "d", "d", "a", "g"].forEach(element => { elements.add(element) }) @@ -208,4 +215,4 @@ describe('Test Es Set Class', () => { expect(elements.has("f")).toBe(true) expect(!elements.has("e")).toBe(true) }) -}) \ No newline at end of file +}) diff --git a/packages/runtime/src/pixelbuffer.ts b/packages/runtime/src/pixelbuffer.ts new file mode 100644 index 0000000000..822bd09d20 --- /dev/null +++ b/packages/runtime/src/pixelbuffer.ts @@ -0,0 +1,269 @@ +import * as ds from "@devicescript/core" +import { blend, rgb } from "./colors" + +/** + * A buffer of RGB colors + */ +export class PixelBuffer { + /** + * Array of RGB colors + * @internal + */ + readonly buffer: ds.Buffer + /** + * Starting pixel index in the original buffer + */ + readonly start: number + /** + * Number of pixels in the buffer + */ + readonly length: number + + constructor(buffer: ds.Buffer, start: number, length: number) { + ds.assert(buffer.length >= (start + length) * 3, "buffer too small") + this.buffer = buffer + this.start = start + this.length = length + } + + /** + * Set a pixel color in the buffer + * @param pixeloffset pixel offset. if negative starts from the end + * @param color RGB color + */ + setAt(pixeloffset: number, color: number) { + pixeloffset = pixeloffset | 0 + while (pixeloffset < 0) pixeloffset += this.length + const i = this.start + pixeloffset + if (i < this.start || i >= this.start + this.length) return + const bi = i * 3 + this.buffer[bi] = (color >> 16) & 0xff + this.buffer[bi + 1] = (color >> 8) & 0xff + this.buffer[bi + 2] = color & 0xff + } + + /** + * Setter for individual RGB colors. Does not check bounds and should be used with care. + * @param pixeloffset + * @param r + * @param g + * @param b + */ + setRgbAt(pixeloffset: number, r: number, g: number, b: number) { + const i = (this.start + (pixeloffset | 0)) * 3 + this.buffer[i] = r + this.buffer[i + 1] = g + this.buffer[i + 2] = b + } + + /** + * Reads the pixel color at the given offsret + * @param pixeloffset pixel offset. if negative starts from the end + * @returns + */ + at(pixeloffset: number): number { + pixeloffset = pixeloffset | 0 + while (pixeloffset < 0) pixeloffset += this.length + const i = this.start + pixeloffset + if (i < this.start || i >= this.start + this.length) return undefined + const bi = i * 3 + const r = this.buffer[bi] + const g = this.buffer[bi + 1] + const b = this.buffer[bi + 2] + return rgb(r, g, b) + } + + /** + * Apply a conversion function to all pixels + * @param converter + */ + apply(converter: (c: number, index: number) => number) { + const n = this.length + const buf = this.buffer + for (let i = 0; i < n; ++i) { + const bi = (this.start + i) * 3 + + const r = buf[bi] + const g = buf[bi + 1] + const b = buf[bi + 2] + const c = rgb(r, g, b) + + const cr = converter(c, i) + + buf[bi] = (cr >> 16) & 0xff + buf[bi + 1] = (cr >> 8) & 0xff + buf[bi + 2] = cr & 0xff + } + } + + /** + * Allocates a clone of the buffer view + * @returns + */ + allocClone() { + const res = new PixelBuffer( + this.buffer.slice(this.start * 3, (this.start + this.length) * 3), + 0, + this.length + ) + return res + } + + /** + * Clears the buffer to #000000 + */ + clear() { + this.buffer.fillAt(this.start * 3, this.length * 3, 0) + } + + /** + * Creates a range view over the color buffer + * @param start start index + * @param length length of the range + * @returns a view of the color buffer + */ + view(start: number, length?: number): PixelBuffer { + const rangeStart = this.start + (start << 0) + const rangeLength = + length === undefined + ? this.length - start + : Math.min(length, this.length - start) + return new PixelBuffer(this.buffer, rangeStart, rangeLength) + } +} + +/** + * Create a color buffer that allows you to manipulate a range of pixels + * @param numPixels number of pixels + */ +export function pixelBuffer(numPixels: number) { + numPixels = numPixels | 0 + const buf = ds.Buffer.alloc(numPixels * 3) + return new PixelBuffer(buf, 0, numPixels) +} + +/** + * Writes a gradient between the two colors. + * @param startColor + * @param endColor + */ +export function fillGradient( + pixels: PixelBuffer, + startColor: number, + endColor: number +): void { + const start = pixels.start + const end = pixels.start + pixels.length + // check if any work needed + const steps = end - start + if (steps < 1) return + + pixels.setAt(start, startColor) + pixels.setAt(end, endColor) + for (let i = start + 1; i < end - 1; ++i) { + const alpha = Math.idiv(0xff * i, steps) + const c = blend(startColor, alpha, endColor) + pixels.setAt(i, c) + } +} + +/** + * Renders a bar graph of value (absolute value) on a pixel buffer + * @param pixels + * @param value + * @param high + * @param options + * @returns + */ +export function fillBarGraph( + pixels: PixelBuffer, + value: number, + high: number, + options?: { + /** + * Color used when the range is empty + */ + emptyRangeColor?: number + /** + * Color used when the value is 0 + */ + zeroColor?: number + } +) { + if (high <= 0) { + const emptyRangeColor = options?.emptyRangeColor + pixels.clear() + pixels.setAt(0, isNaN(emptyRangeColor) ? 0xffff00 : emptyRangeColor) + return + } + + value = Math.abs(value) + const n = pixels.length + const n1 = n - 1 + let v = Math.idiv(value * n, high) + if (v === 0) { + const zeroColor = options?.zeroColor + pixels.setAt(0, isNaN(zeroColor) ? 0x666600 : zeroColor) + for (let i = 1; i < n; ++i) pixels.setAt(i, 0) + } else { + for (let i = 0; i < n; ++i) { + if (i <= v) { + const b = Math.idiv(i * 255, n1) + pixels.setRgbAt(i, b, 0, 255 - b) + } else pixels.setRgbAt(i, 0, 0, 0) + } + } +} + +/** + * Sets all the color in the buffer to the given color + * @param c 24bit rgb color + */ +export function fillSolid(pixels: PixelBuffer, c: number) { + const r = (c >> 16) & 0xff + const g = (c >> 8) & 0xff + const b = c & 0xff + const n = pixels.length + for (let i = 0; i < n; ++i) { + pixels.setRgbAt(i, r, g, b) + } +} + +/** + * Fades each color channels according to the brigthness value between 0 dark and 1 full brightness. + * @param brightness + */ +export function fillFade(pixels: PixelBuffer, brightness: number) { + brightness = Math.max(0, Math.min(0xff, (brightness | 0) << 8)) + + const s = pixels.start * 3 + const e = (pixels.start + pixels.length) * 3 + const buf = pixels.buffer + + for (let i = s; i < e; ++i) { + buf[i] = (buf[i] * brightness) >> 8 + } +} + +/** + * Applies a inplace gamma correction to the pixel colors + * @param pixels + * @param gamma + */ +export function correctGamma(pixels: PixelBuffer, gamma: number = 2.7) { + const s = pixels.start * 3 + const e = (pixels.start + pixels.length) * 3 + const buf = pixels.buffer + + for (let i = s; i < e; ++i) { + const c = buf[i] + let o = + c === 0 + ? 0 + : c === 0xff + ? 0xff + : Math.round(Math.pow(c / 255.0, gamma) * 255.0) + if (c > 0 && o === 0) o = 1 + buf[i] = o + } +} diff --git a/devs/samples/display-dimmer.ts b/packages/sampleprj/src/maindisplaydimmer.ts similarity index 73% rename from devs/samples/display-dimmer.ts rename to packages/sampleprj/src/maindisplaydimmer.ts index ee377e74fa..ab38c263bc 100644 --- a/devs/samples/display-dimmer.ts +++ b/packages/sampleprj/src/maindisplaydimmer.ts @@ -1,9 +1,9 @@ import * as ds from "@devicescript/core" +import "@devicescript/runtime" const pot = new ds.Potentiometer() const ledD = new ds.Led() const btn = new ds.Button() -let p pot.reading.subscribe(async p => { console.log("tick", p) @@ -11,13 +11,13 @@ pot.reading.subscribe(async p => { }) ledD.binding().subscribe(async () => { - await ledD.setAll(0xff0000) + await ledD.showAll(0xff0000) }) btn.down.subscribe(async () => { - await ledD.setAll(0xff00ff) + await ledD.showAll(0xff00ff) }) btn.up.subscribe(async () => { - await ledD.setAll(0x0000ff) + await ledD.showAll(0x0000ff) }) diff --git a/packages/sampleprj/src/mainled.ts b/packages/sampleprj/src/mainled.ts index a7dc52e65a..41caf3a541 100644 --- a/packages/sampleprj/src/mainled.ts +++ b/packages/sampleprj/src/mainled.ts @@ -1,10 +1,43 @@ -import { delay, Led } from "@devicescript/core" -import { rgb } from "@devicescript/runtime" +import { delay, Led, LedVariant } from "@devicescript/core" +import { startLed } from "@devicescript/drivers" +import { startLedDisplay } from "@devicescript/runtime" -const led = new Led() +const jdled = new Led() +const led = await startLed({ + length: 12, + columns: 3, + variant: LedVariant.Ring, +}) +const led2 = await startLed({ + length: 256, + variant: LedVariant.Strip, +}) +const ledm = await startLed({ + columns: 16, + length: 256, + variant: LedVariant.Matrix, + gamma: 2.7, +}) +const display = await startLedDisplay(ledm) + +let ci = 0 +setInterval(async () => { + ci = (ci + 1) % display.palette.length + if (!ci) ci++ + await jdled.showAll(0x00ff00) + await led.showAll(0xff0000) + await led2.showAll(0x0000ff) + display.image.fill(0) + display.image.drawCircle(6, 6, 5, ci) + await display.show() + await delay(1000) + + await jdled.showAll(0x0f0fff) + await led.showAll(0x00ff00) + await led2.showAll(0x00ff00) + display.image.fill(0) + display.image.drawRect(2, 2, 10, 8, ci) + await display.show() -setTimeout(async () => { - await led.setAll(rgb(255, 0, 0)) await delay(500) - await led.setAll(0) }, 500) diff --git a/website/docs/api/clients-custom/led-about.mdp b/website/docs/api/clients-custom/led-about.mdp new file mode 100644 index 0000000000..db9f3a156e --- /dev/null +++ b/website/docs/api/clients-custom/led-about.mdp @@ -0,0 +1,15 @@ +:::note + +Additional runtime support is provided for `Led` +by importing the `@devicescript/runtime` package. + +Please refer to the [LEDs developer documentation](/developer/leds). + +```ts +import { Led } from "@devicescript/core" +import "@devicescript/runtime" + +const led = new Led() +``` + +::: diff --git a/website/docs/api/runtime/index.mdx b/website/docs/api/runtime/index.mdx new file mode 100644 index 0000000000..4073c6cfbf --- /dev/null +++ b/website/docs/api/runtime/index.mdx @@ -0,0 +1,8 @@ +# Runtime + +The `@devicescript/runtime` [builtin](/developer/packages) module +provides additional runtime helpers. + +```ts +import "@devicescript/runtime" +``` diff --git a/website/docs/api/runtime/pixelbuffer.mdx b/website/docs/api/runtime/pixelbuffer.mdx new file mode 100644 index 0000000000..921f816b80 --- /dev/null +++ b/website/docs/api/runtime/pixelbuffer.mdx @@ -0,0 +1,109 @@ +# PixelBuffer + +A 1D color vector to support color manipulation of LED strips. All colors are 24bit RGB colors. + +The pixel buffer is typically accessed through a [Led](/api/clients/led) client. + +```ts +import { Led } from "@devicescript/core" +import "@devicescript/runtime" + +const led = new Led() +// highlight-next-line +const pixels = await led.buffer() +``` + +It can also be allocated using `pixelBuffer`. + +```ts +import { pixelBuffer } from "@devicescript/runtime" + +// highlight-next-line +const pixels = pixelBuffer(32) +``` + +## Usage + +### `at`, `setAt` + +Indexing functions similar to `Array.at`, they allow to set color of individual LEDs and support negative indices. + +```ts skip +const c0 = pixels.at(0) +pixels.setAt(1, c0) +``` + +### `clear` + +Clears all colors to `#000000`. + +```ts skip +pixels.clear() +``` + +### `view` + +Creates a aliased range view of the buffer so that you can apply operations on a subset of the colors. + +```ts skip +const view = pixels.view(5, 10) +view.clear() +``` + +## Helpers + +Here are a few helpers built for `PixelBuffer`, but many other could be added! + +### `fillSolid` + +This helper function asigns the given color to the entire range. + +```ts +import { Led } from "@devicescript/core" +import { fillSolid } from "@devicescript/runtime" + +const led = new Led() +const pixels = await led.buffer() + +// highlight-next-line +fillSolid(pixels, 0x00_ff_00) + +await led.show() +``` + +### `fillGradient` + +Helper function that does a linear interpolation of two colors accross the RGB space. + +```ts +import { Led } from "@devicescript/core" +import { fillGradient } from "@devicescript/runtime" + +const led = new Led() +const pixels = await led.buffer() + +// highlight-next-line +fillGradient(pixels, 0x00_ff_00, 0x00_00_ff) + +await led.show() +``` + +### `fillBarGraph` + +A tiny bar chart engine to render on a value on a LED strip + +```ts +import { Led } from "@devicescript/core" +import { fillBarGraph } from "@devicescript/runtime" + +const led = new Led() +const pixels = await led.buffer() + +const current = 25 +const max = 100 + +// highlight-next-line +fillBarGraph(pixels, current, max) + +await led.show() +``` diff --git a/website/docs/api/runtime/index.md b/website/docs/api/runtime/schedule.mdx similarity index 82% rename from website/docs/api/runtime/index.md rename to website/docs/api/runtime/schedule.mdx index c9c7b5bf76..44c5416a0a 100644 --- a/website/docs/api/runtime/index.md +++ b/website/docs/api/runtime/schedule.mdx @@ -1,8 +1,4 @@ -# Runtime - -The `@devicescript/runtime` [builtin](/developer/packages) module provides additional runtime helpers. - -### `schedule` {#schedule} +# `schedule` The schedule function combines `setTimeout` and `setInterval` to provide a single function that can be used to schedule a function to run once or repeatedly. The function also tracks the number of invocation, total elapsed time and time since last invocation. diff --git a/website/docs/developer/graphics/display.mdx b/website/docs/developer/graphics/display.mdx index 6072602fd7..ca5462d969 100644 --- a/website/docs/developer/graphics/display.mdx +++ b/website/docs/developer/graphics/display.mdx @@ -25,6 +25,7 @@ on the simulated device (simulation does not work on hardware device as the comm import { SSD1306Driver, startIndexedScreen } from "@devicescript/drivers" const display = await startIndexedScreen( + // implements Display new SSD1306Driver({ width: 128, height: 64, devAddr: 0x3c }) ) display.image.print(`Hello world!`, 3, 10) diff --git a/website/docs/developer/leds/_category_.json b/website/docs/developer/leds/_category_.json new file mode 100644 index 0000000000..247b3e8875 --- /dev/null +++ b/website/docs/developer/leds/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "LEDs", + "position": 10.5 +} diff --git a/website/docs/developer/leds/display.mdx b/website/docs/developer/leds/display.mdx new file mode 100644 index 0000000000..2be0aa3c8f --- /dev/null +++ b/website/docs/developer/leds/display.mdx @@ -0,0 +1,27 @@ +--- +title: Display +--- + +# LED Display + +You can wrap an LED matrix as a [Display](/developer/graphics/display) +and render graphics to it as if it was just another screen. + +```ts +import { Led } from "@devicescript/core" +import { startLedDisplay } from "@devicescript/runtime" + +const led = new Led() +// highlight-next-line +const display = await startLedDisplay(led) +// render +display.image.print(`Hello world!`, 3, 10) +// and show +await display.show() +``` + +The palette is automatically infered from the `waveLength` register; otherwise you can provide one. + +```ts skip +const display = await startLedDisplay(led, palette) +``` diff --git a/website/docs/developer/leds/driver.mdx b/website/docs/developer/leds/driver.mdx new file mode 100644 index 0000000000..b5e94a9f26 --- /dev/null +++ b/website/docs/developer/leds/driver.mdx @@ -0,0 +1,27 @@ +--- +title: LED Driver +--- + +# LED Driver + +You can start a driver for WS2812 or AP102 using `startLed`. + +```ts +import { LedVariant } from "@devicescript/core" +import { startLed } from "@devicescript/drivers" + +// highlight-start +const led = await startLed({ + length: 32, + variant: LedVariant.Ring, +}) +// highlight-end +``` + +## Simulation and long strips (> 64 LEDs) + +For short LED strips, 64 LEDs or less, DeviceScript provides full simulation and device twin +for hardware and simulated devices. + +For strips longer than 64 LEDs, the simulator device will work but the hardware device twin will +not work anymore. This is a simple limitation that the data overflows the packets used in Jacdac. diff --git a/website/docs/developer/leds/index.mdx b/website/docs/developer/leds/index.mdx new file mode 100644 index 0000000000..cb4d7b5733 --- /dev/null +++ b/website/docs/developer/leds/index.mdx @@ -0,0 +1,86 @@ +--- +title: LEDs +--- + +# LEDs + +Controlling strips of programmable LEDs can be done through the `Led` client. + +**This client requires to import the `@devicescript/runtime` to get all the functionalities.** + +```ts +import { Led } from "@devicescript/core" +// highlight-next-line +import "@devicescript/runtime" + +const led = new Led() +``` + +## Driver + +You can start a driver for WS2812 or AP102 using [startLed](./leds/driver). + +```ts +import { LedVariant } from "@devicescript/core" +import { startLed } from "@devicescript/drivers" + +// highlight-start +const led = await startLed({ + length: 32, + variant: LedVariant.Ring, +}) +// highlight-end +``` + +:::tip + +Support for hardware strips is still under constructions. Currently, simulation is supported +but hardware in progress. + +::: + +## PixelBuffer and show + +The `Led` client has a [pixel buffer](/api/runtime/pixelbuffer), a 1D vector of colors, +that can be used to perform color operations, and a `show` function to render the buffer to the hardware. + +A typical LED program would then look like this: + +```ts +import { Led } from "@devicescript/core" +import { fillSolid } from "@devicescript/runtime" + +const led = new Led() +// retreive pixel buffer from led +const pixels = await led.buffer() +// do operations on pixels, like setting LEDs to green +fillSolid(pixels, 0x00ee00) +// send colors to hardware +await led.show() +``` + +## showAll + +A convenience function `showAll` is provided to set the color of all LEDs. + +```ts +import { Led } from "@devicescript/core" +import "@devicescript/runtime" + +const led = new Led() +// highlight-next-line +await led.showAll(0x00ee00) +``` + +## LED Display + +You can mount a LED matrix as a [display](./leds/display). + +```ts +import { Led } from "@devicescript/core" +import { startLedDisplay } from "@devicescript/runtime" + +const led = new Led() +// highlight-next-line +const display = await startLedDisplay(led) +``` diff --git a/website/docs/developer/status-light.mdx b/website/docs/developer/status-light.mdx index fb871f3938..3f5bd88e4c 100644 --- a/website/docs/developer/status-light.mdx +++ b/website/docs/developer/status-light.mdx @@ -1,14 +1,30 @@ --- sidebar_position: 1.1 description: Learn how to use the status light on DeviceScript to message - application states. Control the status LED until a system LED pattern is - scheduled by the runtime. + application states. Control the status LED until a system LED pattern is + scheduled by the runtime. --- # Status Light DeviceScript uses various blinking patterns on the status LED to message application states. +## Controlling the status led + +You can set the status LED (until a system LED pattern is scheduled by the runtime) +using the `setStatusLight` function. + +```ts +import { delay } from "@devicescript/core" +import { setStatusLight } from "@devicescript/runtime" + +setInterval(async () => { + await setStatusLight(0x00ff00) // green + await delay(500) + await setStatusLight(0x000000) // off +}, 500) +``` + ## Network connectivity - connecting to network: slow yellow glow @@ -28,19 +44,3 @@ DeviceScript uses various blinking patterns on the status LED to message applica - generic error: short red blink The blinking patterns are defined at https://github.com/microsoft/jacdac-c/blob/main/inc/jd_io.h#L77 - -## Controlling the status led - -You can also set the status LED (until a system LED pattern is scheduled by the runtime) -using the `setStatusLight` function. - -```ts -import { delay } from "@devicescript/core" -import { setStatusLight } from "@devicescript/runtime" - -setInterval(async () => { - await setStatusLight(0x00ff00) // green - await delay(500) - await setStatusLight(0x000000) // off -}, 500) -``` diff --git a/website/docs/samples/schedule-blinky.mdx b/website/docs/samples/schedule-blinky.mdx index 823e2e02fe..ed9067aea8 100644 --- a/website/docs/samples/schedule-blinky.mdx +++ b/website/docs/samples/schedule-blinky.mdx @@ -5,7 +5,7 @@ title: Schedule Blinky # Schedule Blinky -This sample blinks the onboard LED using the [schedule](/api/runtime#schedule) +This sample blinks the onboard LED using the [schedule](/api/runtime/schedule) ```ts import { schedule, setStatusLight } from "@devicescript/runtime"