Evan Rust
Published © GPL3+

Pix-a-Sketch - A Virtual Etch-a-Sketch on an LED Matrix

Use two rotary encoders to draw whatever picture your heart desires, and then shake it to erase the image — just like the real thing!

IntermediateFull instructions provided6 hours6,589

Things used in this project

Hardware components

Raspberry Pi 3 Model B+
Raspberry Pi 3 Model B+
×1
Adafruit RGB Matrix HAT
×1
DFRobot 64 x 64 RGB LED Matrix Panel
×1
6 DOF Sensor - MPU6050
DFRobot 6 DOF Sensor - MPU6050
×1
Arduino Nano R3
Arduino Nano R3
×1
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×2
Momentary Pushbutton
×1

Software apps and online services

VS Code
Microsoft VS Code
Arduino IDE
Arduino IDE
Fusion 360
Autodesk Fusion 360

Hand tools and fabrication machines

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

Story

Read more

Custom parts and enclosures

Plastic Body

Split the model to print if necessary

Base

Cut with CNC router

Schematics

Schematic for Encoder

Code

Raspberry Pi Code

Python
import smbus
import sys
from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics
from PIL import Image, ImageDraw
from mpu6050 import mpu6050
from gpiozero import Button
from time import sleep
import random

def constrain(x, a, b):
    if x < a:
        return a
    elif x > b:
        return b
    else:
        return x

class PixelSketch(object):
    ARDUINO_ADDRESS = 0x10
    MPU_ADDRESS = 0x68
    BTN_PIN = 25
    SHAKE_THRESHOLD = 4.0   # shake harder than 6 m/s^2 to clear
    def __init__(self, interrupt_en = 0, matrix_w = 64, matrix_h = 64):
        self.matrix_w = matrix_w
        self.matrix_h = matrix_h
        self.options = RGBMatrixOptions()
        self.options.rows = matrix_w
        self.options.cols = matrix_h
        self.options.chain_length = 1
        self.options.parallel = 1
        self.options.hardware_mapping = 'adafruit-hat'
        self.matrix = RGBMatrix(options=self.options)
        self.matrix.Clear()
        self.sensor = mpu6050(PixelSketch.MPU_ADDRESS)
        self.bus = self.sensor.bus
        self.bus.write_byte_data(PixelSketch.ARDUINO_ADDRESS, 0x00, 0x01)   # reset the Arduino
        self.bus.write_byte_data(PixelSketch.ARDUINO_ADDRESS, 0x01, interrupt_en)   # enable interrupt
        self.button = Button(PixelSketch.BTN_PIN, bounce_time = .1, hold_time = 2.0)
        self.button.when_held = self.resetBoard
        self.cursorPosition = [0, 0]
        self.pixelMap = [[0 for x in range(matrix_w)] for y in range(matrix_h)]     # keep track of matrix in memory
        self.litPixels = []
    def update(self):
        encoderAmts = self.readRotaryEncoderData()
        goalPos = [self.cursorPosition[0] + encoderAmts[0], self.cursorPosition[1] + encoderAmts[1]]
        goalPos[0] = constrain(goalPos[0], 0, self.matrix_w-1)
        goalPos[1] = constrain(goalPos[1], 0, self.matrix_h-1)
        stepAmts = [0, 0]
        if encoderAmts[0] != 0:
            stepAmts[0] = int(encoderAmts[0] / abs(encoderAmts[0]))
        if encoderAmts[1] != 0:
            stepAmts[1] = int(encoderAmts[1] / abs(encoderAmts[1]))
        shouldContinue = True
        while shouldContinue:
            prevPos = [self.cursorPosition[0], self.cursorPosition[1]]
            self.matrix.SetPixel(prevPos[0], prevPos[1], 150, 150, 0)
            if self.cursorPosition[0] == goalPos[0]:
                shouldContinue = False
            else:
                self.cursorPosition[0] += stepAmts[0]
                self.pixelMap[self.cursorPosition[1]][self.cursorPosition[0]] = 1
                self.litPixels.append([self.cursorPosition[0], self.cursorPosition[1]])
                self.matrix.SetPixel(self.cursorPosition[0], self.cursorPosition[1], 150, 150, 0)
                self.matrix.SetPixel(prevPos[0], prevPos[1], 0, 0, 150)
                prevPos = [self.cursorPosition[0], self.cursorPosition[1]]
                shouldContinue = True
            if self.cursorPosition[1] == goalPos[1]:
                shouldContinue = False
            else:
                self.cursorPosition[1] += stepAmts[1]
                self.pixelMap[self.cursorPosition[1]][self.cursorPosition[0]] = 1
                self.litPixels.append([self.cursorPosition[0], self.cursorPosition[1]])
                self.matrix.SetPixel(self.cursorPosition[0], self.cursorPosition[1], 150, 150, 0)
                self.matrix.SetPixel(prevPos[0], prevPos[1], 0, 0, 150)
                shouldContinue = True
        if self.isBeingShaken():
            for x in range(5):
                if len(self.litPixels) > 0:
                    randChoice = random.choice(self.litPixels)
                    self.pixelMap[randChoice[1]][randChoice[0]] = 0
                    self.matrix.SetPixel(randChoice[0], randChoice[1], 0, 0, 0)
                    self.litPixels.remove(randChoice)
                sleep(.005)
        sleep(0.1)

    def isBeingShaken(self):
        vals = self.sensor.get_accel_data()
        print(vals)
        if abs(vals['x']) >= PixelSketch.SHAKE_THRESHOLD or abs(vals['y']) >= PixelSketch.SHAKE_THRESHOLD:
            return True
        return False

    def readRotaryEncoderData(self):
        encoderValues = [0, 0]
        try:
            encoderValues[0] = self.bus.read_byte_data(PixelSketch.ARDUINO_ADDRESS, 0x02)
            encoderValues[1] = self.bus.read_byte_data(PixelSketch.ARDUINO_ADDRESS, 0x03)
            print(encoderValues)
            for index, val in enumerate(encoderValues):
                if val > 127:
                    encoderValues[index] = (256 - val) * -1
        except OSError:
            pass
        encoderValues[0] = -encoderValues[0]
        encoderValues[1] = -encoderValues[1]
        return encoderValues

    def resetBoard(self):
        self.litPixels = []
        self.cursorPosition = [0, 0]
        self.pixelMap = [[0 for x in range(self.matrix_w)] for y in range(self.matrix_h)]
        self.bus.write_byte_data(PixelSketch.ARDUINO_ADDRESS, 0x00, 0x01)   # reset the Arduino
        self.matrix.Clear()

if __name__ == "__main__":
    sketch = PixelSketch()
    while 1:
        sketch.update()

Arduino Nano Code

C/C++
#include <Wire.h>
#include "EncoderStepCounter.h"

#define ENCODER_PINL1 2
#define ENCODER_PINL2 3
#define ENCODER_PINR1 4
#define ENCODER_PINR2 5
#define INT_READY_PIN 6

#define OP_RESET 0x00
#define OP_INTERRUPT_EN 0x01
#define OP_READ_ENCODER_L 0x02
#define OP_READ_ENCODER_R 0x03

#define SELF_I2C_ADDR 0x10

volatile uint8_t opcode = 0x04;
int8_t registerStack[4];

volatile int8_t posL = 0, posR = 0;

EncoderStepCounter encoderL(2, 3);
EncoderStepCounter encoderR(4, 5);

void setup() {
    Serial.begin(115200);
    delay(50);
    Serial.println("It works");
    Wire.begin(SELF_I2C_ADDR);
    Wire.onRequest(requestEvent);
    Wire.onReceive(receiveEvent);
    encoderL.begin();
    encoderR.begin();
    softReset();
}

void loop() {
    encoderL.tick();
    encoderR.tick();

    int8_t pos = encoderL.getPosition();
    if(pos != 0) {
        posL += pos;
        Serial.print("L: "); Serial.println(posL);
        encoderL.reset();
    }
    pos = encoderR.getPosition();
    if(pos != 0) {
        posR += pos;
        Serial.print("R: "); Serial.println(posR);
        encoderR.reset();
    }
}

void receiveEvent(int bytes) {
    opcode = Wire.read();
    if(bytes > 1) {
        switch(opcode) {
            case OP_RESET:
                if(Wire.read()) softReset();
                break;
            case OP_INTERRUPT_EN:
                registerStack[opcode] = Wire.read();
                break;
        }
    }
}

void requestEvent() {
    switch(opcode) {
        case OP_READ_ENCODER_L:
            Wire.write(posL);
            posL = 0;
            break;
        case OP_READ_ENCODER_R:
            Wire.write(posR);
            posR = 0;
            break;
    }
}

void softReset() {
    encoderL.reset();
    encoderR.reset();
    opcode = 0x04;
}

Credits

Evan Rust

Evan Rust

120 projects • 1049 followers
IoT, web, and embedded systems enthusiast. Contact me for product reviews or custom project requests.

Comments