Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Changes from recovered fw dir. Not sure if this is latest
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Framework Laptop 16 LED Matrix Input Module Control
# Framework Laptop 16 LED Matrix Input Module Controlh

## The upstream repo is apparently no longer monitored, so this fork will probably never be merged.

The following enhancements are provided:

- Import export capability
- Matrix values are saved as a 39 by 9 byte array
- Persist button
- Continually wakes the matrix when selected, so the display does not turn off

[View it in your browser.](https://ledmatrix.frame.work)

This little web app can directly connect to the Framework Laptop 16 LED matrix
Expand Down
268 changes: 267 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const VERSION_CMD = 0x20;

const WIDTH = 9;
const HEIGHT = 34;
const WAKE_LOOP_INTERVAL_MSEC = 50_000

const PATTERNS = [
'Custom',
Expand All @@ -42,15 +43,19 @@ var msbendian = false;
let portLeft = null;
let portRight = null;
let swap = false;
let persist = false

$(function() {
matrix_left = createArray(34, 9);
matrix_right = createArray(34, 9);

updateTableLeft();
updateTableRight();
initOptions();
initSoftwareExportOptions();
startWakeLoop()

for (pattern of PATTERNS) {
for (const pattern of PATTERNS) {
$("#select-left").append(`<option value="${pattern}">${pattern}</option>`);
$("#select-left").on("change", async function() {
if (pattern == 'Custom') return;
Expand All @@ -67,6 +72,8 @@ $(function() {
}
});

// startWakeLoop()

function drawPattern(matrix, pattern, pos) {
for (let col = 0; col < WIDTH; col++) {
for (let row = 0; row < HEIGHT; row++) {
Expand Down Expand Up @@ -144,6 +151,24 @@ function initOptions() {
matrix_left = createArray(matrix_left.length, matrix_left[0].length);
updateTableLeft();
sendToDisplay(true);
});
$('#importLeftBtn').click(function() {
importMatrixLeft();
});
$('#importRightBtn').click(function() {
importMatrixRight();
});
$('#exportLeftBtn').click(function() {
//Export raw data (i.e. a 2D array instead of a 1d array of encoded bits)
exportMatrixLeft(true);
// No need to suport export of encoded file since import can handle either type
// exportMatrixLeft(false)
});
$('#exportRightBtn').click(function() {
//Export raw data (i.e. a 2D array instead of a 1d array of encoded bits)
exportMatrixRight(true);
// No need to suport export of encoded file since import can handle either type
// exportMatrixRight(false)
});
$('#wakeBtn').click(function() {
wake(portLeft, true);
Expand All @@ -152,6 +177,9 @@ function initOptions() {
$('#sleepBtn').click(function() {
wake(portLeft, false);
wake(portRight, false);
});
$('#persistCb').click(function() {
persist = !persist;
});
$('#bootloaderBtn').click(function() {
bootloader(portLeft);
Expand All @@ -178,6 +206,49 @@ function initOptions() {
});
}

function initSoftwareExportOptions() {
$('#exportLeftSoftwareBtn').click(function() {
const grayscale = $('input[name="exportFormat"]:checked').val() !== 'binary';
exportMatrixSoftware(matrix_left, 'left', grayscale);
});

$('#exportRightSoftwareBtn').click(function() {
const grayscale = $('input[name="exportFormat"]:checked').val() !== 'binary';
exportMatrixSoftware(matrix_right, 'right', grayscale);
});
}

function exportMatrixSoftware(matrix, side, grayscale = true) {
const width = matrix[0].length; // 9
const height = matrix.length; // 34

// Fixed column-major: 9 columns x 34 rows
const vals = Array(width).fill(0).map(() => Array(height).fill(0));
for (let col = 0; col < width; col++) {
for (let row = 0; row < height; row++) {
const isLit = matrix[row][col] === 0; // LED on
if (grayscale) {
vals[col][row] = isLit ? 255 : 0;
} else {
vals[col][row] = isLit ? 1 : 0;
}
}
}

const formatStr = grayscale ? 'grayscale' : 'binary';
const filename = `matrix_${side}_${formatStr}_colmajor.json`;

const blob = new Blob([JSON.stringify(vals, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}

async function command(port, id, params) {
const writer = port.writable.getWriter();

Expand Down Expand Up @@ -228,6 +299,7 @@ async function checkFirmwareVersion(port, side) {
reader.releaseLock();
}

//Get matrix values encoded as a 39-byte array
function prepareValsForDrawingLeft() {
const width = matrix_left[0].length;
const height = matrix_left.length;
Expand All @@ -246,6 +318,7 @@ function prepareValsForDrawingLeft() {
return vals;
}

//Get matrix values encoded as a 39-byte array
function prepareValsForDrawingRight() {
const width = matrix_right[0].length;
const height = matrix_right.length;
Expand All @@ -264,6 +337,190 @@ function prepareValsForDrawingRight() {
return vals;
}

//Get matrix values set directly in a 39 x 9 item array
function getRawValsMatrixRight() {
const width = matrix_right[0].length;
const height = matrix_right.length;

let vals = new Array(height)
for (const i in [...Array(height).keys()]) {
vals[i] = Array(width).fill(0)
}

for (let col = 0; col < width; col++) {
for (let row = 0; row < height; row++) {
const cell = matrix_right[row][col];
vals[row][col] = (cell == null || cell == 1) ? 0 : 1
}
}
return vals;
}

//Get matrix values set directly in a 39 x 9 item array
function getRawValsMatrixLeft() {
const width = matrix_left[0].length;
const height = matrix_left.length;

let vals = new Array(height)
for (const i in [...Array(height).keys()]) {
vals[i] = Array(width).fill(0)
}

for (let col = 0; col < width; col++) {
for (let row = 0; row < height; row++) {
const cell = matrix_left[row][col];
vals[row][col] = (cell == null || cell == 1) ? 0 : 1
}
}
return vals;
}

//Set matrix values by decoding a 39-byte array
function setMatrixLeftFromVals(vals) {
const width = matrix_left[0].length;
const height = matrix_left.length;

for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const i = col + row * width;
const val = vals[Math.trunc(i/8)]
const bit = (val >> i % 8) & 1;
matrix_left[row][col] = (bit + 1) % 2;
}
}
}

//Set matrix values by decoding a 39-byte array
function setMatrixRightFromVals(vals) {
const width = matrix_right[0].length;
const height = matrix_right.length;

for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const i = col + row * width;
const val = vals[Math.trunc(i/8)]
const bit = (val >> i % 8) & 1;
matrix_right[row][col] = (bit + 1) % 2;
}
}
}

//Set matrix values from a 39 x 9 item array
function setMatrixRightFromRawVals(vals) {
const width = matrix_right[0].length;
const height = matrix_right.length;

for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
matrix_right[row][col] = !!!vals[row][col]
}
}
}

//Set matrix values from a 39 x 9 item array
function setMatrixLeftFromRawVals(vals) {
const width = matrix_left[0].length;
const height = matrix_left.length;

for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
matrix_left[row][col] = !!!vals[row][col]
}
}
}

function exportMatrixLeft(raw) {
let vals
if (raw) {
//set vals as a 39 by 9 byte array
vals = getRawValsMatrixLeft()
} else {
//encode vals into a 39-byte array
vals = prepareValsForDrawingLeft();
}
//save json file
const blob = new Blob([JSON.stringify(vals)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "matrix_left.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}

function exportMatrixRight(raw) {
let vals
if (raw) {
//set vals as a 39 by 9 byte array
vals = getRawValsMatrixRight()
} else {
//encode vals into a 39-byte array
vals = prepareValsForDrawingRight();
}
console.log('Exported values')
console.log(vals)
//save json file
const blob = new Blob([JSON.stringify(vals)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${raw ? 'matrix_right(raw).json' : 'matrix_right.json'}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}

function importMatrixLeft() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = async function (event) {
const file = event.target.files[0];
if (!file) return;

const reader = new FileReader();
reader.onload = function (e) {
const vals = JSON.parse(e.target.result);
if (vals[0].length > 1) {
setMatrixLeftFromRawVals(vals)
} else {
setMatrixLeftFromVals(vals);
}
updateMatrix(matrix_left, 'left')
sendToDisplay(true);
$("#select-left").val('Custom');
};
reader.readAsText(file);
};
input.click();
}
function importMatrixRight() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = async function (event) {
const file = event.target.files[0];
if (!file) return;

const reader = new FileReader();
reader.onload = function (e) {
const vals = JSON.parse(e.target.result);
if (vals[0].length > 1) {
setMatrixRightFromRawVals(vals)
} else {
setMatrixRightFromVals(vals);
}
updateMatrix(matrix_right, 'right')
sendToDisplay(true);
$("#select-right").val('Custom');
};
reader.readAsText(file);
};
input.click();
}

async function sendToDisplay(recurse) {
await sendToDisplayLeft(recurse);
Expand Down Expand Up @@ -386,6 +643,15 @@ function createArray(length) {
return arr;
}

function startWakeLoop() {
setInterval(() => {
if (persist) {
wake(portLeft, true);
wake(portRight, true);
}
}, WAKE_LOOP_INTERVAL_MSEC)
}

async function wake(port, wake) {
await sendCommand(port, 0x03, [wake ? 0 : 1]);
}
Expand Down
Loading