Avatar for consolenaut

consolenaut

Member since Jan 2018 • Last active Sep 2020
  • 4 conversations
  • 14 comments

Most recent activity

  • in The Place for Patreon Patrons
    Avatar for consolenaut

    Just a quick question on the pinModes/setWatch method - is there a way to set an array of pin modes & watches like how I'm able to read an array of pins with digitalRead? I couldn't find anything about it in the docs, is it something on the roadmap? I'm trying to get rid of any unnecessary loops to get this to run faster and I think It'd be really useful :)

  • in The Place for Patreon Patrons
    Avatar for consolenaut

    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(_numberOfActiveK­eys, 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();
    
    
  • in The Place for Patreon Patrons
    Avatar for consolenaut

    Thanks, I'm looking forward to getting it working :)

    I'm going to give the compiled JS a shot today hopefully! You're right that I wouldn't need the MCP chip to scan a 4x16 matrix, originally I wanted to do it that way for future expandability but the MDBT42Q has 32 GPIO pins, so that gives me the headroom to do a full size keyboard (6x22) with some extra GPIO to spare to do things like LED backlighting/dedicated buttons

    So, set up watches on my columns, only scan when a watch gets triggered to stop us burning processor cycles at idle, and do the scanning in compiled JS or inline C to get a speed boost - that sounds like enough for me to get going with and see how far I get :)

    Thanks for helping me out on this one, I might go quiet for a couple of days while I try this out but I'll post and update to let you know how its working out!

  • in The Place for Patreon Patrons
    Avatar for consolenaut

    Thats a great suggestion, it sounds so obvious now you've said it :) I'll definitely be handling >1 press but that still takes a lot of load off the espruino, if nothing else it'll probably save me some power I would've burned sequentially turning pins on and off.

    I'm guessing I'll still want to do some messing around doing the scanning with the low level NRF52 lib to offload that to a peripheral rather than having the core do it. I'll likely still want to do processing on the core while keys are pressed/held to send keycodes with modifiers and things, but if I only have to do that after a pin state change thats great.

    My end goal is to build a mechanical keyboard firmware lib that handles the hardware level stuff & does a bit of extra helpful stuff like classify press/release cycles as a 'tap' or a 'hold' without the user having to do that, provides a lightweight mechanism for defining keymaps as a JSON file, and lets the user write their own javascript functions to make their keyboard do interesting stuff. This already exists in proper embedded code (QMK) but I'd love to do something similar on espruino since Javascript is so much more accessible :)

  • in Espruino Work
    Avatar for consolenaut

    Thought I'd give the Espruino Jobs board a go to see if someone can give me a hand with a project I'm working on :)

    I'm looking for a dev to write some code to scan a 4x16 matrix of keys using the Espruino MDBT42Q and an MCP23017 port expander chip.

    The code should be split into a hardware handling module (communication with the MCP chip/driving the matrix/detecting state changes), and a metadata module (keeping track of if keys were pressed or released & how long they were pressed for, determining if they key was 'tapped' or 'held') which fires events.

    The hardware handling module should:

    • Drive the matrix row outputs sequentially from MDBT42Q GPIO pins
    • Detect matrix column inputs from the MCP23017 (ideally using IRQs to handle state changes)
    • Be efficient, keeping as much off the main Espruino thread as possible (likely using NRF52 Programmable Peripheral Interconnect events/tasks)
    • Handle switch debouncing
    • Only fire a key press or release event when a key state has changed, not on every matrix scan
    • Emit hardwareKeyPress/hardwareKeyRelease events with the row & column in the data payload
    • Expose a method to get the current state of a single key
    • Expose a method to get the current state of all keys

    The metadata module should:

    • Keep track of when a key was pressed/released
    • Keep track of how long a key was pressed for
    • Figure out if a key press was a 'tap' or a 'hold', based on a configurable timeout (key press/release cycles below 200ms classed as a 'tap', above 200ms classed as a 'hold')
    • Fire events for all of the above:
      • 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
      • keyHold when a a key press/release 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

    Nice-to-haves:

    • Code should have unit tests, but I know that can be a little tricky with Espruino so I'm happy to drop this
    • Code should be versioned in git if possible, but not essential
    • Code should be reasonably 'tidy' and readable, split into multiple files if needed
    • Code should have at least basic documentation so I can modify it in future, or alternatively a quick handover so I have a good understanding of your approach

    I'm technical with a lot of commercial experience in Javascript & a decent amount with embedded code, but I'm super busy right now and some of this code is a little above my experience level (especially the NRF52 PPI). I can work with you to get this done or leave you to do it solo, whichever you prefer, and I'm pretty flexible on the requirements & implementation - if anything sounds crazy I'm open to a different approach :)

    Reference:
    Primer on keyboard matrix scanning: https://summivox.wordpress.com/2016/06/0­3/keyboard-matrix-scanning-and-debouncin­g/
    Espruino NRF52 Programmable Peripheral Interconnect library: https://www.espruino.com/NRF52LL
    Espruino MCP23017 library: https://www.espruino.com/MCP23xxx
    Untested MCP23017 library with interrupt handling: http://forum.espruino.com/comments/14047­043/

  • Avatar for consolenaut

    Hi there Gordon! I wanted to ask your advice on the best approach to take writing some keyboard matrix scanning code.

    I had a go at this a couple of years ago using an MCP23017, clocking the row outputs sequentially from the espruino & reading the column inputs on the MCP each cycle to determine which keys were pressed. This worked okay but I had a couple of weird quirks with the MCP chip & ended up burning way too many espruino cycles just turning the row lines on and off - not the best approach!

    I'm taking another crack at it, this time using an MDBT42Q instead of a Pico, I think using the NRF52's PPI to keep as much off the processor as possible is the right approach. I was thinking of using the NRF52's GPIO pins to drive the rows sequentially again, then listening for an interrupt from the MCP to know when to read its buffer to get the column states, and if anything has changed send it back up to the processor properly to send a keycode. I wanted to ask if that sounds sensible and, if so, which events and tasks it would make sense for me to set up.

    I appreciate that's a reeeeaallly long question, but I'd love a nudge in the right direction to get me going :)

  • in Puck.js, Pixl.js and MDBT42
    Avatar for consolenaut

    Tried the cutting-edge build but still seeing a tiny bit of a flicker, but I'm happy with that being an inefficiency in my code rather than a problem with PWM on the MDBT42Q. I appreciate you getting a build out so fast, and I'm totally down with sharing my code if you wanted to try and reproduce what I was seeing :)

    I dropped in a PCA9685 to handle the PWM control of my LED strips to free up the MDBT42Q and all is looking great now

Actions