Hey! Just wanted to post an update, I had a go at implementing the suggestions yesterday evening & got something working - it catches even fast button presses but occasionally drops them which is something I'm looking into. I've attached some code, its still ugly POC code & I'm sure there are things I can do to make it run more efficiently :)
//=======================================================================
// MATRIX
//=======================================================================
const matrix = (rows, cols) => {
let _scanning;
let _onStopScanning;
let _numberOfIdleScansBeforeStop;
let _numberOfActiveKeys;
let _numberOfIdleScans;
let _keyStates;
let _rowStates;
let _cols = cols;
let _rows = rows;
const defaultKeyStates = new Array(_rows.length).fill(new Array(_cols.length).fill(0));
// const resetKeyStates = () => _keyStates = defaultKeyStates;
// Make this flexible for different rows/cols
const resetKeyStates = () => _keyStates = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
];
// Make this flexible for different rows/cols
const resetRowStates = () => _rowStates = [ 0, 0, 0, 0 ];
const resetNumberOfIdleScans = () => _numberOfIdleScans = 0;
const resetNumberOfActiveKeys = () => _numberOfActiveKeys = 0;
const getMatrixState = () => _keyStates;
const getKeyState = (row, col) => _keyStates[row][col];
const setPinModes = () => {
"compiled";
// @TODO: Can we set an array of pinmodes instead of iterating?
_rows.forEach(row => pinMode(row, 'output'));
_cols.forEach(col => pinMode(col, 'input_pulldown'));
};
const unsetPinModes = () => {
"compiled";
_rows.forEach(row => pinMode(row, 'input_pulldown'));
_cols.forEach(col => pinMode(col, 'output'));
};
const setup = () => {
"compiled";
resetNumberOfIdleScans();
resetNumberOfActiveKeys();
resetKeyStates();
resetRowStates();
};
const startScanning = (onChange, onStop, numberOfIdleScansBeforeStop, interval) => {
"compiled";
console.log("======================");
setPinModes();
_numberOfIdleScansBeforeStop = numberOfIdleScansBeforeStop;
_onStopScanning = onStop;
if (!_scanning) _scanning = setInterval(() => scanMatrix(onChange), interval || 1);
};
const stopScanning = () => {
"compiled";
resetNumberOfIdleScans();
resetNumberOfActiveKeys();
resetKeyStates();
resetRowStates();
unsetPinModes();
if (_scanning) { clearInterval(_scanning); _scanning = undefined; }
if (_onStopScanning) _onStopScanning();
};
const isScanning = () => !!_scanning;
const updateNumberOfIdleScans = (activeKeys, callback) => {
"compiled";
if (activeKeys === 0) _numberOfIdleScans = _numberOfIdleScans + 1;
callback();
};
const stopOnIdle = () => {
"compiled";
if (_numberOfIdleScansBeforeStop && (_numberOfIdleScans >= _numberOfIdleScansBeforeStop)) stopScanning();
};
const updateNumberOfActiveKeys = (isActive) => {
"compiled";
if (isActive) {_numberOfActiveKeys = _numberOfActiveKeys + 1;}
else if (_numberOfActiveKeys > 0) {_numberOfActiveKeys = _numberOfActiveKeys - 1;}
};
const parseRowToString = (row) => {
"compiled";
return row.toString(2);
};
const detectChange = (rowState, currentRowIndex, onChange) => {
"compiled";
if (rowState !== _rowStates[currentRowIndex]) {
_rowStates[currentRowIndex] = rowState;
const parsedRow = parseRowToString(rowState);
// @TODO: This feels inefficient
for (let colIndex = 0; colIndex < parsedRow.length; colIndex++) {
const col = Number(parsedRow[colIndex]);
if (col !== _keyStates[currentRowIndex][colIndex]) {
_keyStates[currentRowIndex][colIndex] = col;
onChange(currentRowIndex, colIndex, col);
updateNumberOfActiveKeys(col);
}
}
}
};
const scanMatrix = (onChange) => {
"compiled";
_rows.forEach((currentRow, currentRowIndex) => {
digitalWrite(currentRow, 1);
detectChange(digitalRead(_cols), currentRowIndex, onChange);
digitalWrite(currentRow, 0);
});
updateNumberOfIdleScans(_numberOfActiveKeys, stopOnIdle);
};
setup();
return {
getMatrixState: getMatrixState,
getKeyState: getKeyState,
startScanning: startScanning,
stopScanning: stopScanning,
isScanning: isScanning
};
};
//=======================================================================
// USAGE
//=======================================================================
const ROWS = [D14, D15, D17, D30];
const COLS = [D16, D18, D19, D20, D22, D25, D26, D27, D28, D29, D31];
let _rowWatchers;
const myMatrix = matrix(ROWS, COLS);
const onMatrixChange = (currentRow, currentCol, isActive) => {
// Keep track of key press/release duration
// Classify as tap or hold
// keyPress when a key is pressed, payload containing key row/column
// keyRelease when a key is released, payload containing row/column, duration of press
// keyTap when a key press/release cycle is < 200ms, payload containing row/column
// keyHoldStart when a key has been pressed for > 200ms, payload containing row/column
// keyHoldEnd fired when held key is released, payload containing row/column, duration of press
console.log(currentRow, currentCol, isActive);
};
const onMatrixStop = () => {
"compiled";
enable();
};
const handleWatchTrigger = (e) => {
"compiled";
unsetWatches(_rowWatchers);
setColsLow(COLS);
myMatrix.startScanning(onMatrixChange, onMatrixStop, 2, 1);
};
const setColsHigh = (cols) => {
"compiled";
cols.forEach(col => digitalWrite(col, 1));
};
const setColsLow = (cols) => {
"compiled";
cols.forEach(col => digitalWrite(col, 0));
};
const setWatches = (rows) => {
"compiled";
_rowWatchers = rows.map(row => setWatch((e) => handleWatchTrigger(e), row, { repeat:false, edge:'rising', debounce: 10 }));
};
// @TODO: Make this more resilient, this errors when it can't clear a watch
const unsetWatches = (rowWatchers) => {
"compiled";
rowWatchers.forEach(rowWatcher => clearWatch(rowWatcher));
};
const enable = () => {
"compiled";
ROWS.forEach(row => pinMode(row, 'input_pulldown'));
COLS.forEach(col => pinMode(col, 'output'));
setColsHigh(COLS);
setWatches(ROWS);
};
enable();
Espruino is a JavaScript interpreter for low-power Microcontrollers. This site is both a support community for Espruino and a place to share what you are working on.
Hey! Just wanted to post an update, I had a go at implementing the suggestions yesterday evening & got something working - it catches even fast button presses but occasionally drops them which is something I'm looking into. I've attached some code, its still ugly POC code & I'm sure there are things I can do to make it run more efficiently :)