diff --git a/ts/index.html b/ts/index.html
index a2b3345..30896d1 100644
--- a/ts/index.html
+++ b/ts/index.html
@@ -184,6 +184,22 @@
}).toBuffer();
},
},
+ {
+ label: "Time",
+ make() {
+ return new MonoDisplayFile({
+ elements_always: {
+ flags: { drawFront: true, clearBuffer: true },
+ elements: [{
+ type: ElementType.CurrentTime,
+ xOffset: 0, yOffset: 32,
+ width: W, height: 16,
+ utcOffsetMinutes: 120,
+ }],
+ },
+ }).toBuffer();
+ },
+ },
];
const controls = document.getElementById("controls");
diff --git a/ts/src/driver.ts b/ts/src/driver.ts
index ec99d6d..4f424e2 100644
--- a/ts/src/driver.ts
+++ b/ts/src/driver.ts
@@ -30,6 +30,12 @@ export interface MonoDisplayDriverOptions {
loop?: boolean;
/** Error handler. Default: throw. */
onError?: (err: Error) => void;
+ /**
+ * Clock source for CurrentTime elements. Called once per render tick.
+ * Default: `() => new Date()` (system time).
+ * Override to freeze or mock the clock, e.g. `() => new Date('2024-01-01T12:00:00Z')`.
+ */
+ now?: () => Date;
}
@@ -66,6 +72,7 @@ export class MonoDisplayDriver {
fps: options.fps ?? 25,
loop: options.loop ?? true,
onError: options.onError ?? ((e: Error) => { throw e; }),
+ now: options.now ?? (() => new Date()),
};
this.parser = new MonoDisplayParser();
}
diff --git a/ts/src/file.ts b/ts/src/file.ts
index 1d240e0..8ea4431 100644
--- a/ts/src/file.ts
+++ b/ts/src/file.ts
@@ -4,7 +4,7 @@
import { packedSize, packPixels, pad32 } from "./helper";
import { MONOFORMAT_MAGIC_HEADER } from "./parser";
-import { ElementType, SectionType, type AnimationElement, type ClippedTextElement, type DrawElement, type ElementsAlwaysDescriptor, type HScrollElement, type HScrollTextElement, type Image2DElement, type LineElement, type MonoDisplayFileDescriptor, type ScrollElementFlags, type SectionFlags, type VScrollElement } from "./types";
+import { ElementType, SectionType, type AnimationElement, type ClippedTextElement, type CurrentTimeElement, type DrawElement, type ElementsAlwaysDescriptor, type HScrollElement, type HScrollTextElement, type Image2DElement, type LineElement, type MonoDisplayFileDescriptor, type ScrollElementFlags, type SectionFlags, type VScrollElement } from "./types";
/**
* Encodes a MonoDisplayFileDescriptor to a valid .bin ArrayBuffer.
@@ -85,6 +85,7 @@ export class MonoDisplayFile {
case ElementType.Line: return this.#encodeLine(el);
case ElementType.ClippedText: return this.#encodeClippedText(el);
case ElementType.HScrollText: return this.#encodeHScrollText(el);
+ case ElementType.CurrentTime: return this.#encodeCurrentTime(el);
default: return new Uint8Array();
}
}
@@ -230,6 +231,28 @@ export class MonoDisplayFile {
return out;
}
+ #encodeCurrentTime(el: CurrentTimeElement): Uint8Array {
+ // Fixed 16 bytes, already 32-bit aligned
+ const out = new Uint8Array(16);
+ const v = new DataView(out.buffer);
+ const f = el.flags;
+ const flagsByte =
+ (f?.clock12h ? 0x01 : 0) |
+ (f?.showHours ?? true ? 0x02 : 0) |
+ (f?.showMinutes ?? true ? 0x04 : 0) |
+ (f?.showSeconds ? 0x08 : 0);
+ v.setUint16( 0, ElementType.CurrentTime, true);
+ v.setUint16( 2, el.xOffset ?? 0, true);
+ v.setUint16( 4, el.yOffset ?? 0, true);
+ v.setUint16( 6, el.width, true);
+ v.setUint16( 8, el.height, true);
+ v.setUint16(10, el.fontIndex ?? 0, true);
+ v.setInt16( 12, el.utcOffsetMinutes ?? 0, true);
+ v.setUint8( 14, flagsByte);
+ v.setUint8( 15, 0); // reserved
+ return out;
+ }
+
// --- utilities ---
#concat(...parts: Uint8Array[]): Uint8Array {
diff --git a/ts/src/parser.ts b/ts/src/parser.ts
index 536609a..4b8106f 100644
--- a/ts/src/parser.ts
+++ b/ts/src/parser.ts
@@ -5,7 +5,7 @@
import { packedSize, packPixels, pad32, unpackPixels, BinaryReader } from "./helper";
import { type MonoFormatFile } from "./types";
-import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatPixelImage, type MonoFormatSection, type MonoFormatSectionFlags, type MonoFormatVScroll } from "./types";
+import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCurrentTime, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatPixelImage, type MonoFormatSection, type MonoFormatSectionFlags, type MonoFormatVScroll } from "./types";
// Magic 0xAF 0x7E 0x2B 0x63 read as uint32 LE
export const MONOFORMAT_MAGIC_HEADER = 0x632B7EAF;
@@ -116,6 +116,7 @@ export class MonoDisplayParser {
case ElementType.Line: return this.#parseLine(r);
case ElementType.ClippedText: return this.#parseClippedText(r, elementStart);
case ElementType.HScrollText: return this.#parseHScrollText(r, elementStart);
+ case ElementType.CurrentTime: return this.#parseCurrentTime(r);
default:
// Unknown element type - cannot safely skip without knowing size; stop parsing section elements
return null;
@@ -238,6 +239,28 @@ export class MonoDisplayParser {
return { type: ElementType.ClippedText, xOffset, yOffset, width, height, fontIndex, text };
}
+ #parseCurrentTime(r: BinaryReader): MonoFormatCurrentTime {
+ // Layout after type field (already consumed): 14 bytes, total element = 16 bytes (aligned)
+ const xOffset = r.readUint16();
+ const yOffset = r.readUint16();
+ const width = r.readUint16();
+ const height = r.readUint16();
+ const fontIndex = r.readUint16();
+ const utcOffsetMinutes = r.readInt16();
+ const flagsByte = r.readByte();
+ r.readByte(); // reserved
+ return {
+ type: ElementType.CurrentTime,
+ xOffset, yOffset, width, height, fontIndex, utcOffsetMinutes,
+ flags: {
+ clock12h: !!(flagsByte & 0x01),
+ showHours: !!(flagsByte & 0x02),
+ showMinutes: !!(flagsByte & 0x04),
+ showSeconds: !!(flagsByte & 0x08),
+ },
+ };
+ }
+
#parseHScrollText(r: BinaryReader, start: number): MonoFormatHScrollText {
const xOffset = r.readUint16();
const yOffset = r.readUint16();
diff --git a/ts/src/renderer.ts b/ts/src/renderer.ts
index b1c5a82..c2a1e13 100644
--- a/ts/src/renderer.ts
+++ b/ts/src/renderer.ts
@@ -4,6 +4,7 @@ import {
SectionType,
type MonoFormatAnimation,
type MonoFormatClippedText,
+ type MonoFormatCurrentTime,
type MonoFormatElement,
type MonoFormatElementsAlways,
type MonoFormatElementsTimespan,
@@ -82,7 +83,7 @@ export class MonoDisplayRenderer {
}
#renderFile(file: MonoFormatFile): void {
- const now = BigInt(Math.floor(Date.now() / 1000));
+ const now = BigInt(Math.floor(this.opts.now().getTime() / 1000));
let cleared = false;
let elementId = 0;
@@ -124,6 +125,7 @@ export class MonoDisplayRenderer {
case ElementType.Line: this.#renderLine(el); break;
case ElementType.ClippedText: this.#renderClippedText(el); break;
case ElementType.HScrollText: this.#renderHScrollText(el, id); break;
+ case ElementType.CurrentTime: this.#renderCurrentTime(el); break;
}
}
@@ -252,4 +254,23 @@ export class MonoDisplayRenderer {
}
this.hScrollPos.set(id, pos);
}
+
+ #renderCurrentTime(el: MonoFormatCurrentTime): void {
+ const d = new Date(this.opts.now().getTime() + el.utcOffsetMinutes * 60_000);
+ const { clock12h, showHours, showMinutes, showSeconds } = el.flags;
+
+ const parts: string[] = [];
+ if (showHours) {
+ const h = d.getUTCHours();
+ parts.push(String(clock12h ? (h % 12 || 12) : h).padStart(2, '0'));
+ }
+ if (showMinutes) parts.push(String(d.getUTCMinutes()).padStart(2, '0'));
+ if (showSeconds) parts.push(String(d.getUTCSeconds()).padStart(2, '0'));
+
+ const text = parts.join(':');
+ if (!text) return;
+
+ const img = this.#getTextImage(text, el.height, el.fontIndex);
+ this.#blitClipped(img, el.xOffset, el.yOffset, el.width, el.height, 0, 0);
+ }
}
diff --git a/ts/src/types.ts b/ts/src/types.ts
index 168bc48..ba6a322 100644
--- a/ts/src/types.ts
+++ b/ts/src/types.ts
@@ -31,12 +31,6 @@ export enum ElementType {
Line = 5,
ClippedText = 16,
HScrollText = 17,
- /**
- * Current time display.
- * NOTE: the spec diagram labels this element "Type: 16", which collides with
- * ClippedText. This is assumed to be a spec typo; 18 is used here as the
- * probable intended value until the spec is corrected.
- */
CurrentTime = 18,
}