Edited at

THETA Z1 OLED描画ライブラリ的なものつくりました


はじめに

リコーの @KA-2 です。

弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。

RICOH THETA VRICOH THETA Z1は、OSにAndroidを採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます(詳細は本記事の末尾を参照)。

日本でも2019年5月24日に発売を開始したRICOH THETA Z1には、OLEDディスプレイが搭載されています。

THETA_Z1.jpg

THETAプラグインからOLEDを操る基本的な方法については、こちらの記事で紹介しています。この記事と共に、THETAプラグインのドキュメントからOLEDを操るためのAPI(「Broadcast Intent」の「Control the OLED」のブロック)を読むと判るのですが、そのまま提供されたAPIだけを利用する場合には、以下のような不都合が生じます。


  • 点、線、四角、丸などの基本図形を描画しにくい。
    (Bitmapクラスにデータを詰め込めばいいだけではありますが…)

  • カラー画像をインプットした場合、2値化するときの輝度閾値を指定できない。

  • 画像表示はプラグインから制御可能な領域全域(下部 128 x 24 dot)だけ。部分的な表示ができない。

  • 画像と文字を混在させられない。

  • 文字表示では、小文字を指定しても大文字に変換されてしまう。

  • 等幅フォントではないので、変化する値を表示させた場合、表示が左右に揺れたり、上下行の位置合わせができず、見栄えが悪くなる可能性がある。

そこで、、、

取り急ぎ、以下のような「OLED描画ライブラリ」的なものを作成してみました。

OLEDライブラリ必要最小限機能イメージ.png

これらは、グラフィカルな白黒表示器を利用したことがある電子工作クラスタにはおなじみのものだと思います。

「描画のターン(表示データを作成) → 出来上がったデータを表示」

という流れで使うので、描画のターンでライブラリのあわせ技をして、凝った画面を作ることもできます。

このライブラリでできることは以下動画な感じです。

Youtube版はこちら


ライブラリの説明

記事のタイトルに「OLED描画ライブラリ的なもの」としています。

このようなソースコードは、THETA Plugin SDKから呼び出されている「pluginlibrary」に含まれているべきなのですが、2019年5月末時点では含んでおりません。

今回紹介したメソッド以外に、いくつか追加が必要な処理があるかもなどなど、もう少し吟味をするお時間をくださいませ。


用意したメソッド

分類

メソッド名称                                               
概要

コンストラクタ
Oled (Context context)
引数からMainActivityのコンテキストを渡します。

公式OLED APIのラッパー
displaySet(String mode)
brightness (int value)
hide ()
blink (int msec)

こちらで公開しているOLED関連のBroadcast Intentを簡単に呼び出せるようラップしたものです。

基本処理
draw ()
clear() ※引数違い2種
invert (boolean invFlag)
drawは諸々の描画処理したデータを公式OLED API(Broadcast Intent)で表示させる処理です。
clearは描画データのクリアします。
invert は描画データを反転させます。

画像描画
setBitmap() ※引数違い4種
輝度閾値とassetsフォルダにある画像ファイル名、または、Bitmapオブジェクトを指定して描画します。OLED全体に描画するものと、細かな表示エリア指定ができるものの2種類があります。
扱える画像ファイルの形式は BitmapFactory.decodeStream()でデコードできるもの全てです。bmp,jpg,pngなどが扱えます。

点を描く
pixel() ※引数違い2種
指定した位置に点を描きます。
色(白/黒)のオプションと指定色と下地が同じ場合に色を反転させるオプションもあります。

線を描く
line() ※引数違い2種
lineH() ※引数違い2種
lineV() ※引数違い2種
指定した2点間に線を描きます。
色(白/黒)のオプションと指定色と下地が同じ場合に色を反転させるオプションもあります。
名称末尾が'H'は水平'V'は垂直に特化した処理です。

四角を描く
rect() ※引数違い2種
rectFill() ※引数違い2種
左上座標と幅、高さを指定して四角を描画します。
名称に'Fill'があるものは中身を塗りつぶします。
色(白/黒)のオプションと指定色と下地が同じ場合に色を反転させるオプションもあります。

円を描く
circle() ※引数違い2種
circleFill() ※引数違い2種
中心座標と半径を指定して円を描画します。
名称に'Fill'があるものは中身を塗りつぶします。
色(白/黒)のオプションと指定色と下地が同じ場合に色を反転させるオプションもあります。
ただし、circleFill()については、ソースコード中のコメントに記載した理由により、下地色との反転させるオプションは無効にしてあります。

文字(列)を描く
setChar() ※引数違い2種
setString() ※引数違い2種
指定した座標から文字を描画します。0x20~0x7EのASCIIコードと、ちょっとしたおまけの記号を描画できます。1文字は5x7 dot(表示エリアは6x8 dot)です。最大21文字×3行が行えます。
色(白/黒)のオプションと指定色と下地が同じ場合に色を反転させるオプションもあります。
スクロールはしません。

余談となりますが、0x20~0x7EのASCIIコードを網羅するような表示サンプルを以下に載せておきます。

font1.png

font2.png

「このフォントは好みではない」「大きな文字のフォントも欲しい」「日本語のフォントが欲しい」などなどのご要望がある場合には、本記事に掲載したソースコードを参考にご自身で追加してみてください。

(そして、GitHubにこのライブラリが公開されたあかつきには、プルリクなどもして頂けたらうれしーです!)


ソースコード

ライブラリ部分のソースコードは以下のとおりです。

畳んで全文掲載しておきます。

「Oled.java」ソースコード全文


Oled.java

/**

* Copyright 2018 Ricoh Company, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.theta360.pluginapplication.oled;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.content.res.*;
import android.util.Log;

import java.io.InputStream;
import java.io.IOException;

public class Oled {
private final Context context;

private static final String ACTION_OLED_DISPLAY_SET = "com.theta360.plugin.ACTION_OLED_DISPLAY_SET";
private static final String ACTION_OLED_BRIGHTNESS_SET = "com.theta360.plugin.ACTION_LED_BRIGHTNESS_SET";
private static final String ACTION_OLED_IMAGE_SHOW = "com.theta360.plugin.ACTION_OLED_IMAGE_SHOW";
private static final String ACTION_OLED_IMAGE_BLINK = "com.theta360.plugin.ACTION_OLED_IMAGE_BLINK";
private static final String ACTION_OLED_HIDE = "com.theta360.plugin.ACTION_OLED_HIDE";

public static final String DISPLAY_SET_PLUGIN = "plug-in";
public static final String DISPLAY_SET_BASIC = "basic";

public static final int OLED_WIDTH = 128;
public static final int OLED_HEIGHT = 24;

public int black = 0xFF000000 ;
public int white = 0xFFFFFFFF ;

private Bitmap screen = null;
public int imgWidth = 0;
public int imgHeight = 0;

public Oled (Context context) {
this.context = context;
screen = Bitmap.createBitmap(OLED_WIDTH, OLED_HEIGHT, Bitmap.Config.ARGB_8888 );
}

public void displaySet(String mode) {
Intent oledIntentSet = new Intent(ACTION_OLED_DISPLAY_SET);
if ( mode.equals(DISPLAY_SET_PLUGIN) ) {
oledIntentSet.putExtra("display", DISPLAY_SET_PLUGIN);
} else {
oledIntentSet.putExtra("display", DISPLAY_SET_BASIC);
}
this.context.sendBroadcast(oledIntentSet);
}

public void brightness (int value) {
if (value<0) {
value = 0;
}
if (value>100) {
value=100;
}
Intent oledBrightnessIntent = new Intent(ACTION_OLED_BRIGHTNESS_SET);
oledBrightnessIntent.putExtra("target", "OLED");
oledBrightnessIntent.putExtra("brightness", value);
context.sendBroadcast(oledBrightnessIntent);
}
public void hide () {
Intent imageIntent = new Intent(ACTION_OLED_HIDE);
context.sendBroadcast(imageIntent);
}
public void blink (int msec) {
if (msec<250) {
msec=250;
}
if (msec>2000) {
msec=2000;
}
Intent imageIntent = new Intent(ACTION_OLED_IMAGE_BLINK);
imageIntent.putExtra("bitmap", screen);
imageIntent.putExtra("period", msec);
context.sendBroadcast(imageIntent);
}

public void draw () {
Intent imageIntent = new Intent(ACTION_OLED_IMAGE_SHOW);
imageIntent.putExtra("bitmap", screen);
context.sendBroadcast(imageIntent);
}

public void clear () {
clear(black);
}
public void clear (int color) {
for (int width=0; width<OLED_WIDTH; width++) {
for (int height = 0; height < OLED_HEIGHT; height++) {
screen.setPixel(width, height, color);
}
}
}

public void invert (boolean invFlag) {
if (invFlag) {
Intent imageIntent = new Intent(ACTION_OLED_IMAGE_SHOW);
Bitmap invertScreen = Bitmap.createBitmap(OLED_WIDTH, OLED_HEIGHT, Bitmap.Config.ARGB_8888 );

for (int width=0; width<OLED_WIDTH; width++) {
for (int height = 0; height < OLED_HEIGHT; height++) {
if ( screen.getPixel(width, height) == white ) {
invertScreen.setPixel(width, height, black);
} else {
invertScreen.setPixel(width, height, white);
}
}
}
imageIntent.putExtra("bitmap", invertScreen);
context.sendBroadcast(imageIntent);
} else {
draw();
}
}

public void setBitmap(int threshold, String assetsFileName) {
setBitmap(0, 0, OLED_WIDTH, OLED_HEIGHT, 0, 0, threshold, assetsFileName) ;
}
public void setBitmap(int threshold, Bitmap inBitmap) {
setBitmap(0, 0, OLED_WIDTH, OLED_HEIGHT, 0, 0, threshold, inBitmap) ;
}
public void setBitmap(int scnX, int scnY, int scnWidth, int scnHeight, int imgX, int imgY, int threshold, String assetsFileName) {
AssetManager assetManager = context.getAssets();
InputStream inputStream;
Bitmap fileBitmap = null;

try {
inputStream = assetManager.open(assetsFileName);
fileBitmap = BitmapFactory.decodeStream(inputStream);

setBitmap(scnX, scnY, scnWidth, scnHeight, imgX, imgY, threshold, fileBitmap) ;

} catch (IOException e) {
e.printStackTrace();
}
}
public void setBitmap(int scnX, int scnY, int scnWidth, int scnHeight, int imgX, int imgY, int threshold, Bitmap inBitmap) {
int xStart;
int xEnd;
int yStart;
int yEnd;
int scnOffsetX=scnX;
int scnOffsetY=scnY;

if ( ( (0<=scnX) && (scnX<OLED_WIDTH) ) && ( (0<scnWidth) || ((scnX+scnWidth)<=OLED_WIDTH) ) &&
( (0<=scnY) && (scnY<OLED_HEIGHT) ) && ( (0<scnHeight) || ((scnY+scnHeight)<=OLED_HEIGHT) ) )
{
xStart = scnX;
xEnd = scnX+scnWidth;
yStart = scnY;
yEnd = scnY+scnHeight;
} else {
return;
}

imgWidth = inBitmap.getWidth();
imgHeight = inBitmap.getHeight();

int imgOffsetX = 0;
int imgOffsetY = 0;
if ( ( (0<=imgX) && (imgX<imgWidth) ) && ((imgX+scnWidth)<=imgWidth) &&
( (0<=imgY) && (imgY<imgHeight) ) && ((imgY+scnHeight)<=imgHeight) )
{
imgOffsetX = imgX;
imgOffsetY = imgY;
} else {
return ;
}

for (int width=xStart; width<xEnd; width++) {
for (int height=yStart; height<yEnd; height++) {

int iColor = inBitmap.getPixel(imgOffsetX+(width-scnOffsetX), +imgOffsetY+(height-scnOffsetY) );

// int color = (A & 0xff) << 24 | (B & 0xff) << 16 | (G & 0xff) << 8 | (R & 0xff);
//Y = 0.299 x R + 0.587 x G + 0.114 x B
double dY = 0.299*(iColor&0x000000FF) + 0.587*((iColor&0x0000FF00)>>8) + 0.114*((iColor&0x00FF0000)>>16);
int Y = (int)(dY+0.5);
if (Y<0) { Y = 0 ;}
if (Y>255) { Y = 255 ;}

if ( Y < threshold ) {
screen.setPixel(width, height, black);
} else {
screen.setPixel(width, height, white);
}
}
}

}

public void pixel(int x, int y) {
pixel(x, y, white, false);
}
public void pixel(int x, int y, int color, boolean xor) {
if ( ( (0<=x) && (x<OLED_WIDTH) ) &&
( (0<=y) && (y<OLED_HEIGHT) ) )
{
if (xor) {
if ( color == white ) {
if ( screen.getPixel(x, y) == white ) {
screen.setPixel(x, y, black);
} else {
screen.setPixel(x, y, white);
}
}
} else {
screen.setPixel(x, y, color);
}
}
}

public void line(int x0, int y0, int x1, int y1) {
line(x0, y0, x1, y1, white, false);
}
public void line(int x0, int y0, int x1, int y1, int color, boolean xor) {
int tmp ;
int dx;
int dy;

boolean steep = (Math.abs(y1 - y0) > Math.abs(x1 - x0) );
if ( steep ) {
//swap x0,y0
tmp = x0;
x0 = y0;
y0 = tmp;

//swap x1,y1
tmp = x1;
x1 = y1;
y1 = tmp;
}

if (x0>x1) {
//swap x0,x1
tmp = x0;
x0 = x1;
x1 = tmp;

//swap y0,y1
tmp = y0;
y0 = y1;
y1 = tmp;
}
dx = x1 - x0;
dy = Math.abs(y1 - y0);

int err = dx/2;
int ystep ;
if (y0 < y1) {
ystep = 1;
} else {
ystep = -1;
}

int y = y0;
for (int x=x0; x<x1; x++) {
if (steep) {
pixel(y, x, color, xor);
} else {
pixel(x, y, color, xor);
}
err -= dy;
if (err < 0) {
y += ystep;
err += dx;
}
}
}
public void lineH(int x, int y, int width) {
line(x, y,(x+width), y);
}
public void lineH(int x, int y, int width, int color, boolean xor) {
line(x, y,(x+width), y, color, xor);
}
public void lineV(int x, int y, int height) {
line(x, y, x,(y+height));
}
public void lineV(int x, int y, int height, int color, boolean xor) {
line(x, y, x,(y+height), color, xor);
}

public void rect(int x, int y, int width, int height) {
rect(x, y, width, height, white, false);
}
public void rect(int x, int y, int width, int height, int color, boolean xor) {
lineH(x,y, width, color, xor);
lineH(x,y+height-1, width, color, xor);

int tempHeight = height-2;
if ( tempHeight >= 1 ) {
lineV(x,y+1, tempHeight, color, xor);
lineV(x+width-1, y+1, tempHeight, color, xor);
}
}
public void rectFill(int x, int y, int width, int height) {
rectFill(x, y, width, height, white, false);
}
public void rectFill(int x, int y, int width, int height, int color, boolean xor) {
for (int i=y; i<y+height; i++) {
lineH(x, i, width, color, xor);
}
}

public void circle(int x0, int y0, int radius) {
circle(x0, y0, radius, white, false);
}
public void circle(int x0, int y0, int radius, int color, boolean xor) {
int f = 1 - radius;
int ddF_x = 1;
int ddF_y = -2 * radius;
int x = 0;
int y = radius;

pixel(x0, y0+radius, color, xor);
pixel(x0, y0-radius, color, xor);
pixel(x0+radius, y0, color, xor);
pixel(x0-radius, y0, color, xor);

while ( x < y ) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;

pixel(x0 + x, y0 + y, color, xor);
pixel(x0 - x, y0 + y, color, xor);
pixel(x0 + x, y0 - y, color, xor);
pixel(x0 - x, y0 - y, color, xor);

pixel(x0 + y, y0 + x, color, xor);
pixel(x0 - y, y0 + x, color, xor);
pixel(x0 + y, y0 - x, color, xor);
pixel(x0 - y, y0 - x, color, xor);
}
}
public void circleFill(int x0, int y0, int radius) {
circleFill(x0, y0, radius, white, false);
}
public void circleFill(int x0, int y0, int radius, int color, boolean xor) {
int f = 1 - radius;
int ddF_x = 1;
int ddF_y = -2 * radius;
int x = 0;
int y = radius;

//重複して描画する箇所が多重反転している問題を
//シンプルな方法で解決できていないので
//一時的にxorモードをオフにしています
if (xor) { return; }

for (int i=y0-radius; i<=y0+radius; i++) {
pixel(x0, i, color, xor);
}

while (x<y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;

for (int i=y0-y; i<=y0+y; i++) {
pixel(x0+x, i, color, xor);
pixel(x0-x, i, color, xor);
}
for (int i=y0-x; i<=y0+x; i++) {
pixel(x0+y, i, color, xor);
pixel(x0-y, i, color, xor);
}
}
}

public static final int FONT_WIDTH = 6;
public static final int FONT_HEIGHT = 8;
private static final short ASCII[][] = {
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x3E, 0x5B, 0x4F, 0x5B, 0x3E},
{0x3E, 0x6B, 0x4F, 0x6B, 0x3E},
{0x1C, 0x3E, 0x7C, 0x3E, 0x1C},
{0x18, 0x3C, 0x7E, 0x3C, 0x18},
{0x1C, 0x57, 0x7D, 0x57, 0x1C},
{0x1C, 0x5E, 0x7F, 0x5E, 0x1C},
{0x00, 0x18, 0x3C, 0x18, 0x00},
{0xFF, 0xE7, 0xC3, 0xE7, 0xFF},
{0x00, 0x18, 0x24, 0x18, 0x00},
{0xFF, 0xE7, 0xDB, 0xE7, 0xFF},
{0x30, 0x48, 0x3A, 0x06, 0x0E},
{0x26, 0x29, 0x79, 0x29, 0x26},
{0x40, 0x7F, 0x05, 0x05, 0x07},
{0x40, 0x7F, 0x05, 0x25, 0x3F},
{0x5A, 0x3C, 0xE7, 0x3C, 0x5A},
{0x7F, 0x3E, 0x1C, 0x1C, 0x08},
{0x08, 0x1C, 0x1C, 0x3E, 0x7F},
{0x14, 0x22, 0x7F, 0x22, 0x14},
{0x5F, 0x5F, 0x00, 0x5F, 0x5F},
{0x06, 0x09, 0x7F, 0x01, 0x7F},
{0x00, 0x66, 0x89, 0x95, 0x6A},
{0x60, 0x60, 0x60, 0x60, 0x60},
{0x94, 0xA2, 0xFF, 0xA2, 0x94},
{0x08, 0x04, 0x7E, 0x04, 0x08},
{0x10, 0x20, 0x7E, 0x20, 0x10},
{0x08, 0x08, 0x2A, 0x1C, 0x08},
{0x08, 0x1C, 0x2A, 0x08, 0x08},
{0x1E, 0x10, 0x10, 0x10, 0x10},
{0x0C, 0x1E, 0x0C, 0x1E, 0x0C},
{0x30, 0x38, 0x3E, 0x38, 0x30},
{0x06, 0x0E, 0x3E, 0x0E, 0x06},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x5F, 0x00, 0x00},
{0x00, 0x07, 0x00, 0x07, 0x00},
{0x14, 0x7F, 0x14, 0x7F, 0x14},
{0x24, 0x2A, 0x7F, 0x2A, 0x12},
{0x23, 0x13, 0x08, 0x64, 0x62},
{0x36, 0x49, 0x56, 0x20, 0x50},
{0x00, 0x08, 0x07, 0x03, 0x00},
{0x00, 0x1C, 0x22, 0x41, 0x00},
{0x00, 0x41, 0x22, 0x1C, 0x00},
{0x2A, 0x1C, 0x7F, 0x1C, 0x2A},
{0x08, 0x08, 0x3E, 0x08, 0x08},
{0x00, 0x80, 0x70, 0x30, 0x00},
{0x08, 0x08, 0x08, 0x08, 0x08},
{0x00, 0x00, 0x60, 0x60, 0x00},
{0x20, 0x10, 0x08, 0x04, 0x02},
{0x3E, 0x51, 0x49, 0x45, 0x3E},
{0x00, 0x42, 0x7F, 0x40, 0x00},
{0x72, 0x49, 0x49, 0x49, 0x46},
{0x21, 0x41, 0x49, 0x4D, 0x33},
{0x18, 0x14, 0x12, 0x7F, 0x10},
{0x27, 0x45, 0x45, 0x45, 0x39},
{0x3C, 0x4A, 0x49, 0x49, 0x31},
{0x41, 0x21, 0x11, 0x09, 0x07},
{0x36, 0x49, 0x49, 0x49, 0x36},
{0x46, 0x49, 0x49, 0x29, 0x1E},
{0x00, 0x00, 0x14, 0x00, 0x00},
{0x00, 0x40, 0x34, 0x00, 0x00},
{0x00, 0x08, 0x14, 0x22, 0x41},
{0x14, 0x14, 0x14, 0x14, 0x14},
{0x00, 0x41, 0x22, 0x14, 0x08},
{0x02, 0x01, 0x59, 0x09, 0x06},
{0x3E, 0x41, 0x5D, 0x59, 0x4E},
{0x7C, 0x12, 0x11, 0x12, 0x7C},
{0x7F, 0x49, 0x49, 0x49, 0x36},
{0x3E, 0x41, 0x41, 0x41, 0x22},
{0x7F, 0x41, 0x41, 0x41, 0x3E},
{0x7F, 0x49, 0x49, 0x49, 0x41},
{0x7F, 0x09, 0x09, 0x09, 0x01},
{0x3E, 0x41, 0x41, 0x51, 0x73},
{0x7F, 0x08, 0x08, 0x08, 0x7F},
{0x00, 0x41, 0x7F, 0x41, 0x00},
{0x20, 0x40, 0x41, 0x3F, 0x01},
{0x7F, 0x08, 0x14, 0x22, 0x41},
{0x7F, 0x40, 0x40, 0x40, 0x40},
{0x7F, 0x02, 0x1C, 0x02, 0x7F},
{0x7F, 0x04, 0x08, 0x10, 0x7F},
{0x3E, 0x41, 0x41, 0x41, 0x3E},
{0x7F, 0x09, 0x09, 0x09, 0x06},
{0x3E, 0x41, 0x51, 0x21, 0x5E},
{0x7F, 0x09, 0x19, 0x29, 0x46},
{0x26, 0x49, 0x49, 0x49, 0x32},
{0x03, 0x01, 0x7F, 0x01, 0x03},
{0x3F, 0x40, 0x40, 0x40, 0x3F},
{0x1F, 0x20, 0x40, 0x20, 0x1F},
{0x3F, 0x40, 0x38, 0x40, 0x3F},
{0x63, 0x14, 0x08, 0x14, 0x63},
{0x03, 0x04, 0x78, 0x04, 0x03},
{0x61, 0x59, 0x49, 0x4D, 0x43},
{0x00, 0x7F, 0x41, 0x41, 0x41},
{0x02, 0x04, 0x08, 0x10, 0x20},
{0x00, 0x41, 0x41, 0x41, 0x7F},
{0x04, 0x02, 0x01, 0x02, 0x04},
{0x40, 0x40, 0x40, 0x40, 0x40},
{0x00, 0x03, 0x07, 0x08, 0x00},
{0x20, 0x54, 0x54, 0x78, 0x40},
{0x7F, 0x28, 0x44, 0x44, 0x38},
{0x38, 0x44, 0x44, 0x44, 0x28},
{0x38, 0x44, 0x44, 0x28, 0x7F},
{0x38, 0x54, 0x54, 0x54, 0x18},
{0x00, 0x08, 0x7E, 0x09, 0x02},
{0x18, 0xA4, 0xA4, 0x9C, 0x78},
{0x7F, 0x08, 0x04, 0x04, 0x78},
{0x00, 0x44, 0x7D, 0x40, 0x00},
{0x20, 0x40, 0x40, 0x3D, 0x00},
{0x7F, 0x10, 0x28, 0x44, 0x00},
{0x00, 0x41, 0x7F, 0x40, 0x00},
{0x7C, 0x04, 0x78, 0x04, 0x78},
{0x7C, 0x08, 0x04, 0x04, 0x78},
{0x38, 0x44, 0x44, 0x44, 0x38},
{0xFC, 0x18, 0x24, 0x24, 0x18},
{0x18, 0x24, 0x24, 0x18, 0xFC},
{0x7C, 0x08, 0x04, 0x04, 0x08},
{0x48, 0x54, 0x54, 0x54, 0x24},
{0x04, 0x04, 0x3F, 0x44, 0x24},
{0x3C, 0x40, 0x40, 0x20, 0x7C},
{0x1C, 0x20, 0x40, 0x20, 0x1C},
{0x3C, 0x40, 0x30, 0x40, 0x3C},
{0x44, 0x28, 0x10, 0x28, 0x44},
{0x4C, 0x90, 0x90, 0x90, 0x7C},
{0x44, 0x64, 0x54, 0x4C, 0x44},
{0x00, 0x08, 0x36, 0x41, 0x00},
{0x00, 0x00, 0x77, 0x00, 0x00},
{0x00, 0x41, 0x36, 0x08, 0x00},
{0x02, 0x01, 0x02, 0x04, 0x02},
{0x3C, 0x26, 0x23, 0x26, 0x3C},
{0x1E, 0xA1, 0xA1, 0x61, 0x12},
{0x3A, 0x40, 0x40, 0x20, 0x7A},
{0x38, 0x54, 0x54, 0x55, 0x59},
{0x21, 0x55, 0x55, 0x79, 0x41},
{0x21, 0x54, 0x54, 0x78, 0x41},
{0x21, 0x55, 0x54, 0x78, 0x40},
{0x20, 0x54, 0x55, 0x79, 0x40},
{0x0C, 0x1E, 0x52, 0x72, 0x12},
{0x39, 0x55, 0x55, 0x55, 0x59},
{0x39, 0x54, 0x54, 0x54, 0x59},
{0x39, 0x55, 0x54, 0x54, 0x58},
{0x00, 0x00, 0x45, 0x7C, 0x41},
{0x00, 0x02, 0x45, 0x7D, 0x42},
{0x00, 0x01, 0x45, 0x7C, 0x40},
{0xF0, 0x29, 0x24, 0x29, 0xF0},
{0xF0, 0x28, 0x25, 0x28, 0xF0},
{0x7C, 0x54, 0x55, 0x45, 0x00},
{0x20, 0x54, 0x54, 0x7C, 0x54},
{0x7C, 0x0A, 0x09, 0x7F, 0x49},
{0x32, 0x49, 0x49, 0x49, 0x32},
{0x32, 0x48, 0x48, 0x48, 0x32},
{0x32, 0x4A, 0x48, 0x48, 0x30},
{0x3A, 0x41, 0x41, 0x21, 0x7A},
{0x3A, 0x42, 0x40, 0x20, 0x78},
{0x00, 0x9D, 0xA0, 0xA0, 0x7D},
{0x39, 0x44, 0x44, 0x44, 0x39},
{0x3D, 0x40, 0x40, 0x40, 0x3D},
{0x3C, 0x24, 0xFF, 0x24, 0x24},
{0x48, 0x7E, 0x49, 0x43, 0x66},
{0x2B, 0x2F, 0xFC, 0x2F, 0x2B},
{0xFF, 0x09, 0x29, 0xF6, 0x20},
{0xC0, 0x88, 0x7E, 0x09, 0x03},
{0x20, 0x54, 0x54, 0x79, 0x41},
{0x00, 0x00, 0x44, 0x7D, 0x41},
{0x30, 0x48, 0x48, 0x4A, 0x32},
{0x38, 0x40, 0x40, 0x22, 0x7A},
{0x00, 0x7A, 0x0A, 0x0A, 0x72},
{0x7D, 0x0D, 0x19, 0x31, 0x7D},
{0x26, 0x29, 0x29, 0x2F, 0x28},
{0x26, 0x29, 0x29, 0x29, 0x26},
{0x30, 0x48, 0x4D, 0x40, 0x20},
{0x38, 0x08, 0x08, 0x08, 0x08},
{0x08, 0x08, 0x08, 0x08, 0x38},
{0x2F, 0x10, 0xC8, 0xAC, 0xBA},
{0x2F, 0x10, 0x28, 0x34, 0xFA},
{0x00, 0x00, 0x7B, 0x00, 0x00},
{0x08, 0x14, 0x2A, 0x14, 0x22},
{0x22, 0x14, 0x2A, 0x14, 0x08},
{0xAA, 0x00, 0x55, 0x00, 0xAA},
{0xAA, 0x55, 0xAA, 0x55, 0xAA},
{0x00, 0x00, 0x00, 0xFF, 0x00},
{0x10, 0x10, 0x10, 0xFF, 0x00},
{0x14, 0x14, 0x14, 0xFF, 0x00},
{0x10, 0x10, 0xFF, 0x00, 0xFF},
{0x10, 0x10, 0xF0, 0x10, 0xF0},
{0x14, 0x14, 0x14, 0xFC, 0x00},
{0x14, 0x14, 0xF7, 0x00, 0xFF},
{0x00, 0x00, 0xFF, 0x00, 0xFF},
{0x14, 0x14, 0xF4, 0x04, 0xFC},
{0x14, 0x14, 0x17, 0x10, 0x1F},
{0x10, 0x10, 0x1F, 0x10, 0x1F},
{0x14, 0x14, 0x14, 0x1F, 0x00},
{0x10, 0x10, 0x10, 0xF0, 0x00},
{0x00, 0x00, 0x00, 0x1F, 0x10},
{0x10, 0x10, 0x10, 0x1F, 0x10},
{0x10, 0x10, 0x10, 0xF0, 0x10},
{0x00, 0x00, 0x00, 0xFF, 0x10},
{0x10, 0x10, 0x10, 0x10, 0x10},
{0x10, 0x10, 0x10, 0xFF, 0x10},
{0x00, 0x00, 0x00, 0xFF, 0x14},
{0x00, 0x00, 0xFF, 0x00, 0xFF},
{0x00, 0x00, 0x1F, 0x10, 0x17},
{0x00, 0x00, 0xFC, 0x04, 0xF4},
{0x14, 0x14, 0x17, 0x10, 0x17},
{0x14, 0x14, 0xF4, 0x04, 0xF4},
{0x00, 0x00, 0xFF, 0x00, 0xF7},
{0x14, 0x14, 0x14, 0x14, 0x14},
{0x14, 0x14, 0xF7, 0x00, 0xF7},
{0x14, 0x14, 0x14, 0x17, 0x14},
{0x10, 0x10, 0x1F, 0x10, 0x1F},
{0x14, 0x14, 0x14, 0xF4, 0x14},
{0x10, 0x10, 0xF0, 0x10, 0xF0},
{0x00, 0x00, 0x1F, 0x10, 0x1F},
{0x00, 0x00, 0x00, 0x1F, 0x14},
{0x00, 0x00, 0x00, 0xFC, 0x14},
{0x00, 0x00, 0xF0, 0x10, 0xF0},
{0x10, 0x10, 0xFF, 0x10, 0xFF},
{0x14, 0x14, 0x14, 0xFF, 0x14},
{0x10, 0x10, 0x10, 0x1F, 0x00},
{0x00, 0x00, 0x00, 0xF0, 0x10},
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
{0xF0, 0xF0, 0xF0, 0xF0, 0xF0},
{0xFF, 0xFF, 0xFF, 0x00, 0x00},
{0x00, 0x00, 0x00, 0xFF, 0xFF},
{0x0F, 0x0F, 0x0F, 0x0F, 0x0F},
{0x38, 0x44, 0x44, 0x38, 0x44},
{0x7C, 0x2A, 0x2A, 0x3E, 0x14},
{0x7E, 0x02, 0x02, 0x06, 0x06},
{0x02, 0x7E, 0x02, 0x7E, 0x02},
{0x63, 0x55, 0x49, 0x41, 0x63},
{0x38, 0x44, 0x44, 0x3C, 0x04},
{0x40, 0x7E, 0x20, 0x1E, 0x20},
{0x06, 0x02, 0x7E, 0x02, 0x02},
{0x99, 0xA5, 0xE7, 0xA5, 0x99},
{0x1C, 0x2A, 0x49, 0x2A, 0x1C},
{0x4C, 0x72, 0x01, 0x72, 0x4C},
{0x30, 0x4A, 0x4D, 0x4D, 0x30},
{0x30, 0x48, 0x78, 0x48, 0x30},
{0xBC, 0x62, 0x5A, 0x46, 0x3D},
{0x3E, 0x49, 0x49, 0x49, 0x00},
{0x7E, 0x01, 0x01, 0x01, 0x7E},
{0x2A, 0x2A, 0x2A, 0x2A, 0x2A},
{0x44, 0x44, 0x5F, 0x44, 0x44},
{0x40, 0x51, 0x4A, 0x44, 0x40},
{0x40, 0x44, 0x4A, 0x51, 0x40},
{0x00, 0x00, 0xFF, 0x01, 0x03},
{0xE0, 0x80, 0xFF, 0x00, 0x00},
{0x08, 0x08, 0x6B, 0x6B, 0x08},
{0x36, 0x12, 0x36, 0x24, 0x36},
{0x06, 0x0F, 0x09, 0x0F, 0x06},
{0x00, 0x00, 0x18, 0x18, 0x00},
{0x00, 0x00, 0x10, 0x10, 0x00},
{0x30, 0x40, 0xFF, 0x01, 0x01},
{0x00, 0x1F, 0x01, 0x01, 0x1E},
{0x00, 0x19, 0x1D, 0x17, 0x12},
{0x00, 0x3C, 0x3C, 0x3C, 0x3C},
{0x00, 0x00, 0x00, 0x00, 0x00}
};

public void setChar(int x, int y, char c) {
setChar(x, y, c, white, false);
}
public void setChar(int x, int y, char c, int color, boolean xor) {
int asciiOffset = 0x00;
int bitMask = 0x80;
int charPos = c - asciiOffset;

if ( (x+(FONT_WIDTH-1)) > OLED_WIDTH ) { return; }
if ( (y+FONT_HEIGHT) > OLED_HEIGHT ) { return; }

if ( 0x00<=c && c<=0xFE ) {
for (int x0 = 0; x0<(FONT_WIDTH-1); x0++) {
int asciiChar = ASCII[charPos][x0];
for (int y0=0; y0<FONT_HEIGHT; y0++) {
int bit = (asciiChar<<y0) & bitMask ;
if (bit==bitMask) {
pixel((x+x0), (y+FONT_HEIGHT-y0), color, xor);
}
}
}
}
}

public void setString(int x, int y, String str) {
setString(x, y, str, white, false);
}
public void setString(int x, int y, String str, int color, boolean xor) {
int posX = x;
int posY = y;

if ( (x+FONT_WIDTH) > OLED_WIDTH ) { return; }
if ( (y+FONT_HEIGHT) > OLED_HEIGHT ) { return; }

for (int pos=0; pos<str.length(); pos++) {
char c = str.charAt(pos);
setChar( posX, posY, c, color, xor);
posX += FONT_WIDTH ;
if (posX>=OLED_WIDTH) {
break;
}
}
}

}




上記ライブラリの使い方

1.「oled」フォルダを作成しフォルダ内にOled.javaを配置

フォルダ作成2.png

2.MainActivity.javaにOLEDライブラリをインポート


MainActivity.java

import com.theta360.pluginapplication.oled.Oled;


3.画像ファイルを扱う場合「assetフォルダ」を作成し画像を配置しておく

assetフォルダーは、Android Studioのメニューから

[File] -> [New] -> [Folder] -> [Assets Folder]

で作成できます。

4.あとはソースコードをサンプルに習って書くだけ


ライブラリの呼び出し例

以下に3通りのライブラリ呼び出し事例を「動作の説明(アニメーションGIF等の画像含む)」「ソースコード」の順で掲載します。

最初の2例はソースコード全文を記載して細かな説明を割愛します。

「こんな表示は、どうライブラリを呼び出しているのだろう?」

という具合に、振る舞いをみて呼び出し箇所を確認するような使い方をしてください。

どの事例についてもこちらのサンプルプロジェクトをベースにしています。


デモ描画(図形や画像と文字の混在ナド)

・スクリーンセーバー的なもの

このサンプルプラグインを起動するとまず表示されるデモンストレーションです。

実際はもっと滑らかな動きですが、アニメGIFにする時にコマ落ちしています。

本記事先頭にあるツイートの動画をご覧ください。

GIF_01_trim.gif

・簡易画像ビューワー

上記状態から無線LANボタンかModeボタンの操作をすると簡易画像ビューワーの状態になります。撮影をすると上記状態に戻れます。

表示しているカラー画像は以下です。192 x 170 pixelのjpegです。

kuma7_192.jpg

実際はもっと滑らかな動きですが、アニメGIFにする時にコマ落ちしています。

本記事先頭にあるツイートの動画をご覧ください。

GIF_02.smallgif.gif


ソースコード

ソースコード全文を記載しておきます。

末尾の「スクリーンセーバー的なもの」に該当するdemoDraw()は見なくてもよいと思います。デモを派手にしたかっただけなモリモリコードなので・・・

サンプルソースコード全文


MainActivity.java

/**

* Copyright 2018 Ricoh Company, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.theta360.pluginapplication;

import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import com.theta360.pluginapplication.task.TakePictureTask;
import com.theta360.pluginapplication.task.TakePictureTask.Callback;
import com.theta360.pluginlibrary.activity.PluginActivity;
import com.theta360.pluginlibrary.callback.KeyCallback;
import com.theta360.pluginlibrary.receiver.KeyReceiver;
import com.theta360.pluginlibrary.values.LedColor;
import com.theta360.pluginlibrary.values.LedTarget;

import com.theta360.pluginapplication.oled.Oled;

public class MainActivity extends PluginActivity {
private TakePictureTask.Callback mTakePictureTaskCallback = new Callback() {
@Override
public void onTakePicture(String fileUrl) {

}
};

//Z1固有Fnボタンのキーコード定義
private static final int KEYCODE_FUNCTION = 119;

//onKeyUp()での長押し操作認識用
boolean longPressWLAN = false;
boolean longPressFUNC = false;

Oled oledDisplay = null; //OLED描画クラス
boolean oledInvert = false; //OLED白黒反転状態

//OLED表示の操作モード関連
private static final int OLED_MODE_MOVE_H = 0;
private static final int OLED_MODE_MOVE_V = 1;
private static final int OLED_MODE_CANGE_THRESH = 2;
int oledMode = OLED_MODE_MOVE_H;
int oledModeTmp = oledMode;

int dispX = 0; //写真表示領域 開始位置 x
int dispY = 0; //写真表示領域 開始位置 y
int dispWidth = 92; //写真表示領域 幅
int dispHeight = 24; //写真表示領域 高さ

String srcFileName = "kuma7_192.jpg";
int bitmapThresh = 92; //表示画像の輝度閾値
int srcX = 48; //表示画像の開始点 x
int srcY = 36; //表示画像の開始点 y

int textStartX = dispWidth + 1; //文字領域 横方向 開始位置 x
int textLine1 = 0; //文字領域 1行目 高さ
int textLine2 = 8; //文字領域 2行目 高さ
int textLine3 = 16; //文字領域 3行目 高さ

//OLED表示スレッド終了用
private boolean mFinished;
private boolean demoDisplay;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Set enable to close by pluginlibrary, If you set false, please call close() after finishing your end processing.
setAutoClose(true);

//OLED描画クラスをnewする
oledDisplay = new Oled(getApplicationContext());

oledDisplay.brightness(100); //輝度設定
oledDisplay.clear(oledDisplay.black); //表示領域クリア設定
oledDisplay.draw(); //表示領域クリア結果を反映

// Set a callback when a button operation event is acquired.
setKeyCallback(new KeyCallback() {
@Override
public void onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyReceiver.KEYCODE_CAMERA) {
/*
* To take a static picture, use the takePicture method.
* You can receive a fileUrl of the static picture in the callback.
*/

new TakePictureTask(mTakePictureTaskCallback).execute();

}
}

@Override
public void onKeyUp(int keyCode, KeyEvent event) {
/**
* You can control the LED of the camera.
* It is possible to change the way of lighting, the cycle of blinking, the color of light emission.
* Light emitting color can be changed only LED3.
*/

notificationLedBlink(LedTarget.LED3, LedColor.BLUE, 1000);

if (keyCode == KeyReceiver.KEYCODE_CAMERA) {
//撮影するとデモモード
demoDisplay = true;
} else if (keyCode != KEYCODE_FUNCTION) {
//Fnボタン操作以外でデモモード解除
demoDisplay = false;
}

if (keyCode == KEYCODE_FUNCTION) {
if (longPressFUNC) {
longPressFUNC = false;
} else {
if (oledInvert) {
oledInvert = false;
} else {
oledInvert = true;
}
}
} else if (keyCode == KeyReceiver.KEYCODE_MEDIA_RECORD) {
if (oledMode == OLED_MODE_MOVE_V) {
srcY += 4;
if (srcY > (oledDisplay.imgHeight - dispHeight)) {
srcY = oledDisplay.imgHeight - dispHeight;
}
} else if (oledMode == OLED_MODE_MOVE_H) {
srcX += 4;
if (srcX > (oledDisplay.imgWidth - dispWidth)) {
srcX = oledDisplay.imgWidth - dispWidth;
}
} else if (oledMode == OLED_MODE_CANGE_THRESH) {
bitmapThresh += 8;
if (bitmapThresh > 256) {
bitmapThresh = 256;
}
}

} else if (keyCode == KeyReceiver.KEYCODE_WLAN_ON_OFF) {
if (longPressWLAN) {
longPressWLAN = false;
} else {
if (oledMode == OLED_MODE_MOVE_V) {
srcY -= 4;
if (srcY < 0) {
srcY = 0;
}
} else if (oledMode == OLED_MODE_MOVE_H) {
srcX -= 4;
if (srcX < 0) {
srcX = 0;
}
} else if (oledMode == OLED_MODE_CANGE_THRESH) {
bitmapThresh -= 8;
if (bitmapThresh < 0) {
bitmapThresh = 0;
}
}
}
}

}

@Override
public void onKeyLongPress(int keyCode, KeyEvent event) {
if (keyCode == KEYCODE_FUNCTION) {
longPressFUNC = true;

if (oledMode == OLED_MODE_MOVE_H) {
oledMode = OLED_MODE_MOVE_V;
} else {
oledMode = OLED_MODE_MOVE_H;
}
} else if (keyCode == KeyReceiver.KEYCODE_WLAN_ON_OFF) {
longPressWLAN = true;

if (oledMode == OLED_MODE_CANGE_THRESH) {
oledMode = oledModeTmp;
} else {
oledModeTmp = oledMode;
oledMode = OLED_MODE_CANGE_THRESH;
}
}
}
});
}

@Override
protected void onResume() {
super.onResume();

if (isApConnected()) {

}

//デモ表示開始設定
demoDisplay = true;

//スレッド開始
mFinished = false;
drawOledThread();
}

@Override
protected void onPause() {
// Do end processing
//close();

//スレッドを終わらせる指示。終了待ちしていません。
mFinished = true;

super.onPause();
}

//OLE描画スレッド
public void drawOledThread() {
new Thread(new Runnable() {
@Override
public void run() {

//描画ループ
while (mFinished == false) {

try {
if (demoDisplay) {
demoDraw();
} else {
drawImageControl();
}

//描画が高頻度になりすぎないよう5msスリープする
Thread.sleep(5);

} catch (InterruptedException e) {
// Deal with error.
e.printStackTrace();
} finally {
}
}
}
}).start();
}

private void drawImageControl() {
//写真表示領域 描画
oledDisplay.setBitmap(dispX, dispY, dispWidth, dispHeight, srcX, srcY, bitmapThresh, srcFileName);

//文字表示領域 クリア
oledDisplay.rectFill(textStartX, textLine1, Oled.OLED_WIDTH - dispWidth, dispHeight, oledDisplay.black, false);

//操作可能なパラメータの左に '*'を描画
if (oledMode == OLED_MODE_MOVE_H) {
oledDisplay.setString(textStartX, textLine1, "*");
} else if (oledMode == OLED_MODE_MOVE_V) {
oledDisplay.setString(textStartX, textLine2, "*");
} else if (oledMode == OLED_MODE_CANGE_THRESH) {
oledDisplay.setString(textStartX, textLine3, "*");
}

//各パラメータを描画
int dispNumX = textStartX + Oled.FONT_WIDTH;
oledDisplay.setString(dispNumX, textLine1, "X=" + Integer.toString(srcX));
oledDisplay.setString(dispNumX, textLine2, "Y=" + Integer.toString(srcY));
oledDisplay.setString(dispNumX, textLine3, "T=" + Integer.toString(bitmapThresh));

//OLEDへ出力指示(oledInvert==false は draw()と同じ)
oledDisplay.invert(oledInvert);
}

//============================================
// 以降、デモンストレーション描画 (盛りすぎです)
//============================================
boolean lineRollDir = true;
int demoMode = 0;
int demoModeBack = 0;
int demoX0=0;
int demoY0=0;
int demoX1=Oled.OLED_WIDTH-1;
int demoY1=0;
int demoX2=Oled.OLED_WIDTH-1;
int demoY2=Oled.OLED_HEIGHT-1;
int demoX3=0;
int demoY3=Oled.OLED_HEIGHT-1;

int demoThresh = 0;
int demoThreshStep = 2;
int demoImgX = 0;
int demoImgY = 0;
int demoStepX = 1;
int demoStepY = 1;

String demoText = "Demo";
int demoTextX = 52;
int demoTextY = 8;
int demoTextStepX = 1;
int demoTextStepY = -1;

private void demoDraw() {
oledDisplay.clear();

if (demoMode!=4) {
oledDisplay.circleFill(demoX0,demoY0,11);
oledDisplay.circleFill(demoX2,demoY2,11);
oledDisplay.circle(demoX1,demoY1, 22);
oledDisplay.circle(demoX3,demoY3, 22);

oledDisplay.rect(demoX2-11,demoY0-11, 22,22, oledDisplay.white,true);
oledDisplay.rect(demoX0-11,demoY2-11, 22,22, oledDisplay.white,true);
oledDisplay.rectFill(demoX3-23,demoY1-23, 47,47, oledDisplay.white,true);
oledDisplay.rectFill(demoX1-23,demoY3-23, 47,47, oledDisplay.white,true);

if (lineRollDir) {
oledDisplay.line(demoX0,demoY0,demoX1,demoY1, oledDisplay.white,true);
oledDisplay.line(demoX2,demoY2,demoX3,demoY3, oledDisplay.white,true);
oledDisplay.line(demoX1,demoY1,demoX2,demoY2, oledDisplay.white,true);
oledDisplay.line(demoX3,demoY3,demoX0,demoY0, oledDisplay.white,true);
} else {
oledDisplay.line(demoX2,demoY0,demoX3,demoY1, oledDisplay.white,true);
oledDisplay.line(demoX0,demoY2,demoX1,demoY3, oledDisplay.white,true);
oledDisplay.line(demoX3,demoY1,demoX0,demoY2, oledDisplay.white,true);
oledDisplay.line(demoX1,demoY3,demoX2,demoY0, oledDisplay.white,true);
}
} else {
oledDisplay.setBitmap(dispX, dispY, Oled.OLED_WIDTH, Oled.OLED_HEIGHT, demoImgX, demoImgY, demoThresh, srcFileName);
}

if (demoMode==0) {
if (demoY1<(Oled.OLED_HEIGHT-1)) {
demoY1++;
demoY3=(Oled.OLED_HEIGHT-1)-demoY1;
} else {
if (demoX1>0){
demoX1--;
demoX3=(Oled.OLED_WIDTH-1)-demoX1;
} else {
demoModeBack=1;
demoMode=4;
}
}
} else if (demoMode==1) {
if (demoX0<(Oled.OLED_WIDTH-1)) {
demoX0++;
demoX2=(Oled.OLED_WIDTH-1)-demoX0;
} else {
if ( demoY0<(Oled.OLED_HEIGHT-1) ) {
demoY0++;
demoY2=(Oled.OLED_HEIGHT-1)-demoY0;
} else {
demoModeBack=2;
demoImgX = oledDisplay.imgWidth-Oled.OLED_WIDTH;
demoStepX=-1;
demoMode=4;
}
}
} else if (demoMode==2) {
if (demoY1>0) {
demoY1--;
demoY3=(Oled.OLED_HEIGHT-1)-demoY1;
} else {
if (demoX1<(Oled.OLED_WIDTH-1)) {
demoX1++;
demoX3=(Oled.OLED_WIDTH-1)-demoX1;
} else {
demoModeBack=3;
demoImgY = oledDisplay.imgHeight-Oled.OLED_HEIGHT;
demoStepY = -1;
demoMode=4;
}
}
} else if (demoMode==3) {
if ( demoX0>0 ) {
demoX0--;
demoX2=(Oled.OLED_WIDTH-1)-demoX0;
} else {
if ( demoY0>0 ) {
demoY0--;
demoY2=(Oled.OLED_HEIGHT-1)-demoY0;
} else {
if (lineRollDir) {
lineRollDir = false;
} else {
lineRollDir = true;
}
demoModeBack=0;
demoImgX = oledDisplay.imgWidth-Oled.OLED_WIDTH;
demoStepX=-1;
demoImgY = oledDisplay.imgHeight-Oled.OLED_HEIGHT;
demoStepY = -1;
demoMode=4;
}
}
} else if (demoMode==4) {

demoThresh+=demoThreshStep;
if (demoThresh>256) {
demoThresh=256;
demoThreshStep*=-1;
demoImgX = 0;
demoImgY = 0;
demoMode = demoModeBack;
} else if (demoThresh<0) {
demoThresh=0;
demoThreshStep*=-1;
demoImgX = 0;
demoImgY = 0;
demoMode = demoModeBack;
}
demoImgX+=demoStepX;
if (demoImgX>(oledDisplay.imgWidth-Oled.OLED_WIDTH)) {
demoImgX = oledDisplay.imgWidth-Oled.OLED_WIDTH;
demoStepX=-1;
} else if ( demoImgX < 0 ) {
demoImgX = 0;
demoStepX = 1;
}
demoImgY+=demoStepY;
if (demoImgY>(oledDisplay.imgHeight-Oled.OLED_HEIGHT)) {
demoImgY = oledDisplay.imgHeight-Oled.OLED_HEIGHT;
demoStepY = -1;
} else if ( demoImgY < 0 ) {
demoImgY = 0;
demoStepY = 1;
}

}

oledDisplay.setString(demoTextX, demoTextY, demoText, oledDisplay.white,true);
demoTextX += demoTextStepX ;
if ( demoTextX > (Oled.OLED_WIDTH - (demoText.length()*Oled.FONT_WIDTH) ) ) {
demoTextX = Oled.OLED_WIDTH - (demoText.length()*Oled.FONT_WIDTH) ;
demoTextStepX *= -1;
} else if ( demoTextX < 0 ) {
demoTextX = 0;
demoTextStepX *= -1;
}
demoTextY += demoTextStepY ;
if ( demoTextY > (Oled.OLED_HEIGHT - Oled.FONT_HEIGHT ) ) {
demoTextY = Oled.OLED_HEIGHT - Oled.FONT_HEIGHT ;
demoTextStepY *= -1;
} else if ( demoTextY < 0 ) {
demoTextY = 0;
demoTextStepY *= -1;
}

//OLEDへ出力指示(oledInvert==false は draw()と同じ)
//負荷サンプルです。oledInvert==true のときに、表示が遅くなるのがわかると思います。
oledDisplay.invert(oledInvert);
}

}




ピンポンゲーム

画面左側のパドル(縦棒)を、無線LANボタンで上、Fnボタンで下に動かすことができます。

5点先取で勝ち負け判定が一定時間表示され、再びゲームを繰り返す動作です。

実際はもっと滑らかな動きですが、アニメGIFにする時にコマ落ちしています。

本記事先頭にあるツイートの動画をご覧ください。

GIF_03 small.gif


ソースコード

スレッドとした処理が描画処理のメインループになっています。

その下の「ピンポンゲーム」というコメントがある所以降にゲーム用の定義やコードがまとまっています。

サンプルソースコード全文


MainActivity.java

/**

* Copyright 2018 Ricoh Company, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.theta360.pluginapplication;

import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import com.theta360.pluginapplication.task.TakePictureTask;
import com.theta360.pluginapplication.task.TakePictureTask.Callback;
import com.theta360.pluginlibrary.activity.PluginActivity;
import com.theta360.pluginlibrary.callback.KeyCallback;
import com.theta360.pluginlibrary.receiver.KeyReceiver;
import com.theta360.pluginlibrary.values.LedColor;
import com.theta360.pluginlibrary.values.LedTarget;

import com.theta360.pluginapplication.oled.Oled;

public class MainActivity extends PluginActivity {
private TakePictureTask.Callback mTakePictureTaskCallback = new Callback() {
@Override
public void onTakePicture(String fileUrl) {

}
};

//Z1固有Fnボタンのキーコード定義
private static final int KEYCODE_FUNCTION = 119;

Oled oledDisplay = null; //OLED描画クラス

//OLED表示スレッド終了用
private boolean mFinished;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Set enable to close by pluginlibrary, If you set false, please call close() after finishing your end processing.
setAutoClose(true);

//OLED描画クラスをnewする
oledDisplay = new Oled(getApplicationContext());

oledDisplay.brightness(100); //輝度設定
oledDisplay.clear(oledDisplay.black); //表示領域クリア設定
oledDisplay.draw(); //表示領域クリア結果を反映

// Set a callback when a button operation event is acquired.
setKeyCallback(new KeyCallback() {
@Override
public void onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyReceiver.KEYCODE_CAMERA) {
/*
* To take a static picture, use the takePicture method.
* You can receive a fileUrl of the static picture in the callback.
*/

new TakePictureTask(mTakePictureTaskCallback).execute();
}

if (keyCode == KeyReceiver.KEYCODE_WLAN_ON_OFF) {
btnUpStat=true;
}
if (keyCode == KEYCODE_FUNCTION) {
btnDownStat=true;
}
}

@Override
public void onKeyUp(int keyCode, KeyEvent event) {
/**
* You can control the LED of the camera.
* It is possible to change the way of lighting, the cycle of blinking, the color of light emission.
* Light emitting color can be changed only LED3.
*/

notificationLedBlink(LedTarget.LED3, LedColor.BLUE, 1000);

if (keyCode == KeyReceiver.KEYCODE_WLAN_ON_OFF) {
btnUpStat=false;
}
if (keyCode == KEYCODE_FUNCTION) {
btnDownStat=false;
}
}

@Override
public void onKeyLongPress(int keyCode, KeyEvent event) {
}
});
}

@Override
protected void onResume() {
super.onResume();

if (isApConnected()) {

}

//スレッド開始
mFinished = false;
drawOledThread();
}

@Override
protected void onPause() {
// Do end processing
//close();

//スレッドを終わらせる指示。終了待ちしていません。
mFinished = true;

super.onPause();
}

//OLE描画スレッド
public void drawOledThread() {
new Thread(new Runnable() {
@Override
public void run() {

//描画ループ
while (mFinished == false) {

try {

//ピンポンゲーム
updatePaddlePositions();
moveBall();
drawGame();
if (checkWin()!=0)
{
drawWin(checkWin());
Thread.sleep(5000);
cleanUp();
}

//描画が高頻度になりすぎないよう5msスリープする
Thread.sleep(5);

} catch (InterruptedException e) {
// Deal with error.
e.printStackTrace();
} finally {
}
}
}
}).start();
}

//============================================
// ピンポンゲーム
//============================================
boolean btnUpStat = false;
boolean btnDownStat = false;

int scoreToWin = 5;
int playerScore = 0;
int player2Score = 0;

double paddleWidth = 3;
double paddleHeight = 8;
double halfPaddleWidth = paddleWidth/2.0;
double halfPaddleHeight = paddleHeight/2.0;

double player1PosX = 1.0 + halfPaddleWidth;
double player1PosY = 0.0;
double player2PosX = (Oled.OLED_WIDTH) - 1.0 - halfPaddleWidth;
double player2PosY = 0.0;
double enemyVelY = 0.5;

final double ballRadius = 2.0;
final double ballSpeedX = 1.0;
double ballPosX = (Oled.OLED_WIDTH)/2.0;
double ballPosY = (Oled.OLED_HEIGHT)/2.0;
double ballVelX = -1.0 * ballSpeedX;
double ballVelY = 0;

final int PLAYER_1_WIN = 1;
final int PLAYER_2_WIN = 2;

final int SINGLE_PLAYER = 0;
final int MULTI_PLAYER = 1;
int playMode = SINGLE_PLAYER;

void updatePaddlePositions() {
if (btnUpStat) {
player1PosY--;
}
if (btnDownStat) {
player1PosY++;
}
player1PosY = constrainPosition(player1PosY);

if (player2PosY < ballPosY)
{
player2PosY += enemyVelY;
}
else if(player2PosY > ballPosY)
{
player2PosY -= enemyVelY;
}
player2PosY = constrainPosition(player2PosY);

}

double constrainPosition(double position) {
double newPaddlePosY = position;

if (position - halfPaddleHeight < 0)
{
newPaddlePosY = halfPaddleHeight;
}
else if (position + halfPaddleHeight > (Oled.OLED_HEIGHT) )
{
newPaddlePosY = (Oled.OLED_HEIGHT) - halfPaddleHeight;
}

return newPaddlePosY;
}

void moveBall(){
ballPosY += ballVelY;
ballPosX += ballVelX;

if (ballPosY < ballRadius)
{
ballPosY = ballRadius;
ballVelY *= -1.0;
}
else if (ballPosY > (Oled.OLED_HEIGHT) - ballRadius)
{
ballPosY = (Oled.OLED_HEIGHT) - ballRadius;
ballVelY *= -1.0;
}

if (ballPosX < ballRadius)
{
ballPosX = ballRadius;
ballVelX = ballSpeedX;
player2Score++;
}
else if (ballPosX > (Oled.OLED_WIDTH) - ballRadius)
{
ballPosX = (Oled.OLED_WIDTH) - ballRadius;
ballVelX *= -1.0 * ballSpeedX;
playerScore++;
}

if (ballPosX < player1PosX + ballRadius + halfPaddleWidth)
{
if (ballPosY > player1PosY - halfPaddleHeight - ballRadius &&
ballPosY < player1PosY + halfPaddleHeight + ballRadius)
{
ballVelX = ballSpeedX;
ballVelY = 2.0 * (ballPosY - player1PosY) / halfPaddleHeight;
}
}
else if (ballPosX > player2PosX - ballRadius - halfPaddleWidth)
{
if (ballPosY > player2PosY - halfPaddleHeight - ballRadius &&
ballPosY < player2PosY + halfPaddleHeight + ballRadius)
{
ballVelX = -1.0 * ballSpeedX;
ballVelY = 2.0 * (ballPosY - player2PosY) / halfPaddleHeight;
}
}
}
void drawGame(){
oledDisplay.clear();
drawScore(playerScore, player2Score);
drawPaddle((int)player1PosX, (int)player1PosY);
drawPaddle((int)player2PosX, (int)player2PosY);
drawBall((int)ballPosX, (int)ballPosY);
oledDisplay.draw();
}
void drawScore(int player1, int player2){
int cursorX=58;
int cursorY=2;
oledDisplay.setString(cursorX, cursorY,Integer.toString(player1), oledDisplay.white, true);
cursorX=70;
cursorY=2;
oledDisplay.setString(cursorX, cursorY,Integer.toString(player2), oledDisplay.white, true);
}
void drawPaddle(int x, int y){
oledDisplay.rect(
(int)(x - halfPaddleWidth),
(int)(y - halfPaddleHeight),
(int)paddleWidth,
(int)paddleHeight);
}
void drawBall(int x, int y){
oledDisplay.circle(x, y, 1);
}

int checkWin(){
if (playerScore >= scoreToWin)
{
return PLAYER_1_WIN;
}
else if (player2Score >= scoreToWin)
{
return PLAYER_2_WIN;
}

return 0;
}

void drawWin(int player){
int cursorX=44;
int cursorY=2;

oledDisplay.clear();
if (player == PLAYER_1_WIN)
{
oledDisplay.setString(cursorX, cursorY,"Player 1");
}
else if (player == PLAYER_2_WIN)
{
oledDisplay.setString(cursorX, cursorY,"Player 2");
}
cursorX+=10;
cursorY=12;
oledDisplay.setString(cursorX, cursorY,"Wins!");
oledDisplay.draw();
}

void cleanUp(){
playerScore=0;
player2Score=0;

ballPosX = (Oled.OLED_WIDTH)/2.0;
ballPosY = (Oled.OLED_HEIGHT)/2.0;
ballVelX = -1.0 * ballSpeedX;
ballVelY = 0;

oledDisplay.clear();
oledDisplay.draw();
}

}




QRコードの表示

QRコードは、「レベル1」と呼ばれる最も小さいサイズで出力しても、余白(マージン)を除く黒がある範囲に21 dot(21セル)の幅が必要です。そのときに含ませられる情報量は、誤り訂正レベルをM(L,M,Q,Hの4段階の下から2番目)とした場合、数字・英大文字と一部の記号の混在で20文字となります。

詳細はこちらを参照ください。

THETAプラグインから操れるOLEDの短辺は24 dotあります。余白(マージン)を1 dotとすれば23x23 dotの範囲に、最小のQRコードをなんとか出力し他の機器に読み取らせることができそうです。

と、いうわけで・・・

チャレンジしたらできちゃいました!

OLEDへの表示結果は以下

QRコード表示_1920.jpg

スマートフォンでの読み取り結果は以下

QRコード読み取り_1920.png


ソースコード

QRコードの生成には、zxing(ゼブラクロッシング)というライブラリを利用しています。

ライブラリの使い方はこちらのような記事をご参照ください。

上記記事のbuild.gradle(Module:app)に記載する1行を以下のように変更しています。


build.gradle

dependencies {

   ~省略~
implementation 'com.journeyapps:zxing-android-embedded:3.6.0'
}

追加したimportは以下です。


MainActivity.java

import java.util.HashMap;

import com.google.zxing.WriterException;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.journeyapps.barcodescanner.BarcodeEncoder;

MainActivityに、QRコード格納用のBitmapオブジェクトとQRコード生成メソッドを作りました。


MainActivity.java

    //QRコード生成テスト

Bitmap qrBitmap;

private void makeQrCodeVer1(String qrText) {
int size = 21;
int margin = 1;

try {
BarcodeEncoder barcodeEncoder = new BarcodeEncoder();
HashMap hints = new HashMap();

hints.put(EncodeHintType.CHARACTER_SET, "utf8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, margin);
hints.put(EncodeHintType.QR_VERSION, 1);

qrBitmap = barcodeEncoder.encodeBitmap(qrText, BarcodeFormat.QR_CODE, size, size, hints);

} catch (WriterException e) {
Log.d ("OLED_TEST", "QR_ERROR");
qrBitmap = Bitmap.createBitmap( (size+margin*2), (size+margin*2), Bitmap.Config.ARGB_8888 );
//set ALL WHITE
for ( int x=0; x<(size+margin*2); x++ ) {
for (int y=0; y<(size+margin*2); y++ ) {
qrBitmap.setPixel(x,y, 0xFFFFFFFF);
}
}

}

}


「CHARACTER_SET」を"utf8"としているのは特に意味はありません。

今回は日本語を使っていなかったのでというだけでございます。

こちらの記事を参考とすると、入力値によりモードは以下のように判定され

(1) 数字だけ→「NUMERICモード」

(2) 数字、英大文字と一部の記号 (0~9, A~Z, スペース, '$', '%', '*', '+', '-', '.', '/', ':')だけ→「ALPHANUMERICモード」

(3) (1)(2)以外の文字や記号が含まれる→「BYTEモード」

バージョン1 誤り訂正Mのときには、以下の情報量をQRコードに埋め込めます。

(1) 34文字

(2) 20文字

(3) 14バイト(14文字)

URLなどは(3)に該当しますので、自動アップロードするようなプラグインのURLを伝えるのは難しそうです。

表示箇所では以下のようなコードを記載しています。

QRコード化した元文字列もOLEDに全て表示したかったため、17文字の表示例となっています。


MainActivity.java

                        oledDisplay.clear();

String qrString = "ABCDEFG0123456789";

makeQrCodeVer1(qrString);
Integer qrX = qrBitmap.getWidth();
Integer qrY = qrBitmap.getHeight();
oledDisplay.setString(25, 0, "QR Width =" + Integer.toString(qrX));
oledDisplay.setString(25, 8, "QR Height=" + Integer.toString(qrY));
oledDisplay.setString(25, 16, qrString);
oledDisplay.setBitmap(0,0,23,23, 0,0,128, qrBitmap);

oledDisplay.draw();


oledDisplayはOledクラスのインスタンスです。インスタンス化やOLEDの初期操作(輝度設定や表示クリア)については、他事例のソースコードを参照してください。

QRコード表示の場合、輝度を高く設定しないほうが、スマートフォンなどで読み取りがしやすいと思います。


まとめ

なかなか使えるOLED描画ライブラリができたかと思います。

幾らかの制約がありますし、もしかしたら・・・まだバグが残っているかもしれませんが、お気づきの点があったらご指摘いただければ幸いです。

表示ライブラリが整うと、以下のようなものも作りやすくなったのではないでしょうか。


  • OpenCVでライブビューのエッジ抽出した絵をリアルタイム表示

  • 撮影モードや諸設定の全情報表示画面

  • ライブビュー用データからのヒストグラム表示

  • グラフィカルな水準器表示

  • マイクから得た音の波形をリアルタイムでザックリ表示

  • ネットワークも利用したゲーム(他人のTHETAへおでかけするたまご○ち的なものとか?)


  • THETAにキーボードを繋げることができるので、ADBコマンドが実行できるターミナル

  • プラグインのデバッグ情報(logcat)をOLEDに垂れ流す

etc

いろいろな発想で、あらたなプラグイン作成にトライして頂けると幸いです。

イ○ベーダー的なもの、ブロック崩し的なもの、極狭テトリスなどなど、レトロゲーム的なものもたくさん作れそうです。

作成した作品?は、THETAプラグインストアへ公開して頂けるとうれしいです。


RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方はこちらをご覧ください。

パートナープログラムへの登録方法はこちらにもまとめてあります。

興味を持たれた方はTwitterのフォローとTHETAプラグイン開発コミュニティ(Slack)への参加もよろしくおねがいします。