Project tutorial

Pix-a-Sketch - A Virtual Etch-a-Sketch on an LED Matrix © GPL3+

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

  • 772 views
  • 0 comments
  • 8 respects

Components and supplies

Necessary tools and machines

3drag
3D Printer (generic)
09507 01
Soldering iron (generic)
CNC Router

Apps and online services

About this project

Overview

This project was created as a throwback to a much simpler toy, the Etch-a-Sketch, but instead of moving a tiny stylus to wipe aluminum particles off of a glass surface, this one plots pixels onto an RGB matrix.

I tried to remain true to the design by adding the ability to clear off the screen by shaking the device.

A Demonstration

Designing and Fabricating an Enclosure

I made it a point to model the Pix-a-Sketch after the actual Etch-a-Sketch toy, including the size, knobs, and position of the "screen". I began by using Fusion 360 to model each component, such as the matrix and rotary encoders. Then they were placed onto a virtual plywood board and had a plastic frame built around it. Each piece was 3D printed in PLA, and then the bottom wooden board was cut with a CNC router.

Below is a render of the final iteration.

Setting Up the Raspberry Pi and Matrix

You have probably heard it a thousand times by now, so just go view a guide on how to set up your Raspberry Pi here.

After connecting it to a WiFi router, install the rgb-matrix library by following the instructions below:

Just run

curl https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/rgb-matrix.sh >rgb-matrix.sh
sudo bash rgb-matrix.sh

Then press y to continue and choose option 2 to select the Adafruit Matrix HAT.

Then choose number 2 to free up pin 18 so that sound can still be output over the audio jack.

To test it go into the examples-api-use directory and run

sudo ./demo -D0 --led-rows=64 --led-cols=64 --hardware-mapping=adafruit-hat

You should see the demo running. Just hit ctrl-c to exit it.

You will also need to install the mpu6050 library with

pip3 install mpu6050-raspberrypi

and then add the user root to the i2c and gpio groups.

Reading Encoder Data

Because the RGB matrix takes up so many GPIO pins, using four extra GPIO pins for the two rotary encoders was out of the question, so I had to get a bit creative.

To solve this problem, I used an Arduino Nano as an I2C slave device that would be able to count the rotations of each encoder and then send that information to the host device when requested. Upon each request for encoder data, the total number of rotations is set back to zero, which gives relative positional data rather than absolute positioning.

Displaying an Image

The Python script begins by initializing the matrix, including information such as size, number of panels, and the pins it uses. Then, the two I2C devices are initialized and reset, along with a hardware push-button switch. Then the update() function is called repeatedly, which is similar to Arduino's loop() function. It reads in new encoder data, finds where the new position should be, and then moves the cursor to there.

Erasing and Resetting

If the device detects it is being shaken, 5 random lit-up pixels are selected and then turned off. To erase the entire matrix and return the cursor to its origin (0, 0), the hardware button can be held down for two seconds.

Code

Raspberry Pi CodePython
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 CodeC/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;
}

Custom parts and enclosures

Plastic Body
Split the model to print if necessary
Cut with CNC router

Schematics

Schematic for Encoder
Schematic m6twjjvmwi

Comments

Similar projects you might like

Playing "Flappy Bird" on an LED Matrix

Project tutorial by Arduino “having11” Guy

  • 4,861 views
  • 1 comment
  • 14 respects

Arduino LED Matrix Game of Life

Project showcase by aerodynamics

  • 6,207 views
  • 2 comments
  • 23 respects

LED Matrix

Project showcase by Team Windows IoT

  • 11,915 views
  • 1 comment
  • 21 respects

ATtiny85 Mini Arcade: Snake

Project tutorial by Arduino “having11” Guy

  • 3,471 views
  • 7 comments
  • 22 respects

48 x 8 Scrolling LED Matrix using Arduino.

Project tutorial by Prasanth K S

  • 43,516 views
  • 11 comments
  • 47 respects

Analog Clock with LED Matrix and Arduino

Project tutorial by LAGSILVA

  • 18,503 views
  • 17 comments
  • 67 respects
Add projectSign up / Login