backup
flop 6 days ago
parent 59d683dbd2
commit 70c29b12ea
  1. 3
      backup/.htaccess
  2. 1
      backup/active.txt
  3. 5097
      backup/alphabet-extended.js
  4. 1756
      backup/app.js
  5. BIN
      backup/avj2305/Artikel1.bin
  6. BIN
      backup/avj2305/Artikel1.png
  7. BIN
      backup/avj2305/Artikel21.bin
  8. BIN
      backup/avj2305/Artikel21.png
  9. BIN
      backup/avj2305/Frosch161.bin
  10. BIN
      backup/avj2305/Frosch161.png
  11. BIN
      backup/avj2305/LaufSpruch1.bin
  12. BIN
      backup/avj2305/LaufSpruch1.png
  13. BIN
      backup/avj2305/LaufSpruch2.bin
  14. BIN
      backup/avj2305/LaufSpruch2.png
  15. BIN
      backup/avj2305/LaufSpruch3.bin
  16. BIN
      backup/avj2305/LaufSpruch3.png
  17. BIN
      backup/avj2305/MvAVJ.bin
  18. BIN
      backup/avj2305/MvAVJ.png
  19. BIN
      backup/avj2305/NotWidersetzen.bin
  20. BIN
      backup/avj2305/NotWidersetzen.png
  21. BIN
      backup/avj2305/Schildkroete.bin
  22. BIN
      backup/avj2305/Schildkroete.png
  23. BIN
      backup/avj2305/Widersetzen.bin
  24. BIN
      backup/avj2305/Widersetzen.png
  25. BIN
      backup/avj2305/Widersetzen1.bin
  26. BIN
      backup/avj2305/Widersetzen1.png
  27. BIN
      backup/avj2305/Zitat1.bin
  28. BIN
      backup/avj2305/Zitat1.png
  29. BIN
      backup/avj2305/Zitat2.bin
  30. BIN
      backup/avj2305/Zitat2.png
  31. BIN
      backup/avj2305/Zitat3.bin
  32. BIN
      backup/avj2305/Zitat3.png
  33. 165
      backup/binfmt.js
  34. 112
      backup/editor/index.html
  35. 5302
      backup/editor/public/mono-display.js
  36. 682
      backup/editor/style.css
  37. BIN
      backup/favicon.ico
  38. BIN
      backup/gpn/endscreen.bin
  39. 43
      backup/gpn/monoformat_bithelpers.php
  40. 213
      backup/gpn/monoformat_schema.php
  41. 1925
      backup/gpn/monoformat_structured.php
  42. BIN
      backup/gpn/noroom.bin
  43. 181
      backup/gpn/schedule.php
  44. BIN
      backup/gpn/template.bin
  45. 83
      backup/index.html
  46. 215
      backup/live.php
  47. BIN
      backup/out/cttue.disabled
  48. BIN
      backup/out/franzwerk.bin
  49. BIN
      backup/output.bin
  50. 203
      backup/schedule_bool.php
  51. 203
      backup/schedule_enum.php
  52. 53
      backup/show.php
  53. 84
      backup/styles.css
  54. 105
      backup/upload.php

@ -0,0 +1,3 @@
RewriteEngine on
RewriteCond %{REQUEST_URI} \.bin$ [NC]
RewriteRule ^(.*\.bin)$ /busanzeiger-webinterface/src/show.php [L,QSA]

@ -0,0 +1 @@
Schildkroete.bin

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

@ -0,0 +1,165 @@
// Binary Framebuffer Encoder
// Converts pixel data to binary format for bus display
const MAGIC = [66, 78, 23, 238];
const DrawEntryType = {
Image: 1,
Animation: 2,
HScroll: 3,
VScroll: 4,
Line: 5
};
function bitmapSizeBytes(width, height) {
return Math.floor((width * height + 7) / 8);
}
function setPixelInBitmap(x, y, memory, width, height, value) {
const index = y * width + x;
const byteIndex = Math.floor(index / 8);
const bitIndex = index % 8;
if (value) {
memory[byteIndex] |= 1 << bitIndex;
} else {
memory[byteIndex] &= ~(1 << bitIndex);
}
}
class BinaryFramebufferEncoder {
entries = [];
addImage(posX, posY, width, height, pixelData) {
const data = new Uint8Array(bitmapSizeBytes(width, height));
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
setPixelInBitmap(x, y, data, width, height, pixelData[y][x]);
}
}
this.entries.push({
type: DrawEntryType.Image,
header: { posX, posY },
width,
height,
data
});
}
addAnimation(posX, posY, width, height, frameCount, updateInterval, frames) {
const frameSize = bitmapSizeBytes(width, height);
const data = new Uint8Array(frameSize * frameCount);
for (let f = 0; f < frameCount; f++) {
const frameData = data.subarray(f * frameSize, (f + 1) * frameSize);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
setPixelInBitmap(x, y, frameData, width, height, frames[f][y][x]);
}
}
}
this.entries.push({
type: DrawEntryType.Animation,
header: { posX, posY },
width,
height,
frameCount,
updateInterval,
data
});
}
encode() {
let totalSize = 5;
for (const entry of this.entries) {
totalSize += 3;
switch (entry.type) {
case DrawEntryType.Image:
totalSize += 2 + entry.data.length;
break;
case DrawEntryType.Animation:
totalSize += 4 + entry.data.length;
break;
}
}
const buffer = new Uint8Array(totalSize);
let pos = 0;
buffer.set(MAGIC, pos);
pos += 4;
buffer[pos++] = this.entries.length;
for (const entry of this.entries) {
buffer[pos++] = entry.type;
buffer[pos++] = entry.header.posX;
buffer[pos++] = entry.header.posY;
switch (entry.type) {
case DrawEntryType.Image:
buffer[pos++] = entry.width;
buffer[pos++] = entry.height;
buffer.set(entry.data, pos);
pos += entry.data.length;
break;
case DrawEntryType.Animation:
buffer[pos++] = entry.width;
buffer[pos++] = entry.height;
buffer[pos++] = entry.frameCount;
buffer[pos++] = entry.updateInterval;
buffer.set(entry.data, pos);
pos += entry.data.length;
break;
}
}
return buffer;
}
}
function parseJSONFrames(text, width = 120, height = 60, maxFrames = 50) {
const json = JSON.parse(text);
if (!Array.isArray(json)) {
throw new Error("JSON must be an array of frames");
}
const frames = [];
for (const frame of json) {
if (!Array.isArray(frame)) {
throw new Error("Each frame must be an array of coordinates");
}
const pixels = Array(height).fill(null).map(() => Array(width).fill(false));
for (const change of frame) {
const [x, y] = change;
if (y < pixels.length && x < pixels[y].length) {
pixels[y][x] = true;
}
}
if (frames.length >= maxFrames) continue;
frames.push(pixels);
}
return frames;
}
function jsonToBinary(jsonString, options = {}) {
const {
startX = 0,
startY = 0,
updateInterval = 3,
width = 120,
height = 60,
maxFrames = 50
} = options;
const encoder = new BinaryFramebufferEncoder();
const frames = parseJSONFrames(jsonString, width, height, maxFrames);
if (frames.length === 1) {
encoder.addImage(startX, startY, width, height, frames[0]);
} else {
encoder.addAnimation(startX, startY, width, height, frames.length, updateInterval, frames);
}
return encoder.encode();
}
// Helper function to drop every nth frame from a frame array
function dropEveryNthFrame(frames, n) {
if (n <= 1 || frames.length <= 1) return frames;
return frames.filter((frame, index) => (index + 1) % n !== 0);
}
// Helper function to keep 1 frame, drop 2 frames in repeating pattern
// Pattern: keep, drop, drop, keep, drop, drop, ...
function keepOneDropTwo(frames) {
if (frames.length <= 1) return frames;
return frames.filter((frame, index) => index % 3 === 0);
}

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>MonoDisplay Editor</title>
<script>!function(){var t=localStorage.getItem('theme');t&&document.documentElement.setAttribute('data-theme',t)}();</script>
<link href="style.css" rel="stylesheet"/>
</head>
<body>
<div id="topbar">
<span class="app-name">MonoDisplay</span>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="document.getElementById('file-input').click()">
<svg viewBox="0 0 24 24">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
Load .bin
</button>
<input type="file" id="file-input" accept=".bin" onchange="loadBin(this)" />
<button class="tb-btn" onclick="exportBin()">
<svg viewBox="0 0 24 24">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
Export .bin
</button>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="clearAll()">
<svg viewBox="0 0 24 24">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14H6L5 6" />
<path d="M10 11v6M14 11v6" />
</svg>
Clear all
</button>
<button class="tb-btn" onclick="addSection()">
<svg viewBox="0 0 24 24">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
Add section
</button>
<span id="filename">untitled</span>
<div style="flex:1"></div>
<div class="tb-sep"></div>
<button class="tb-btn" id="theme-toggle" onclick="cycleTheme()">⊙ auto</button>
</div>
<div id="main">
<div id="left">
<span class="pv-label">preview · 120 × 60</span>
<div id="display-box">
<canvas id="canvas_root" width="120" height="60"></canvas>
</div>
<span class="pv-label">demos</span>
<div id="demo-btns"></div>
<div id="sec-meta">
<div class="meta-row">
<label>Selected section</label>
<span class="meta-val green" id="meta-name">-</span>
</div>
<div class="meta-row">
<label>Elements</label>
<span class="meta-val" id="meta-count">-</span>
</div>
<div class="meta-row">
<label>Section flags</label>
<label class="flag-check">
<input type="checkbox" id="flag-drawFront" onchange="setSectionFlag('drawFront',this.checked)" />
drawFront
</label>
<label class="flag-check" style="margin-left:-8px">
<input type="checkbox" id="flag-drawBack" onchange="setSectionFlag('drawBack',this.checked)" />
drawBack
</label>
<label class="flag-check" style="margin-left:-8px">
<input type="checkbox" id="flag-clearBuffer" onchange="setSectionFlag('clearBuffer',this.checked)" />
clearBuffer
</label>
</div>
</div>
</div>
<div id="right">
<div id="right-hdr">
<span class="rh-title">sections</span>
</div>
<div id="sections-wrap">
<div class="empty-state">No sections yet.<br />Use <b>Add section</b> to get started.</div>
</div>
</div>
</div>
<!-- confirm dialog -->
<div id="confirm-overlay">
<div id="confirm-box">
<div id="confirm-msg"></div>
<div id="confirm-btns"></div>
</div>
</div>
<script src="./public/mono-display.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

@ -0,0 +1,682 @@
:root {
--bg: #111;
--bg-bar: #1a1a1a;
--bg-raised: #161616;
--bg-hover: #1b1b1b;
--bg-hover2: #222;
--bg-sunken: #141414;
--bg-accent: #141e14;
--bg-active: #1e2e1e;
--bg-deep: #0f0f0f;
--bg-input: #0a0a0a;
--bg-canvas: #000;
--bg-xhover: #1e1010;
--bg-overlay: rgba(0,0,0,.6);
--bd: #2a2a2a;
--bd-inner: #1e1e1e;
--bd-deep: #1c1c1c;
--bd-card: #222;
--bd-btn: #2d2d2d;
--bd-type: #252525;
--bd-active: #2b3d2b;
--bd-hover: #444;
--bd-strong: #333;
--tx: #ccc;
--tx-hover: #bbb;
--tx-sub: #888;
--tx-label: #777;
--tx-meta: #666;
--tx-dim: #555;
--tx-faint: #444;
--tx-ghost: #333;
--tx-file: #3a3a3a;
--tx-empty: #2e2e2e;
--ac: #33ff66;
--dirty: #886622
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg: #f5f5f5;
--bg-bar: #e8e8e8;
--bg-raised: #efefef;
--bg-hover: #e5e5e5;
--bg-hover2: #e0e0e0;
--bg-sunken: #ebebeb;
--bg-accent: #e8f5e8;
--bg-active: #dff0df;
--bg-deep: #f0f0f0;
--bg-input: #fff;
--bg-canvas: #fff;
--bg-xhover: #ffe8e8;
--bg-overlay: rgba(0,0,0,.4);
--bd: #d0d0d0;
--bd-inner: #ddd;
--bd-deep: #e0e0e0;
--bd-card: #ddd;
--bd-btn: #ccc;
--bd-type: #d8d8d8;
--bd-active: #7dc87d;
--bd-hover: #bbb;
--bd-strong: #ccc;
--tx: #222;
--tx-hover: #333;
--tx-sub: #555;
--tx-label: #666;
--tx-meta: #777;
--tx-dim: #888;
--tx-faint: #999;
--tx-ghost: #aaa;
--tx-file: #aaa;
--tx-empty: #bbb;
--ac: #1a9940;
--dirty: #b37a00
}
}
[data-theme="light"] {
--bg: #f5f5f5;
--bg-bar: #e8e8e8;
--bg-raised: #efefef;
--bg-hover: #e5e5e5;
--bg-hover2: #e0e0e0;
--bg-sunken: #ebebeb;
--bg-accent: #e8f5e8;
--bg-active: #dff0df;
--bg-deep: #f0f0f0;
--bg-input: #fff;
--bg-canvas: #fff;
--bg-xhover: #ffe8e8;
--bg-overlay: rgba(0,0,0,.4);
--bd: #d0d0d0;
--bd-inner: #ddd;
--bd-deep: #e0e0e0;
--bd-card: #ddd;
--bd-btn: #ccc;
--bd-type: #d8d8d8;
--bd-active: #7dc87d;
--bd-hover: #bbb;
--bd-strong: #ccc;
--tx: #222;
--tx-hover: #333;
--tx-sub: #555;
--tx-label: #666;
--tx-meta: #777;
--tx-dim: #888;
--tx-faint: #999;
--tx-ghost: #aaa;
--tx-file: #aaa;
--tx-empty: #bbb;
--ac: #1a9940;
--dirty: #b37a00
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0
}
body {
background: var(--bg);
color: var(--tx);
font-family: monospace;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
font-size: 12px
}
#topbar {
height: 36px;
background: var(--bg-bar);
border-bottom: 1px solid var(--bd);
display: flex;
align-items: center;
gap: 8px;
padding: 0 12px;
flex-shrink: 0
}
#topbar .app-name {
color: var(--tx-dim);
font-size: 11px;
letter-spacing: .12em;
text-transform: uppercase;
margin-right: 4px
}
.tb-btn {
background: var(--bg-raised);
color: var(--tx-sub);
border: 1px solid var(--bd-btn);
border-radius: 3px;
padding: 3px 10px;
font-family: monospace;
font-size: 11px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap
}
.tb-btn:hover {
background: var(--bg-hover2);
color: var(--tx-hover);
border-color: var(--bd-hover)
}
.tb-btn svg {
width: 13px;
height: 13px;
stroke: currentColor;
fill: none;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
flex-shrink: 0
}
#file-input {
display: none
}
#filename {
color: var(--tx-file);
font-size: 11px;
margin-left: 2px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.tb-sep {
width: 1px;
height: 18px;
background: var(--bd)
}
.dirty {
color: var(--dirty) !important
}
#main {
flex: 1;
display: flex;
min-height: 0
}
#left {
width: 48%;
border-right: 1px solid var(--bd-inner);
display: flex;
flex-direction: column;
padding: 14px;
gap: 10px;
overflow-y: auto;
flex-shrink: 0
}
#display-box {
width: 100%;
aspect-ratio: 2/1;
background: var(--bg-canvas);
border: 1px solid var(--bd);
border-radius: 3px;
overflow: hidden
}
#canvas_root {
width: 100%;
height: 100%;
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges
}
.pv-label {
font-size: 10px;
color: var(--tx-ghost);
letter-spacing: .1em;
text-transform: uppercase
}
#demo-btns {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 6px
}
#demo-btns > div {
display: contents;
}
canvas.pixel-editor {
display: block;
cursor: crosshair;
image-rendering: pixelated;
max-width: 100%;
border: 1px solid var(--bd-inner);
}
.anim-editor {
display: flex;
flex-direction: column;
gap: 4px;
}
.anim-frame-tabs {
display: flex;
flex-wrap: wrap;
gap: 3px;
align-items: center;
}
.anim-frame-tab {
display: flex;
align-items: center;
gap: 2px;
padding: 2px 6px;
border: 1px solid var(--bd-inner);
border-radius: 3px;
cursor: pointer;
font-size: 11px;
user-select: none;
}
.anim-frame-tab.active {
border-color: var(--accent, #EC0);
color: var(--accent, #EC0);
}
.x-btn-sm {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0 2px;
font-size: 13px;
line-height: 1;
opacity: 0.6;
}
.x-btn-sm:hover { opacity: 1; }
#sec-meta {
background: var(--bg-sunken);
border: 1px solid var(--bd-inner);
border-radius: 3px;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 6px
}
.meta-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px
}
.meta-row label {
color: var(--tx-faint);
min-width: 90px
}
.meta-val {
color: var(--tx-meta)
}
.green {
color: var(--ac)
}
select.meta-val {
background: var(--bg-input);
color: var(--ac);
border: 1px solid var(--bd-inner);
border-radius: 3px;
padding: 1px 4px;
}
.flag-check {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--tx-dim);
cursor: pointer
}
.flag-check input {
accent-color: var(--ac);
cursor: pointer
}
#right {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden
}
#right-hdr {
height: 36px;
background: var(--bg-sunken);
border-bottom: 1px solid var(--bd-inner);
display: flex;
align-items: center;
padding: 0 10px;
gap: 6px;
flex-shrink: 0
}
#right-hdr .rh-title {
flex: 1;
color: var(--tx-faint);
font-size: 11px;
letter-spacing: .08em;
text-transform: uppercase
}
#sections-wrap {
flex: 1;
overflow-y: auto;
padding: 8px
}
.empty-state {
color: var(--tx-empty);
font-size: 11px;
padding: 28px 16px;
text-align: center;
line-height: 2
}
/* section card */
.sec-card {
border: 1px solid var(--bd-card);
border-radius: 3px;
margin-bottom: 5px;
overflow: hidden
}
.sec-card.active {
border-color: var(--bd-active)
}
.sec-hdr {
display: flex;
align-items: center;
gap: 7px;
padding: 6px 8px;
cursor: pointer;
user-select: none;
background: var(--bg-raised);
position: relative
}
.sec-hdr:hover {
background: var(--bg-hover)
}
.sec-card.active .sec-hdr,
.sec-hdr.active {
background: var(--bg-active)
}
.sec-arrow {
color: var(--tx-ghost);
font-size: 13px;
line-height: 1;
transition: transform .12s;
flex-shrink: 0
}
.sec-card.open .sec-arrow {
transform: rotate(90deg)
}
.sec-label {
flex: 1;
color: var(--tx-label);
font-size: 11px
}
.sec-badge {
font-size: 10px;
color: var(--ac);
border: 1px solid var(--bd-active);
background: var(--bg-accent);
border-radius: 2px;
padding: 1px 6px;
flex-shrink: 0
}
/* red × - section & element */
.x-btn {
background: none;
border: none;
color: var(--bd);
cursor: pointer;
font-size: 15px;
line-height: 1;
padding: 2px 5px;
border-radius: 2px;
flex-shrink: 0;
transition: color .1s, background .1s
}
.x-btn:hover {
color: #ff4444;
background: var(--bg-xhover)
}
/* element */
.el-list {
background: var(--bg-deep);
border-top: 1px solid var(--bd-deep);
padding: 6px
}
.el-item {
border: 1px solid var(--bd-inner);
border-radius: 3px;
background: var(--bg-sunken);
margin-bottom: 4px;
overflow: hidden
}
.el-item.active {
border-color: var(--ac)
}
.el-item-hdr {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 8px;
cursor: pointer
}
.el-item-hdr:hover {
background: var(--bg-hover)
}
.el-type {
font-size: 10px;
color: var(--tx-meta);
border: 1px solid var(--bd-type);
border-radius: 2px;
padding: 1px 6px;
flex-shrink: 0
}
.el-item.active .el-type {
color: var(--ac);
border-color: var(--bd-active)
}
.el-name {
flex: 1;
color: var(--tx-dim);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px
}
.el-fields {
padding: 6px 8px 8px;
border-top: 1px solid var(--bg-bar);
display: flex;
flex-direction: column;
gap: 5px
}
.fields-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 10px
}
.field {
display: flex;
flex-direction: column;
gap: 2px
}
.field.full {
grid-column: 1/-1
}
.field>label {
font-size: 10px;
color: var(--tx-faint)
}
.field input[type=text],
.field input[type=number],
.field input[type=datetime-local],
.field select,
.field textarea {
background: var(--bg-input);
color: var(--tx-sub);
border: 1px solid var(--bd-card);
border-radius: 2px;
padding: 3px 6px;
font-family: monospace;
font-size: 11px;
width: 100%;
outline: none
}
.field input:focus,
.field select:focus,
.field textarea:focus {
border-color: var(--ac);
color: var(--tx)
}
.field textarea {
resize: vertical;
min-height: 44px;
line-height: 1.4
}
.field select option {
background: var(--bg-bar);
color: var(--tx-ghost)
}
.flags-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 2px 0
}
.add-el {
background: transparent;
color: var(--tx-ghost);
border: 1px dashed var(--bd-card);
border-radius: 3px;
padding: 5px 8px;
font-family: monospace;
font-size: 11px;
cursor: pointer;
width: 100%;
text-align: center;
margin-top: 2px
}
.add-el:hover {
color: var(--tx-sub);
border-color: var(--bd-hover)
}
/* confirm dialog */
#confirm-overlay {
display: none;
position: fixed;
inset: 0;
background: var(--bg-overlay);
z-index: 100;
align-items: center;
justify-content: center
}
#confirm-overlay.show {
display: flex
}
#confirm-box {
background: var(--bg-bar);
border: 1px solid var(--bd-strong);
border-radius: 4px;
padding: 20px 24px;
max-width: 340px;
width: 90%;
display: flex;
flex-direction: column;
gap: 14px
}
#confirm-msg {
color: var(--tx-ghost);
font-size: 12px;
line-height: 1.6
}
#confirm-btns {
display: flex;
gap: 8px;
justify-content: flex-end
}
.cb {
background: var(--bg-raised);
color: var(--tx-sub);
border: 1px solid var(--bd-btn);
border-radius: 3px;
padding: 4px 14px;
font-family: monospace;
font-size: 11px;
cursor: pointer
}
.cb:hover {
background: var(--bg-hover2);
color: var(--tx-hover)
}
.cb.primary {
border-color: var(--bd-active);
color: var(--ac)
}
.cb.primary:hover {
background: var(--bg-active)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

@ -0,0 +1,43 @@
<?php
namespace monoformat {
function isBitSet(int $value, int $bit) {
if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) {
return false;
}
$mask = 1 << $bit;
return ($value & $mask) == $mask;
}
function setBit(int &$value, int $bit) {
if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) {
return;
}
$mask = 1 << $bit;
$value |= $mask;
}
function unsetBit(int &$value, int $bit) {
if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) {
return;
}
$mask = 1 << $bit;
$value &= ~$mask;
}
function maybeSetBit(int &$value, int $bit, int $set) {
if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) {
return;
}
$mask = 1 << $bit;
if ($set) {
$value |= $mask;
} else {
$value &= ~$mask;
}
}
} // namespace monoformat
?>

@ -0,0 +1,213 @@
<?php
namespace monoformat {
include_once("monoformat_bithelpers.php");
trait FlagStruct {
public static function fromJSON(array $json) {
$myClass = static::class;
$reflect = new \ReflectionClass($myClass);
$properties = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC);
$result = new $myClass();
foreach ($properties as $property) {
$name = $property->getName();
if (array_key_exists($name, $json)) {
$result->$name = (bool) $json[$name];
}
}
return $result;
}
public function toJSON(): array {
$result = [];
$myClass = static::class;
$reflect = new \ReflectionClass($myClass);
$properties = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC);
foreach ($properties as $property) {
$name = $property->getName();
$result[$name] = $this->$name;
}
return $result;
}
}
enum SectionType : int {
case AlwaysDrawn = 1;
case TimeBasedDrawn = 2;
case MultiTimeBasedDrawn = 3;
case ExpiryDate = 31;
case CustomFont = 32;
public static function fromName(string $name): SectionType {
foreach (self::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
throw new \ValueError("$name is not a valid backing value for enum " . self::class);
}
}
enum ElementType : int {
case Image = 1;
case Animation = 2;
case HScrollImage = 3;
case VScrollImage = 4;
case Line = 5;
case Box = 6;
case ClippedText = 16;
case HScrollText = 17;
//case VScrollText = 18;
case CurrentTime = 32;
public static function fromName(string $name): SectionType {
foreach (self::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
throw new \ValueError("$name is not a valid backing value for enum " . self::class);
}
}
class ScrollFlags {
use FlagStruct;
public bool $endless = false;
public bool $invertDirection = false;
public bool $padBefore = false;
public bool $padAfter = false;
public static function fromBitField(int $bitfield): ScrollFlags {
$result = new ScrollFlags();
$result->endless = isBitSet($bitfield, 0);
$result->invertDirection = isBitSet($bitfield, 1);
$result->padBefore = isBitSet($bitfield, 2);
$result->padAfter = isBitSet($bitfield, 3);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->endless);
maybeSetBit($result, 1, $this->invertDirection);
maybeSetBit($result, 2, $this->padBefore);
maybeSetBit($result, 3, $this->padAfter);
return $result;
}
}
enum LineStyle : int {
case Solid = 0;
public static function fromName(string $name): SectionType {
foreach (self::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
throw new \ValueError("$name is not a valid backing value for enum " . self::class);
}
}
class LineFlags {
use FlagStruct;
public bool $dark = false;
public static function fromBitField(int $bitfield): LineFlags {
$result = new LineFlags();
$result->dark = isBitSet($bitfield, 0);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->dark);
return $result;
}
}
enum FillPatternStyle : int {
case Solid = 0;
public static function fromName(string $name): SectionType {
foreach (self::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
throw new \ValueError("$name is not a valid backing value for enum " . self::class);
}
}
class FillFlags {
use FlagStruct;
public bool $dark = false;
public static function fromBitField(int $bitfield): FillFlags {
$result = new FillFlags();
$result->dark = isBitSet($bitfield, 0);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->dark);
return $result;
}
}
class TextFlags {
use FlagStruct;
public bool $dark = false;
public bool $autoWordWrap = false;
public static function fromBitField(int $bitfield): TextFlags {
$result = new TextFlags();
$result->dark = isBitSet($bitfield, 0);
$result->autoWordWrap = isBitSet($bitfield, 1);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->dark);
maybeSetBit($result, 1, $this->autoWordWrap);
return $result;
}
}
class TimeDisplayFlags {
use FlagStruct;
public bool $use12h = false;
public bool $showHours = false;
public bool $showMinutes = false;
public bool $showSeconds = false;
public static function fromBitField(int $bitfield): TimeDisplayFlags {
$result = new TimeDisplayFlags();
$result->use12h = isBitSet($bitfield, 0);
$result->showHours = isBitSet($bitfield, 1);
$result->showMinutes = isBitSet($bitfield, 2);
$result->showSeconds = isBitSet($bitfield, 3);
return $result;
}
public function toBitField(): int {
$result = 0;
maybeSetBit($result, 0, $this->use12h);
maybeSetBit($result, 1, $this->showHours);
maybeSetBit($result, 2, $this->showMinutes);
maybeSetBit($result, 3, $this->showSeconds);
return $result;
}
}
} // namespace monoformat
?>

File diff suppressed because it is too large Load Diff

Binary file not shown.

@ -0,0 +1,181 @@
<?php
include_once("monoformat_schema.php");
include_once("monoformat_structured.php");
function replaceTalkInfo($str, $talks) {
return preg_replace_callback("/\\$\\{([^}]*)\\}/", function ($matches) use ($talks) {
if (str_contains($matches[1], ".")) {
[$id, $what] = explode(".", $matches[1], 2);
$id = (int) $id;
if ($id < 0 || $id >= count($talks)) {
return "";
}
$talk = $talks[$id];
if (array_key_exists($what, $talk)) {
return $talk[$what];
} else {
return "";
}
} else {
return "";
}
}, $str);
}
$roomMapping = [
"1020ba76eb04" => "a873f07f-4ab2-57ea-9183-99b187e8b0cf", // Medientheater
"001122334466" => "0273ef15-d23a-5413-9c74-3edc092b6ee8", // Kubus
"001122334477" => "80d66dbe-d978-57b5-86e2-b3d480136b9a", // Vortragssaal
];
if (!isset($_GET["mac"]) or !array_key_exists($_GET["mac"], $roomMapping)) {
/* We didn't find this device in the mapping, so let's just
* send a default file to the user.
*/
Header("Content-Type: application/octet-stream");
readfile(dirname(__FILE__) . "/noroom.bin");
exit(0);
}
$roomGUID = strtolower($roomMapping[$_GET["mac"]]);
$pretalx = json_decode(file_get_contents("https://cfp.gulas.ch/gpn24/schedule/export/schedule.json"), true);
$roomName = null;
foreach ($pretalx["schedule"]["conference"]["rooms"] as $room) {
$thisRoomGUID = strtolower($room["guid"]);
if ($thisRoomGUID === $roomGUID) {
$roomName = $room["name"];
break;
}
}
if ($roomName === null) {
die("Room not found!");
}
$talks = [];
$now = new DateTimeImmutable("now");
foreach ($pretalx["schedule"]["conference"]["days"] as $day) {
if (!array_key_exists($roomName, $day["rooms"])) {
continue;
}
$roomTalks = $day["rooms"][$roomName];
foreach ($roomTalks as $t) {
[$h, $m] = explode(":", $t["duration"]);
$duration = $h * 3600 + $m * 60;
$duration = DateInterval::createFromDateString("$duration sec");
$speakers = [];
foreach ($t["persons"] as $person) {
array_push($speakers, $person["name"]);
}
$talkBegin = new DateTimeImmutable($t["date"]);
$talkEnd = $talkBegin->add($duration);
if ($talkEnd < $now) {
continue;
}
$talk = [
"Time" => $talkBegin->format("H:i"),
"Duration" => $duration->format("H:i"),
"Persons" => implode(", ", $speakers),
"Title" => $t["title"],
"_start" => $talkBegin,
"_end" => $talkEnd,
];
array_push($talks, $talk);
}
}
$template = file_get_contents(dirname(__FILE__) . "/template.bin");
$template = monoformat\parseFile($template);
if (count($template->sections) != 1 or $template->sections[0]->sectionType() != monoformat\SectionType::AlwaysDrawn) {
die("Invalid section in template file");
}
$elements = [];
for ($i = 0; $i < $template->sections[0]->elementCount(); ++$i) {
array_push($elements, $template->sections[0]->elementAt($i));
}
$end_template = file_get_contents(dirname(__FILE__) . "/endscreen.bin");
$end_template = monoformat\parseFile($end_template);
if (count($end_template->sections) != 1 or $end_template->sections[0]->sectionType() != monoformat\SectionType::AlwaysDrawn) {
die("Invalid section in end screen template file");
}
$end_elements = [];
for ($i = 0; $i < $end_template->sections[0]->elementCount(); ++$i) {
array_push($end_elements, $end_template->sections[0]->elementAt($i));
}
$n = 42;
$last = false;
if ($n > count($talks)) {
$n = count($talks);
$last = true;
}
$lastEnd = 0;
$outputFile = new monoformat\File();
if (!$last) {
$section = new monoformat\ExpiryDateSection();
$section->setExpiryDate($talks[$n - 1]["_end"]->getTimestamp());
array_push($outputFile->sections, $section);
}
for ($i = 0; $i < $n; ++$i) {
$subTalks = array_slice($talks, $i);
$end = $subTalks[0]["_end"]->getTimestamp();
$section = new monoformat\TimeBasedDrawnSection();
$section->setDrawOnFront(true);
$section->setDrawOnBack(true);
$section->setClearBeforeDrawing(true);
$section->setStartTimestamp($lastEnd);
$section->setEndTimestamp($end);
foreach ($elements as $element) {
if ($element->elementType() == monoformat\ElementType::ClippedText) {
$x = $element->x();
$y = $element->y();
$width = $element->width();
$height = $element->height();
$textFlags = $element->textFlags();
$fontIndex = $element->fontIndex();
$text = replaceTalkInfo($element->text(), $subTalks);
$element = new monoformat\ClippedTextElement($x, $y, $width, $height, $textFlags, $fontIndex, $text);
} else if ($element->elementType() == monoformat\ElementType::HScrollText) {
$x = $element->x();
$y = $element->y();
$width = $element->width();
$height = $element->height();
$textFlags = $element->textFlags();
$flags = $element->flags();
$scrollSpeed = $element->scrollSpeed();
$fontIndex = $element->fontIndex();
$text = replaceTalkInfo($element->text(), $subTalks) . " ";
$element = new monoformat\HScrollTextElement($x, $y, $width, $height, $textFlags, $flags, $scrollSpeed, $fontIndex, $text);
}
$section->appendElement($element);
}
array_push($outputFile->sections, $section);
$lastEnd = $end;
}
if ($last) {
$section = new monoformat\TimeBasedDrawnSection();
$section->setDrawOnFront(true);
$section->setDrawOnBack(true);
$section->setClearBeforeDrawing(true);
$section->setStartTimestamp($lastEnd);
// Set this far enough in the future that it doesn't matter
$section->setEndTimestamp($lastEnd + 86400 * 300);
foreach ($end_elements as $element) {
$section->appendElement($element);
}
array_push($outputFile->sections, $section);
}
Header("Content-Type: application/octet-stream");
print(monoformat\serializeFile($outputFile));
?>

Binary file not shown.

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Busanzeiger Paint</title>
<link rel="stylesheet" href="styles.css">
<script src="https://cdn.jsdelivr.net/npm/omggif@1.0.10/omggif.min.js"></script>
<script src="alphabet-extended.js"></script>
<script src="binfmt.js"></script>
</head>
<body>
<h1>Busanzeiger Paint</h1>
<div id="modeToggle">
<label><input type="radio" name="mode" value="image" checked> Image Mode</label>
<label><input type="radio" name="mode" value="text"> Text Mode</label>
<label style="margin-left: 20px;"><input type="checkbox" id="animationCreation"> Animation Creation</label>
<label style="margin-left: 20px;">
Duration (seconds): <input type="number" id="animationDuration" min="0.1" max="60" step="0.1" value="3.0" style="width: 60px;">
<button id="revertDuration" title="Unlock and recalculate from frame count"></button>
</label>
</div>
<div id="dropzone">Drag and drop image(s) here or click to upload</div>
<div id="controls">
<input type="file" id="upload" accept="image/*" style="display:none;" />
<button id="invert">Invert</button>
<button id="dither">Dither: OFF</button>
<button id="export">Prepare Export</button>
<button id="downloadBinary">Download Binary</button>
<button id="draw">Draw</button>
<button id="erase">Erase</button>
<label>
Brush:
<select id="brushSize">
<option value="1" selected>1px</option>
<option value="2">2px</option>
<option value="4">4px</option>
<option value="6">6px</option>
</select>
</label>
<button id="reset">Reset</button>
</div>
<div id="frameControls" style="display: none; margin-top: 10px; display: flex; gap: 10px; align-items: center; justify-content: center;">
<button id="prevFrame">← Prev Frame</button>
<button id="playStop">▶ Play</button>
<span id="frameCounter"></span>
<button id="nextFrame">Next Frame →</button>
<button id="addEmptyFrame" style="display: none;">+ Empty Frame</button>
<button id="duplicateFrame" style="display: none;">+ Duplicate Frame</button>
<button id="deleteFrame" style="display: none;">🗑 Delete Frame</button>
</div>
<div id="textInputArea">
<label>Scrolling Text:<br>
<textarea id="scrollText" rows="4" cols="40"></textarea>
</label>
<label>Speed: <input type="number" id="scrollSpeed" min="0" max="63" value="4"></label>
<label><input type="checkbox" id="scrollRight"> Scroll Right</label>
<label><input type="checkbox" id="scrollWrap" checked> Wrap Around</label>
</div>
<div id="matrix"></div>
<form action="upload.php" method="post">
<textarea id="array_output" name="array_output" rows="4" cols="50"></textarea>
<textarea id="scrolls_output" name="scrolls" rows="4" cols="50" style="display:none;"></textarea>
<br>
Filename: <input type="text" id="fileName" name="fileName">
<br>
Passwort: <input type="password" id="password" name="password">
<button type="submit">Speichern</button>
</form>
<script src="app.js"></script>
</body>
</html>

@ -0,0 +1,215 @@
<?php
session_start();
define('PASSWORD', 'grund_cttue_gesetz');
define('FILES_DIR', __DIR__ . '/avj2305');
define('ACTIVE_FILE', __DIR__ . '/active.txt');
// =============================================================================
// HELPERS
// =============================================================================
function redirect(string $url): never {
header('Location: ' . $url);
exit;
}
function require_auth(): void {
if (empty($_SESSION['auth'])) redirect('?login');
}
function get_active(): string {
if (!file_exists(ACTIVE_FILE)) return '';
$v = trim(file_get_contents(ACTIVE_FILE));
return $v !== '' ? $v : '';
}
function set_active(string $filename): void {
file_put_contents(ACTIVE_FILE, $filename);
}
function get_bin_files(): array {
$files = glob(FILES_DIR . '/*.bin');
return $files ? array_map('basename', $files) : [];
}
function safe_filename(string $name): bool {
return $name === basename($name)
&& str_ends_with($name, '.bin')
&& !str_contains($name, "\0");
}
function has_png(string $bin_basename): bool {
$png = substr($bin_basename, 0, -4) . '.png';
return file_exists(FILES_DIR . '/' . $png);
}
function png_path(string $bin_basename): string {
return FILES_DIR . '/' . substr($bin_basename, 0, -4) . '.png';
}
// =============================================================================
// ACTIONS (POST handlers - redirect, never render)
// =============================================================================
function action_login(): never {
if (hash_equals(PASSWORD, $_POST['password'] ?? '')) {
$_SESSION['auth'] = true;
redirect('?edit');
}
redirect('?login&err=1');
}
function action_set_file(): never {
require_auth();
$f = $_POST['file'] ?? '';
if (safe_filename($f) && file_exists(FILES_DIR . '/' . $f)) {
set_active($f);
}
redirect('?edit');
}
// =============================================================================
// RENDERERS (GET handlers - output HTML, never redirect)
// =============================================================================
function render_login(): never {
$err = !empty($_GET['err']); ?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Login</title>
<style>
body{font:14px monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#111;color:#eee}
form{display:flex;flex-direction:column;gap:8px;width:220px}
input[type=password]{padding:6px;background:#222;border:1px solid #555;color:#eee}
button{padding:6px;background:#444;border:none;color:#eee;cursor:pointer}
button:hover{background:#555}
.err{color:#f66;font-size:12px}
</style>
</head>
<body>
<form method="post" action="?login">
<label>Password</label>
<input type="password" name="password" autofocus>
<button type="submit">Login</button>
<?php if ($err): ?><span class="err">Wrong password.</span><?php endif; ?>
</form>
</body></html>
<?php exit; }
function render_edit(): never {
require_auth();
$files = get_bin_files();
$active = get_active(); ?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Select File</title>
<style>
body{font:14px monospace;background:#111;color:#eee;padding:24px;margin:0}
h2{margin:0 0 16px}
ul{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px}
li form{margin:0}
.row{display:flex;align-items:center;gap:10px}
button{padding:6px 12px;background:#333;border:1px solid #555;color:#eee;cursor:pointer;flex:1;text-align:left}
button:hover{background:#444}
button.active{border-color:#6af;color:#6af}
.thumb{width:480px;height:270px;object-fit:cover;border:1px solid #444;background:#222;flex-shrink:0}
.nothumb{width:64px;height:64px;flex-shrink:0}
.none{color:#888}
</style>
</head>
<body>
<h2>Live file selector</h2>
<p>Active: <strong><?= $active !== '' ? htmlspecialchars($active) : '<span class="none">none</span>' ?></strong></p>
<?php if ($files): ?>
<ul>
<?php foreach ($files as $f): ?>
<li>
<form method="post" action="?edit">
<div class="row">
<?php if (has_png($f)): ?>
<img class="thumb" src="?img=<?= urlencode($f) ?>" alt="">
<?php else: ?>
<div class="nothumb"></div>
<?php endif; ?>
<input type="hidden" name="file" value="<?= htmlspecialchars($f) ?>">
<button type="submit"<?= $f === $active ? ' class="active"' : '' ?>><?= htmlspecialchars($f) ?></button>
</div>
</form>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="none">No .bin files found in avj2305/</p>
<?php endif; ?>
</body></html>
<?php exit; }
// =============================================================================
// FILE SERVER (default route)
// =============================================================================
function serve_png(): never {
require_auth();
$f = $_GET['img'] ?? '';
if (!safe_filename($f)) {
http_response_code(400); exit('Invalid filename.');
}
$path = png_path($f);
if (!file_exists($path)) {
http_response_code(404); exit('No preview.');
}
header('Content-Type: image/png');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
}
function serve_active_file(): never {
$active = get_active();
if ($active === '') {
http_response_code(404); exit('No active file set.');
}
if (!safe_filename($active)) {
http_response_code(500); exit('Invalid filename.');
}
$path = FILES_DIR . '/' . $active;
if (!file_exists($path)) {
http_response_code(404); exit('File not found.');
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: inline; filename="' . addslashes($active) . '"');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
}
// =============================================================================
// ROUTES
// =============================================================================
//
// GET /live.php -> serve active .bin file
// GET /live.php?login -> login page
// POST /live.php?login -> process login
// GET /live.php?edit -> file picker [auth required]
// POST /live.php?edit -> set active file [auth required]
// GET /live.php?img=x.bin -> serve x.png preview [auth required]
//
// =============================================================================
$method = $_SERVER['REQUEST_METHOD'];
$route = array_key_first($_GET) ?? '';
match (true) {
$method === 'POST' && $route === 'login' => action_login(),
$method === 'POST' && $route === 'edit' => action_set_file(),
$method === 'GET' && $route === 'login' => render_login(),
$method === 'GET' && $route === 'edit' => render_edit(),
$method === 'GET' && $route === 'img' => serve_png(),
default => serve_active_file(),
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,203 @@
<?php
namespace monoformat {
enum SectionType : int {
case AlwaysDrawn = 1;
case TimeBasedDrawn = 2;
case CustomFont = 32;
}
enum ElementType : int {
case Image = 1;
case Animation = 2;
case HScrollImage = 3;
case VScrollImage = 4;
case Line = 5;
case ClippedText = 16;
case HScrollText = 17;
case CurrentTime = 32;
}
enum LineStyle : int {
case Solid = 0;
}
class Element {
public function serialize() {
return "";
}
}
class HScrollTextElement extends Element {
public $x = 0;
public $y = 0;
public $width = 0;
public $height = 0;
public $flags = 0;
public $scrollSpeed = 0;
public $fontIndex = 0;
public $text = "";
public function serialize() {
$len = strlen($this->text);
$result = pack("vvvvvCCvv",
17,
$this->x,
$this->y,
$this->width,
$this->height,
$this->flags,
$this->scrollSpeed,
$this->fontIndex,
$len);
$result .= (string) $this->text;
if ($len % 4 != 0) {
$n = 4 - ($len % 4);
for ($i = 0; $i < $n; ++$i) {
$result .= chr(0);
}
}
return $result;
}
}
class CurrentTimeElement extends Element {
public $x = 0;
public $y = 0;
public $width = 0;
public $height = 0;
public $fontIndex = 0;
public $utcOffset = 0;
public $flags = 0;
public function serialize() {
$result = pack("vvvvvvvv",
32,
$this->x,
$this->y,
$this->width,
$this->height,
$this->fontIndex,
$this->utcOffset,
$this->flags);
return $result;
}
}
class Section {
public function serialize() {
return "";
}
}
class AlwaysDrawnSection extends Section {
public $drawOnFront = false;
public $drawOnBack = false;
public $clearBeforeDrawing = false;
public $elements = [];
public function serialize() {
$flags = 0;
if ($this->drawOnFront) {
$flags |= 0x01;
}
if ($this->drawOnBack) {
$flags |= 0x02;
}
if ($this->clearBeforeDrawing) {
$flags |= 0x04;
}
$inner = pack("vv", $flags, count($this->elements));
foreach ($this->elements as $element) {
$inner .= $element->serialize();
}
$len = strlen($inner) + 4;
$len = substr(pack("V", $len), 0, 3);
return pack("C", 1) . $len . $inner;
}
}
class File {
public $sections = [];
public function serialize() {
$nSections = count($this->sections);
$result = "\xAF\x7E\x2B\x63";
$result .= pack("Vvv", 1, $nSections, 0);
foreach ($this->sections as $section) {
$result .= $section->serialize();
}
return $result;
}
}
} // namespace monoformat
namespace {
$roomName= "BOOL";
$data = json_decode(file_get_contents("https://cfp.cttue.de/tdf5/schedule/export/schedule.json"), true);
$talks = [];
$now = new DateTimeImmutable("now");
foreach ($data["schedule"]["conference"]["days"] as $day) {
foreach ($day["rooms"][$roomName] as $t) {
[$h, $m] = explode(":", $t["duration"]);
$duration = $h * 3600 + $m * 60;
$duration = DateInterval::createFromDateString("$duration sec");
$talk = [
"date" => new DateTimeImmutable($t["date"]),
"duration" => $duration,
"title" => $t["title"],
];
$talkEnd = $talk["date"]->add($talk["duration"]);
if ($talkEnd < $now) {
continue;
}
if ($talk["date"] > $now && count($talks) > 2) {
break;
}
array_push($talks, $talk);
}
}
$elements = [];
function putText($x, $y, $width, $height, $text) {
global $elements;
$e = new monoformat\HScrollTextElement();
$e->x = $x;
$e->y = $y;
$e->width = $width;
$e->height = $height;
$e->flags = 0;
$e->scrollSpeed = 15;
$e->fontIndex = 0;
$e->text = $text;
array_push($elements, $e);
}
$i = 0;
foreach ($talks as $talk) {
putText(10, $i * 20, 25, 20, $talk["date"]->format("H:i"));
putText(35, $i * 20, 85, 20, $talk["title"]);
++$i;
}
$section = new monoformat\AlwaysDrawnSection();
$section->drawOnFront = true;
$section->drawOnBack = true;
$section->clearBeforeDrawing = true;
$section->elements = $elements;
$file = new monoformat\File();
array_push($file->sections, $section);
Header("Content-Type: application/octet-stream");
print($file->serialize());
} // global
?>

@ -0,0 +1,203 @@
<?php
namespace monoformat {
enum SectionType : int {
case AlwaysDrawn = 1;
case TimeBasedDrawn = 2;
case CustomFont = 32;
}
enum ElementType : int {
case Image = 1;
case Animation = 2;
case HScrollImage = 3;
case VScrollImage = 4;
case Line = 5;
case ClippedText = 16;
case HScrollText = 17;
case CurrentTime = 32;
}
enum LineStyle : int {
case Solid = 0;
}
class Element {
public function serialize() {
return "";
}
}
class HScrollTextElement extends Element {
public $x = 0;
public $y = 0;
public $width = 0;
public $height = 0;
public $flags = 0;
public $scrollSpeed = 0;
public $fontIndex = 0;
public $text = "";
public function serialize() {
$len = strlen($this->text);
$result = pack("vvvvvCCvv",
17,
$this->x,
$this->y,
$this->width,
$this->height,
$this->flags,
$this->scrollSpeed,
$this->fontIndex,
$len);
$result .= (string) $this->text;
if ($len % 4 != 0) {
$n = 4 - ($len % 4);
for ($i = 0; $i < $n; ++$i) {
$result .= chr(0);
}
}
return $result;
}
}
class CurrentTimeElement extends Element {
public $x = 0;
public $y = 0;
public $width = 0;
public $height = 0;
public $fontIndex = 0;
public $utcOffset = 0;
public $flags = 0;
public function serialize() {
$result = pack("vvvvvvvv",
32,
$this->x,
$this->y,
$this->width,
$this->height,
$this->fontIndex,
$this->utcOffset,
$this->flags);
return $result;
}
}
class Section {
public function serialize() {
return "";
}
}
class AlwaysDrawnSection extends Section {
public $drawOnFront = false;
public $drawOnBack = false;
public $clearBeforeDrawing = false;
public $elements = [];
public function serialize() {
$flags = 0;
if ($this->drawOnFront) {
$flags |= 0x01;
}
if ($this->drawOnBack) {
$flags |= 0x02;
}
if ($this->clearBeforeDrawing) {
$flags |= 0x04;
}
$inner = pack("vv", $flags, count($this->elements));
foreach ($this->elements as $element) {
$inner .= $element->serialize();
}
$len = strlen($inner) + 4;
$len = substr(pack("V", $len), 0, 3);
return pack("C", 1) . $len . $inner;
}
}
class File {
public $sections = [];
public function serialize() {
$nSections = count($this->sections);
$result = "\xAF\x7E\x2B\x63";
$result .= pack("Vvv", 1, $nSections, 0);
foreach ($this->sections as $section) {
$result .= $section->serialize();
}
return $result;
}
}
} // namespace monoformat
namespace {
$roomName= "ENUM";
$data = json_decode(file_get_contents("https://cfp.cttue.de/tdf5/schedule/export/schedule.json"), true);
$talks = [];
$now = new DateTimeImmutable("now");
foreach ($data["schedule"]["conference"]["days"] as $day) {
foreach ($day["rooms"][$roomName] as $t) {
[$h, $m] = explode(":", $t["duration"]);
$duration = $h * 3600 + $m * 60;
$duration = DateInterval::createFromDateString("$duration sec");
$talk = [
"date" => new DateTimeImmutable($t["date"]),
"duration" => $duration,
"title" => $t["title"],
];
$talkEnd = $talk["date"]->add($talk["duration"]);
if ($talkEnd < $now) {
continue;
}
if ($talk["date"] > $now && count($talks) > 2) {
break;
}
array_push($talks, $talk);
}
}
$elements = [];
function putText($x, $y, $width, $height, $text) {
global $elements;
$e = new monoformat\HScrollTextElement();
$e->x = $x;
$e->y = $y;
$e->width = $width;
$e->height = $height;
$e->flags = 0;
$e->scrollSpeed = 15;
$e->fontIndex = 0;
$e->text = $text;
array_push($elements, $e);
}
$i = 0;
foreach ($talks as $talk) {
putText(10, $i * 20, 25, 20, $talk["date"]->format("H:i"));
putText(35, $i * 20, 85, 20, $talk["title"]);
++$i;
}
$section = new monoformat\AlwaysDrawnSection();
$section->drawOnFront = true;
$section->drawOnBack = true;
$section->clearBeforeDrawing = true;
$section->elements = $elements;
$file = new monoformat\File();
array_push($file->sections, $section);
Header("Content-Type: application/octet-stream");
print($file->serialize());
} // global
?>

@ -0,0 +1,53 @@
<?php
$path = 'out';
$files = scandir($path);
$binaryFiles = [];
foreach($files as $file) {
if(str_ends_with($file, 'bin')) {
$binaryFiles[] = $file;
}
}
$current = date('i') % sizeof($binaryFiles);
$downloadName = $binaryFiles[$current];
$filePath = __DIR__ . '/out/' . $downloadName;
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
echo 'File not found.';
echo $filePath;
exit;
}
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream'); // generic binary MIME
header('Content-Disposition: attachment; filename="' . basename($downloadName) . '"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, private');
header('Pragma: public');
header('Content-Length: ' . filesize($filePath));
while (ob_get_level()) {
ob_end_clean();
}
$chunkSize = 8192;
$handle = fopen($filePath, 'rb');
if ($handle === false) {
http_response_code(500);
echo 'Unable to open file.';
exit;
}
while (!feof($handle)) {
// Output a chunk and flush it immediately
echo fread($handle, $chunkSize);
flush(); // push to client
}
fclose($handle);
exit;
?>

@ -0,0 +1,84 @@
body {
font-family: sans-serif;
background: #111;
color: white;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
image-rendering: pixelated;
margin-top: 20px;
}
#matrix {
display: grid;
grid-template-columns: repeat(var(--col-count, 10), 1fr);
grid-gap: 1px;
margin-top: 20px;
}
.dot {
width: 10px;
height: 10px;
background: #222;
border-radius: 50%;
}
.dot.on {
background: orange;
}
#dropzone {
border: 2px dashed #555;
padding: 20px;
margin-top: 20px;
text-align: center;
width: 300px;
cursor: pointer;
}
#controls {
margin-top: 10px;
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
#textInputArea {
display: none;
margin-top: 10px;
}
input, textarea, select {
background: #222;
color: white;
border: 1px solid #555;
padding: 5px;
border-radius: 4px;
}
select option {
background: #222;
color: white;
}
button {
background: #333;
color: white;
border: 1px solid #555;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #444;
}
button.active {
background: #f60;
border-color: #f60;
}

@ -0,0 +1,105 @@
<?php
$width = 120;
$height = 60;
$PASSWORD = "Ky/6lG3kbA>/TM?C(V@S";
// --- Helpers ---
function setPixel(int $x, int $y, int $width, &$bitmap) {
if ($x < 0 || $y < 0) return;
$index = $y * $width + $x;
$byteIdx = intdiv($index, 8);
$bitPos = $index % 8;
$bitmap[$byteIdx] |= (1 << $bitPos);
}
function createImageBitmap(array $pixelsOn, int $width, int $height): array {
$totalBits = $width * $height;
$bitmapSize = intdiv($totalBits + 7, 8);
$bitmap = array_fill(0, $bitmapSize, 0);
foreach ($pixelsOn as [$x, $y]) {
setPixel($x, $y, $width, $bitmap);
}
return $bitmap;
}
function packBitmap(array $bitmap): string {
return implode('', array_map(fn($b) => pack('C', $b), $bitmap));
}
function createImageObject(int $width, int $height, array $bitmap, int $xOffset = 0, int $yOffset = 0): string {
$payload = pack('C', $width);
$payload .= pack('C', $height);
$payload .= packBitmap($bitmap);
$header = pack('C*', 0x01, $xOffset, $yOffset); // Type = 1
return $header . $payload;
}
function createScrollObject(int $xOffset, int $yOffset, int $width, int $height, int $contentWidth, array $bitmap, int $speed = 0, bool $directionRight = false, bool $wrap = true): string {
$CW = pack('v', $contentWidth);
$UF = (($wrap ? 1 : 0) << 7) | (($directionRight ? 1 : 0) << 6) | ($speed & 0x3F);
$payload = pack('C', $width);
$payload .= pack('C', $height);
$payload .= $CW;
$payload .= pack('C', $UF);
$payload .= packBitmap($bitmap);
$header = pack('C*', 0x03, $xOffset, $yOffset);
return $header . $payload;
}
function createBlob(array $objects, string $fileName) {
$blob = '';
$blob .= pack('C*', 0x42, 0x4E, 0x17, 0xEE);
$blob .= pack('C', count($objects));
foreach ($objects as $obj) {
$blob .= $obj;
}
file_put_contents($fileName, $blob);
echo "Blob written to {$fileName} (" . strlen($blob) . " bytes)\n";
}
// --- Validation ---
if (!isset($_POST['password'])) die("password missing.");
if (htmlspecialchars($_POST['password']) !== htmlspecialchars($PASSWORD)) die("wrong password.");
// --- Determine mode ---
$mode = $_POST['mode'] ?? 'image';
$objects = [];
$fileName = 'out/' . $_POST['fileName'] . '.bin' ?? 'out/' .'output.bin';
mkdir('out/');
// --- Handle Image Mode ---
if ($mode === 'image') {
$pixelsOn = json_decode($_POST['array_output'] ?? '[]', true);
if (!is_array($pixelsOn)) die("invalid or missing array_output for image mode.");
$bitmap = createImageBitmap($pixelsOn, $width, $height);
$objects[] = createImageObject($width, $height, $bitmap, 0, 0);
}
// --- Handle Text Mode (scrolling text) ---
elseif ($mode === 'text') {
$scrolls = json_decode($_POST['scrolls'] ?? '[]', true);
if (!is_array($scrolls) || empty($scrolls)) die("invalid or missing scrolls for text mode.");
foreach ($scrolls as $scroll) {
$pixelsOn = $scroll['pixelsOn'] ?? [];
$bitmap = createImageBitmap($pixelsOn, $scroll['contentWidth'], $scroll['height']);
$objects[] = createScrollObject(
$scroll['x'] ?? 0,
$scroll['y'] ?? 0,
$scroll['width'] ?? $width,
$scroll['height'] ?? 8,
$scroll['contentWidth'] ?? 60,
$bitmap,
$scroll['speed'] ?? 0,
$scroll['directionRight'] ?? false,
$scroll['wrap'] ?? true
);
}
}
else {
die("unknown mode: {$mode}");
}
createBlob($objects, $fileName);
Loading…
Cancel
Save