Tiny E-Ink Picture Display

After being surprised by the capabilities of a three-color e-ink display (and struggling to get it to work!), I thought I’d put together a little guide.

Hardware

The hardware I used:

In this case, I messed up a little. I already had an ESP32 feather board from Adafruit, so I should have grabbed an e-ink “feather wing” which would have plugged straight into the ESP32 board.

But since I did not do that… here’s how I wired up the display:

  • 3V3 (power) to 3V
  • GND (ground) to GND
  • SCK (clock) to SCK
  • MISO to MISO
  • MOSI to MOSI
  • ECS to 27
  • D/C to 33
  • SRCS to 15
  • SDCS to 32

The rest I didn’t connect.

Note: Don’t be tempted to use pins 12 and 13!! Pin 13 is actually shared with the onboard LED, and documentation for pin 12 says “this pin has a pull-down resistor built into it, we recommend using it as an output only”.

The names on the board don’t quite match the names in the code, so here’s a cheatsheet:

#define EPD_DC 33     // D/C
#define EPD_CS 27     // ECS
#define SRAM_CS 15    // SRCS
#define EPD_BUSY -1   // can set to -1 to not use a pin
#define EPD_RESET -1  // can set to -1 and share with chip Reset (can't deep sleep)
#define SD_CS 32      // SDCS

The libraries use defaults for the rest of the pins automatically.

Setting Up Pictures

To fit the display exactly, pictures should be 250 pixels x 122 pixels. However, the display will crop as needed.

I used Gimp and ImageMagick to make the pictures, but the main thing is the images need to be 24-bit bitmaps. I couldn’t get Gimp to save images directly to a working format.

Here are the steps I took:

  • In Gimp, crop and resize to 250x122 pixels (I prefer to crop, resize to 250 pixels wide, crop again to 122 pixels high.)
  • Set palette:
    • Go to ImageModeIndexed...
    • Select “black and white 1 bit palette”
    • OR create a new black/red/white palette and use that
    • Choose a dithering option that looks good

As far as the color to use for “red”, I believe as long as it has the r value of 255, it will work.

Gimp image mode

Then…

  • FileExport As...
  • Rename to end in .bmp and save

Then…

From the command line, run

  • convert your_image.bmp -type truecolor your_image_24.bmp

Upload

Save the pictures to the root directory of a micro SD card, then put the card in the display (for me, it’s text “down”). The slot is spring-loaded, so just push on the end to eject.

Back of e-ink display showing micro SD card inserted

Arduino

I used the Arduino IDE (2.3.2).

For the board type, use “Adafruit ESP32 Feather”. (This may seem obvious, but it took me a while to figure out which to use!)

These are the libraries I used (via the IDE’s Library Manager):

Arduino IDE

Code

Full code is available here!

Other good examples to start from:

You’ll want to adjust the pin definitions like I did above if you are following along.

ThinkInk_213_Tricolor_RW is the right type to use for the display above.

In my code, I stripped out anything not related to loading and displaying images from the SD card. If you are doing something different, try looking at the other examples.

Update these lines with the names of your images!

  int num_images = 4; // Update with number of images

  // List image paths
  char *images[num_images] = {
    "/image1.bmp",
    "/image2.bmp",
    "/image3.bmp",
    "/image4.bmp",
  };

The program will cycle through the images and update every 5 minutes (or whatever you change the delay to - recommended minimum is 3 minutes).

Results

Here are some examples. Images look best from a little distance.

E-ink display connected to ESP32; showing picture of a computer

E-ink display showing picture of a house

E-ink display showing picture of a rhinoceros

No Power?!

Yep, the main cool thing about an e-ink display is that they don’t need power to maintain the image.

However, I found just removing power from the ESP32 would cause the red pixels to “bloom” and make everything a bit pink.

To prevent this, just disconnect power from the display first. It’s possible there is a way to fix this in the code - let me know if you figure it out!

Have Fun!

E-ink display showing picture of a handsome fella

DragonRuby: Deploying on Android

This is a little less-polished-than-usual post about how to build/install Android applications with DragonRuby Pro. on a Linux system. The higher tiers of features in DragonRuby tend to be less well-documented, so here is a bit of a braindump on getting games running on a real Android device.

(Mostly to remind myself how I did all this.)

Building the Package

To be clear, you will need the “Pro” version of DragonRuby to follow this guide.

Running

dragonruby-publish --package

will generate binaries and packages for all supported platforms and dump them in builds/.

Signing

Creating a Keystore

Following the DragonRuby documentation, create a keystore file like this:

keytool -genkey -v -keystore YOUR_APP.keystore -alias your_app_name -keyalg RSA -keysize 2048 -validity 10000

This will generate a file called YOUR_APP.keystore.

keytool is probably already on your system, but if not you’ll need to install a Java JDK package using your system tools.

Getting apksigner

To install the package on your Android device, you’ll need to sign the .apk build using apksigner. I’m only going to cover one specific way of getting this program on Linux. Your experience may vary.

The easiest way to get apksigner is probably to install Android Studio and go from there. However, I prefer doing things the hard way (and not installing a whole IDE to get one binary…)

First, go to https://developer.android.com/studio/ and scroll allll the way to the bottom to “Command line tools only”. Grab the .zip file from there.

Unzip it and find the sdkmanager binary, likely in latest/bin.

Run ./sdkmanager --list | grep build-tools to find the latest version of build-tools.

Then run something like ./sdkmanager --install "build-tools;34.0.0" to install.

The files will probably end up somewhere like ../../../build-tools. In there you’ll find apksigner!

Signing the Package

apksigner sign -ks YOUR_APP.keystore builds/YOUR-GAME-android.apk

You’ll need to figure out paths for apksigner, YOUR_APP.keystore, etc.

See the official docs for more, especially if publishing to the Google Play store.

Installing on Device

Enable Debug Mode

On your Android device setup developer options, enable USB debug mode, and plug your device into your computer.

Getting adb

The adb tool can be downloaded as part of the SDK Platform tools here: https://developer.android.com/tools/releases/platform-tools#downloads.html

Install the APK

adb install builds/your-game-android.apk

Viewing Logs

To view logs from the device:

adb logcat -e your-game

where your-game is the gameid or packageid configured in mygame/metadata/game_metadata.txt.

Remote Hotload

Building, signing, and installing packages becomes a bit painful if you are doing all that during development.

How about hot-loading code on your Android device just like you can on your development machine?

Setup

When running dragonruby, it opens up a webserver on port 9001. Besides what’s obviously visible on the webpage, it’s also how your device can connect and load code dynamically.

For that all to work, your development machine and Android device need to be on the same network, and your firewall needs to allow TCP connections on port 9001.

To verify it’s working, try opening up your development machines IP on port 9001 from your Android device (e.g., visit https://YOUR.DEV.IP:9001 in a browser).

Building

Run

dragonruby-publish --package-with-remote-hotload

To create a hotloading version of the game, then sign+install like above.

Running

Make sure your are running dragonruby locally on your development machine.

Then open your game on your mobile device - it should flash “Remote hotload enabled” at the bottom if it has been built properly. This does not ensure it actually connected to the development server, though!

To test, try making a visible change to a file on your local machine and see if the change is reflected on the Android device.

Somehow, magically, the hot-loaded changes will persist even through restarts. However, only changes made while the development server is running will be picked up.

Bonus: Detecting the “Back” “Button”

I am old, so I still use the virtual “back” button on Android.

In DragonRuby, this can be detected with

args.inputs.keyboard.key_down.ac_back

For example:

if inputs.keyboard.key_down.ac_back
  gtk.request_quit
end

DragonRuby: Following the Mouse

Recently I discovered it is very easy to have objects move towards (or away from) any points in DragonRuby.

This post might be a little easier if you’ve already read my post on moving in arbitrary directions, but actually the code here is even simpler.

If I skip any explanations here, the concepts should have been covered earlier in the series.

Setup

To get started, let’s just output a square (roughly) in the middle of the screen.

args.grid.center_x and args.grid.center_y are helpful for this instead of remembering/hardcoding the screen size.

In addition, the code uses args.state.tick_count == 0 to do some setup on the first tick.

def tick(args)
  # On the first tick...
  if args.state.tick_count == 0
    # Create a 50x50 pixel square in the middle of the screen
    args.state.player = { x: args.grid.center_x, y: args.grid.center_y, h: 50, w: 50}

    # Output that square on every tick
    args.outputs.static_solids << args.state.player
  end
end

A square in the middle of a window

Pretty basic!

(Note I’m skipping straight to static_solids because that’s what I’d prefer in a “real” game.)

Moving to a Point

Now we’ll move the “player” to a given point - in this case where the mouse is. In typical DragonRuby fashion, args.inputs.mouse can be used to as a point, even though it has a bunch of other information attached to it.

To get the angle from the player to the mouse, there is a very convenient angle_to method! (Also angle_from depending on which way you’d like to go.)

Just like args.inputs.mouse, the player solid can be treated as if it is a point, too.

One the angle is calculated, vector_x and vector_y will provide the magnitude to move in the x and y directions.

def tick(args)
  if args.state.tick_count == 0
    args.state.player = {
      x: args.grid.center_x,
      y: args.grid.center_y,
      h: 50,
      w: 50
    }

    args.outputs.static_solids << args.state.player
  end

  # Find angle from the square to the current location of the mouse
  angle = args.state.player.angle_to(args.inputs.mouse)
  
  # Move towards the mouse using the unit vector
  args.state.player.x += angle.vector_x
  args.state.player.y += angle.vector_y
end

Square following the mouse around

And… that’s it!! Less than 10 lines of code (without comments/spaces) and it just works.

So now let’s make it more complicated…

Centering

It is annoying that the player moves so the bottom, right-hand corner meets the mouse, there is a simple fix: use anchor_x and anchor_y. DragonRuby will automatically use the anchor point for calculations.

To learn more about anchor points, see this earlier post. But typically the values are set to 0.5 which means “middle of the object”.

def tick(args)
  if args.state.tick_count == 0
    args.state.player = {
      x: args.grid.center_x,
      y: args.grid.center_y,
      h: 50,
      w: 50,
      anchor_x: 0.5,
      anchor_y: 0.5,
    }

    args.outputs.static_solids << args.state.player
  end

  angle = args.state.player.angle_to(args.inputs.mouse)
  args.state.player.x += angle.vector_x
  args.state.player.y += angle.vector_y
end

Square following the mouse around, but centered

Moving to Classes

I continue to prefer taking an object/class-based approach in my own code. This makes it much easier to manage as the code grows larger, even if it seems silly for these small examples.

In a little departure from previous posts, I am not walking through the code in detail. (Please check out my other posts in this series to learn more!)

The main difference from the above is adding a speed to the movement calculation. Other than that, this is how I generally move from simple code like the above into a structure more suitable (in my opinion) as the code.

class Game
  attr_gtk

  def initialize(args)
    # Separate setup method is easier if you need to
    # reset during the game
    setup(args)
  end

  def setup(args)
    # Since static_sprites persists between ticks,
    # need to clear it in case of reset
    args.outputs.static_sprites.clear

    @player = Player.new(x: args.grid.center_x, y: args.grid.center_y, h: 50, w: 50, speed: 2)

    args.outputs.static_sprites << @player
  end

  def tick(args)
    @player.tick(args)
  end
end

class Player
  attr_sprite

  def initialize(x:, y:, h:, w:, speed:)
    @x = x
    @y = y
    @h = h
    @w = w
    @anchor_x = 0.5
    @anchor_y = 0.5
    @speed = speed
  end

  def tick(args)
    angle = self.angle_to(args.inputs.mouse)

    @x += angle.vector_x * @speed
    @y += angle.vector_y * @speed
  end
end

def tick(args)
  $Game ||= Game.new(args)
  $Game.tick(args)
end

One minor note if you actually run this code - the square will be white (default for sprites) instead of black (default for solids).