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
224 changes: 223 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,18 @@ 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();
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 +71,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 +150,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 +176,9 @@ function initOptions() {
$('#sleepBtn').click(function() {
wake(portLeft, false);
wake(portRight, false);
});
$('#persistCb').click(function() {
persist = !persist;
});
$('#bootloaderBtn').click(function() {
bootloader(portLeft);
Expand Down Expand Up @@ -228,6 +255,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 +274,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 +293,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 +599,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
16 changes: 13 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ <h2>Device Information</h2>
<button type="button" class="btn btn-default" id="sleepBtn">Sleep</button>
<button type="button" class="btn btn-default" id="bootloaderBtn">Bootloader</button>
</div>

<div class="btn-group">
<input type="checkbox" id="persistCb" class="btn btn-default">Persist
</div>
<p class="slider-wrapper">
<label for="brightnessRange">Brightness:</label>
<input type="range" id="brightnessRange" min="0" max="255">
Expand All @@ -80,8 +82,16 @@ <h2>Device Information</h2>
<button type="button" class="btn btn-default" id="clearLeftBtn">Clear Left</button>
<button type="button" class="btn btn-default" id="clearRightBtn">Clear Right</button>
</div>
<!--<button type="button" class="btn btn-default" id="sendButton">Send</button>-->
</div>
<div class="btn-group">
<button type="button" class="btn btn-default" id="importLeftBtn">Import Left</button>
<button type="button" class="btn btn-default" id="importRightBtn">Import Right</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-default" id="exportLeftBtn">Export Left</button>
<button type="button" class="btn btn-default" id="exportRightBtn">Export Right</button>
</div>
<!--<button type="button" class="btn btn-default" id="sendButton">Send</button>-->
</div>

<div>
<h2>Learn More</h2>
Expand Down