Bastiaan Slee
Published © GPL3+

Meshtastic 20.25

Are you ready to go off-grid? Introducing Meshtastic 20.25! Made to go camping or hiking, during a festival or just commuting to the office.

AdvancedWork in progressOver 1 day440
Meshtastic 20.25

Things used in this project

Hardware components

Wio Tracker L1 E-ink
Seeed Studio Wio Tracker L1 E-ink
×1
E-ink display 2.13 Inch
The Seeed Studio Wio Tracker L1 that I ordered didn't had a screen as the order time was too long. So I did order the one with OLED and a separate screen.
×1
Mini keyboard
Full QWERTY on a Meshtastic device, just borrow from a full keyboard!
×1
RP2350 Development board
For the Keyboard matrix scanning
×1
Solar panel
×1
Vibration motor
×1
FPC 24pin 0.5mm extension connector
×1
FPC 24pin 0.5mm cable of 30cm reversed
×1
Lithium Polymer (LiPo) battery 3500mAh
From my drawer, this type is not for sale anymore, but can be replaced with any other LiPo battery
×1
Reset switch
The 5mm NO (Normally Open) item
×1
ON/OFF switch
The 7mm L (locking) item. NOTE: THIS WAS THE WRONG SWITCH. YOU NEED 3 PINS!
×1
BC548 NPN transistor
×1
USB-C extension cable
×1
USB-C mount
×1
Enameled copper wire
×1

Software apps and online services

Meshtastic
Meshtastic
Fusion
Autodesk Fusion

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

F3Z file: Meshtastic 20.25

Enclosure final design as of 2025-10-07, made in Fusion 360. This file does include all models as well.

F3D file: Seeed Wio Tracker L1 with OLED

Fusion 360 Model design as of 2025-09-05
Model created and provided by Seeed Studio. I did only add colors.

F3D file: E-ink screen (part of Seeed Wio Tracker L1 kit)

Fusion 360 Model design as of 2025-09-11

F3D file: LoRa antenna (part of Seeed Wio Tracker L1 kit)

Fusion 360 Model design as of 2025-09-05

F3D file: GNSS antenna (part of Seeed Wio Tracker L1 kit)

Fusion 360 Model design as of 2025-09-05

STL file for enclosure: back

This file can be loaded in your 3D printing software

Sketchfab still processing.

STL file for enclosure: front

This file can be loaded in your 3D printing software

Sketchfab still processing.

STL file for enclosure: front-ring

This file can be loaded in your 3D printing software.

Sketchfab still processing.

Schematics

Vibration Motor

The vibration motor uses more amps than a GPIO pin can provide. With a NPN tranisitor (BC548) you can switch full power directly from GND and 3.3V pins

Code

Keyboard Matrix

C/C++
The RP2350 does scan the Keyboard Matrix. On I2C request it sends the first queued keypress to the Wio Tracker L1.
/*    Sketch for Keyboard Matrix
 *    Based on project by by Cameron Coward: https://www.hackster.io/cameroncoward/64-key-prototyping-keyboard-matrix-for-arduino-4c9531
 *	  Based on the M5 Stack Card Keyboard: https://github.com/m5stack/M5-ProductExampleCodes/blob/master/Unit/CARDKB/firmware_328p/CardKeyBoard/CardKeyBoard.ino
 */


#include <Arduino.h>
#include <MD_CirQueue.h>
#include <Wire.h>

// Define I2C setup
#define I2C_DEV_ADDR 0x5f
#define I2C_PIN_SDA 4
#define I2C_PIN_SCL 5

char RequestedEventI2C = 0;


// Define circular queue
#define QUEUE_SIZE 20
MD_CirQueue Queue(QUEUE_SIZE, sizeof(uint8_t));



// Define keyboard matrix
const byte ROWS = 8;  // rows (horizontal matrix)
const byte COLS = 12; // columns (vertical matrix)
const byte KEYS = 62; // keys (that will be mapped)

const byte rowPins[ROWS] = {17, 18, 19, 25, 26, 27, 28, 29}; //connect to the row pinouts of the keypad
const byte colPins[COLS] = {2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 16, 1}; //connect to the column pinouts of the keypad
const byte ledPin = 0;

// keyState will contain pressed keys for each row, to avoid double key press registration if a key is pressed for a longer time
char keyState[ROWS][COLS] = {
  {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,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,0,0,0,0,0,0,0,0}
};

// ASCII codes for keys with no modifiers pressed. Unused keys are NULL (0),
// Modifiers (shift and capslock) will be looked up later.
// See for virtual key codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
const char keyMap[ROWS][COLS] = {
/* PINS:    2,      3,      6,      7,      8,      9,      10,     11,     12,     13,     16,     1 */  
/* 17 */  { 0,      0,      0,      0,      0,      0,      8,      0x25,   0x27,   0,      0x08,   0     },    // WWW COM _ _ _ _ DEL LEFT RIGHT _ BACK S8
/* 18 */  { 0,      0,      0,      '\\',   0,      0,      0,      '\'',   0,      0x28,   0x26,   0x29  },    // _ S2 _ \ END HOME _ ' S4 DOWN UP SELECT
/* 19 */  { 0,      0,      0,      0,      0,      0,      0,      0,      0,      0,      0,      0x24  },    // ESC F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 S1
/* 25 */  { '`',    '1',    '2',    '3',    '4',    '5',    '6',    '7',    '8',    '9',    '0',    0     },    // ` 1 2 3 4 5 6 7 8 9 0 S7
/* 26 */  { 0x09,   'q',    'w',    'e',    'r',    't',    'y',    'u',    'i',    'o',    'p',    0     },    // TAB q w e r t y u i o p S3
/* 27 */  { 0x14,   'a',    's',    'd',    'f',    'g',    'h',    'j',    'k',    'l',    0x0D,   0x23  },    // CAPS a s d f g h j k l ENTER S5
/* 28 */  { 0x10,   'z',    'x',    'c',    'v',    'b',    'n',    'm',    ',',    '.',    ';',    0     },    // SHIFT z x c v b n m , . ; _
/* 29 */  { 0,      0,      '-',    '=',    0,      ' ',    0,      0,      '[',    ']',    '/',    0     }     // FN CTRL - = ALT SPACE ALTGR WIN [ ] / S6
};

unsigned char keyModifier[KEYS][4] =
{ // norm,  shift,  caps,   shift+caps
  {  0,     0,      0,      0     },  // no key

  { '`',    '~',    '`',    '~'   },  // `
  { '1',    '!',    '1',    '!'   },  // 1
  { '2',    '@',    '2',    '@'   },  // 2
  { '3',    '#',    '3',    '#'   },  // 3
  { '4',    '$',    '4',    '$'   },  // 4
  { '5',    '%',    '5',    '%'   },  // 5
  { '6',    '^',    '6',    '^'   },  // 6
  { '7',    '&',    '7',    '&'   },  // 7
  { '8',    '*',    '8',    '*'   },  // 8
  { '9',    '(',    '9',    '('   },  // 9
  { '0',    ')',    '0',    ')'   },  // 0
  { 0x08,   0x08,   0x08,   0x08  },  // backspace

  { 0x09,    0x09,   0x09,  0x09  },  // tab
  { 'q',    'Q',    'Q',    'q'   },  // q
  { 'w',    'W',    'W',    'w'   },  // w
  { 'e',    'E',    'E',    'e'   },  // e
  { 'r',    'R',    'R',    'r'   },  // r
  { 't',    'T',    'T',    't'   },  // t
  { 'y',    'Y',    'Y',    'y'   },  // y
  { 'u',    'U',    'U',    'u'   },  // u
  { 'i',    'I',    'I',    'i',  },  // i
  { 'o',    'O',    'O',    'o'   },  // o
  { 'p',    'P',    'P',    'p'   },  // p
  { '\\',   '\\',   '\\',   '\\'  },  // \

  { 0x14,   0x14,   0x14,   0x14  },  // capslock
  { 'a',    'A',    'A',    'a'   },  // a
  { 's',    'S',    'S',    's'   },  // s
  { 'd',    'D',    'D',    'd'   },  // d
  { 'f',    'F',    'F',    'f'   },  // f
  { 'g',    'G',    'G',    'g'   },  // g
  { 'h',    'H',    'H',    'h'   },  // h
  { 'j',    'J',    'J',    'j'   },  // j
  { 'k',    'K',    'K',    'k'   },  // k
  { 'l',    'L',    'L',    'l'   },  // l
  { 0x0D,   0x0D,   0x0D,   0x0D  },  // enter

  { 0x10,   0x10,   0x10,   0x10  },  // shift
  { 'z',    'Z',    'Z',    'z',  },  // z
  { 'x',    'X',    'X',    'x' 	},  // x
  { 'c',    'C',    'C',    'c'   },  // c
  { 'v',    'V',    'V',    'v'   },  // v
  { 'b',    'B',    'B',    'b'   },  // b
  { 'n',    'N',    'N',    'n'   },  // n
  { 'm',    'M',    'M',    'm'   },  // m
  { ',',    '<',    ',',    '<'   },  // ,
  { '.',    '>',    '.',    '>'   },  // .
  { ';',    ':',    ';',    ':'   },  // ;
  { '\'',   '"',    '\'',   '"'   },  // '

  { '-',    '_',    '-',    '_'   },  // -
  { '=',    '+',    '=',    '+'   },  // =
  { ' ',    ' ',    ' ',    ' '   },  // space
  { '[',    '{',    '[',    '{'   },  // [
  { ']',    '}',    ']',    '}'   },  // ]
  { '/',    '?',    '/',    '?'   },  // /

  { 0x25,   0x25,   0x25,   0x25  },  // LEFT
  { 0x26,   0x26,   0x26,   0x26  },  // UP
  { 0x27,   0x27,   0x27,   0x27  },  // RIGHT
  { 0x28,   0x28,   0x28,   0x28  },  // DOWN
  { 0x29,   0x29,   0x29,   0x29  },  // SELECT
  { 0x24,   0x24,   0x24,   0x24  },  // S1 - Home
  { 0x23,   0x23,   0x23,   0x23  },  // S5 - Message
};

int modifier = 0; // 0-> normal   1-> shift    2-> caps   3-> chift+caps
bool caps = false;  // is caps lock on?
bool shift = false; // is shift pressed?
unsigned char pressedKey = 0;




// the follow variables is a long because the time, measured in miliseconds,
// will quickly become a bigger number than can be stored in an int.
long keyboardInterval = 500;           // interval at which to check keyboard (microseconds)
long previousKeyboardMicros = 0;        // will store last time keyboard was checked






void onRequestEventI2C() {

  if (RequestedEventI2C == 4) { // initialisation
    Wire.write(0x00);
    Serial.println("onRequestEventI2C: 4 >> 0x00");
    RequestedEventI2C = 99;

  } else if (RequestedEventI2C == 99) { // first query after turning on is to ScanI2C::DeviceType::CARDKB kb_model
    Wire.write(0x00);
    Serial.println("onRequestEventI2C: 99 >> 0x00");
    RequestedEventI2C = 0;

  } else if (RequestedEventI2C == 0) { // ask for character is without a Receive event, so if we have a character we just return that
    if (!Queue.isEmpty()) {
      uint8_t n;
      Queue.pop((uint8_t *)&n);
      Wire.write(n);

      Serial.print("onRequestEventI2C: ");
      Serial.print((uint8_t)RequestedEventI2C);
      Serial.print(" >> 0x");
      Serial.println(n, HEX);

    } else {
      Wire.write(0);

    }

  } else { // unknown code
    Wire.write(0x00);
    
    Serial.print("onRequestEventI2C: ");
    Serial.print((uint8_t)RequestedEventI2C);
    Serial.println(" >> 0x00");
    RequestedEventI2C = 0;
  }
}

void onReceiveEventI2C(int bytes_count) {     // bytes_count gives number of bytes in rx buffer   
  RequestedEventI2C = ((uint8_t)Wire.read());
  Serial.print("onReceiveEventI2C: ");
  Serial.println((uint8_t)RequestedEventI2C);
}


void setup() {

  Queue.begin();

  Wire.setSDA(I2C_PIN_SDA);  // setup SDA pin
  Wire.setSCL(I2C_PIN_SCL);  // setup SCL pin
  Wire.begin(I2C_DEV_ADDR);  // join i2c bus as "slave"
  Wire.onReceive(onReceiveEventI2C);    // i2c interrupt receive
  Wire.onRequest(onRequestEventI2C);    // i2c interrupt send

  Serial.begin(9600);

  // setup all column pin as inputs with internal pullup resistors
  for (int i = 0; i < COLS; i++) {    // iterate through each COL
    pinMode(colPins[i], INPUT_PULLUP); 
  }
  for (int i = 0; i < ROWS; i++) {  // iterate through each ROW
    pinMode(rowPins[i], OUTPUT);
    digitalWrite(rowPins[i], HIGH);
  }

  // LED for CapsLock
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

}

void loop() {

  unsigned long currentMicros = micros(); // how many microseconds has the Arduino been running?
  
  if(currentMicros - previousKeyboardMicros > keyboardInterval) { // if elapsed time since last check exceeds the interval
    previousKeyboardMicros = currentMicros;    // save the last time the keyboard was checked
    checkKeyboard(); // check all of the keys and print out the results to serial
  }
}

void checkKeyboard() {

  // check if shift key is pressed
  digitalWrite(rowPins[6], LOW);
  if (digitalRead(colPins[0]) == LOW) {
    shift = true;
  } else {
    shift = false;
  }
  digitalWrite(rowPins[6], HIGH);

  // all other keys
  for (int i = 0; i < ROWS; i++) {                    // iterate through each row
    digitalWrite(rowPins[i], LOW);
    for (int j = 0; j < COLS; j++) {                  // iterate through each bit in that row

      if (digitalRead(colPins[j]) == LOW) {           // That is a key press!

        if (keyState[i][j] == 0) {                       // only allows button press if state has changed
          keyState[i][j] = 1;

          if((i == 5 && j == 0)) {
            caps =! caps;
            digitalWrite(ledPin, caps);
          }

          if (shift == true && caps == true) {
            modifier = 3;
          } else if (shift == true) {
            modifier = 1;
          } else if (caps == true) {
            modifier = 2;
          } else {
            modifier = 0;     
          }

          pressedKey = keyMap[i][j];

          if(pressedKey != 0) {
            if (modifier != 0) {
              for (int q = 0; q < KEYS; q++) {                    // iterate through key list
                if(pressedKey == keyModifier[q][0]) {
                  pressedKey = keyModifier[q][modifier];
                }
              }
            }
            Queue.push((uint8_t *)&pressedKey);       // Store the key press in queue

            // print some stuff to see what is happening
            Serial.print("Keypress: ");
            Serial.print("(");
            Serial.print(i);
            Serial.print(",");
            Serial.print(j);
            Serial.print(") ");
            Serial.print(pressedKey);
            Serial.print(" 0x");
            Serial.print(pressedKey,HEX);
            Serial.print(" shift: ");
            Serial.print(shift);
            Serial.print(" caps: ");
            Serial.print(caps);
            Serial.println();
          }

        }  
      } else {
        keyState[i][j] = 0;
      }
    }
    digitalWrite(rowPins[i], HIGH);
  }

}

Credits

Bastiaan Slee
6 projects • 39 followers
Tinkerer with the main goal: fun! Repurposing old devices using Raspberry Pi, Arduino (+clones), 3D Printing, Laser and CNC.

Comments