Bラボデジタルサイネージ（長期用テキスト1/3）

![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fuser-images.githubusercontent.com%2F29194025%2F137117015-af05fe4f-7c32-4e38-876f-e153ff2f2f7d.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=2bbf46fb5c1d8dec16f2fcb5d089fde2)

https://github.com/ohayota/digital-signage-template

本記事は、[株式会社 函館ラボラトリ](http://www.hakolab.co.jp)が運営する「[Bラボ](http://www.hakolab.co.jp/b-lab/index.html)」における、大人向け「看板アプリ（デジタルサイネージ）を作るコース」用テキストです。
一からコードを書くわけではなく、あらかじめ用意したコードの一部を改変しながら、デジタルサイネージを作っていきましょう。

http://www.hakolab.co.jp/b-lab/index.html

テンプレートプログラムは、下記GitHubリポジトリの`LongTerm/Part1_Start`ディレクトリに入っています。

https://github.com/ohayota/BLabo-digital-signage-sample/tree/main/LongTerm/Part1_Start


（12/12時点、本テキストは執筆中です）


# デジタルサイネージとは何か？

下記記事の「概要」をご参照ください。

https://qiita.com/ohayota/items/f4aa3e1b452060eb52ad#概要


# 必要なもの

下記記事の「必要なもの」をご参照ください。

https://qiita.com/ohayota/items/f4aa3e1b452060eb52ad#必要なもの


# 実装

あらかじめ途中まで実装した状態のプログラムを用意します（`LongTerm/Part1`ディレクトリの中にあります）。

https://github.com/ohayota/BLabo-digital-signage-sample

モジュールについての説明は、下記記事の「機能実現の方針」をご参照ください。

https://qiita.com/ohayota/items/41a746d2258673726541#機能実現の方針

ひとつひとつのモジュールが表示されるよう、プログラムを書いていきましょう。


# 電子部品の配置

モジュールのうち、下記のモジュールの機能を実現するために、ブレッドボード上に電子部品を配置します。

- 開店／閉店表示機能（OpenCloseRModule）用のスライドスイッチ
- 温度表示機能（TemperatureRModule）用の温度センサ
- 明るさ表示機能（BrightnessRModule）用の明るさセンサ

## スライドスイッチの配置

下記記事の「電子部品の配置」に従い、ブレッドボードに部品を配置してください。

https://qiita.com/ohayota/items/284b5c7e7101f602016f#電子部品の配置

## 温度センサの配置

下記記事の「電子部品の配置」に従い、ブレッドボードに部品を配置してください。

https://qiita.com/ohayota/items/386c27a021601fb3ccfd#電子部品の配置

## 明るさセンサの配置

下記記事の「電子部品の配置」に従い、ブレッドボードに部品を配置してください。

https://qiita.com/ohayota/items/76279927b4ef35eaf628#電子部品の配置


# フォントのインストール

下記記事の「フォントのインストール」に従い、ラズパイにフォントをインストールしてください。

https://qiita.com/ohayota/items/88fbcf177e6606311efb#フォントのインストール


# 画像素材読み込み

デジタルサイネージ自体の背景や、RModuleの背景は、`data`ディレクトリの中に入っている画像を読み込んで表示します。
画像を読み込む処理を実装しましょう。
新しいファイル`Initialize.pde`を作り、以下のように記述してください。

``` java:Initialize.pde
void initialize() {
  initializeImage();
}

void initializeImage() {
  busMap = pImageCut(loadImage(BUS_PATH + "bus_map.jpg"), CENTER, CENTER, 1280, 720);
  adImage = new PImage[AD_IMAGE_COUNT];
  for (int i = 0; i < AD_IMAGE_COUNT; i++) {
    adImage[i] = loadImage(AD_PATH + "ad" + i + ".jpg");
  }
  dummy360x360 = loadImage(DUMMY_PATH + "360x360.jpg");
}
```

データ読み込みなど、以降「初期化処理」と呼ばれる処理を実行するときは、`initialize()`を呼び出します。
`initializeImage()`は、`data`ディレクトリ内にある画像を読み込むための関数として定義しています。

`busMap`のところで使っている関数`pImageCut`は、指定した大きさに画像をカットする関数です。
この関数を作った意図については、必要であれば、下記記事の「元の画像が縦横に引き伸ばされている場合の対処」を参照してください。

https://qiita.com/ohayota/items/4a9c8423459c251f4911#元の画像が縦横に引き伸ばされている場合の対処

関数`pImageCut`を作ります。

``` java:DigitalSignage.pde
// 画像を指定されたサイズにカットして返す。
PImage pImageCut(PImage image, int modeX, int modeY, int afterWidth, int afterHeight) {
  int w = image.width;
  int h = image.height;
  
  if (modeX == LEFT) image = image.get(0, 0, afterWidth, h);
  if (modeX == CENTER) image = image.get(w/2-afterWidth/2, 0, afterWidth, h);
  if (modeX == RIGHT) image = image.get(w-afterWidth, 0, afterWidth, h);
  
  if (modeY == TOP) image = image.get(0, 0, afterWidth, afterHeight);
  if (modeY == CENTER) image = image.get(0, h/2-afterHeight/2, afterWidth, afterHeight);
  if (modeY == BOTTOM) image = image.get(0, h-afterHeight, afterWidth, afterHeight);
  
  return image;
}
```


# RModuleのスクリーンショットを表示する

`LongTerm/Part1/data`の中には、RModule（WeatherRModule、GomiRModule、BusRModule、TwitterRModule、BrightnessRModule、OpenCloseRModule、TemperatureRModule）のスクリーンショットが入っています。
たとえば、下図のようなものです。

![sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368192/77cbbcb9-a2a5-f704-9b7c-8554a7fe55d1.jpeg)


## RModule表示のための基準作成

このようなスクリーンショットを、実際に画面へ表示できるようにしていきます。
RModuleを表示できる基準となる場所は、下記記事の「モジュール表示に使う基準座標を定める」に書かれているように、8つの場所があります。

https://qiita.com/ohayota/items/df704f523b5aa4dbb180#モジュール表示に使う基準座標を定める

8つの場所それぞれの左上の座標は、このあとRModuleを表示するときの基準となります。
まずは、座標を取得するための関数を作りましょう。
下記記事の「モジュール表示に使う基準座標を定める」に従って、関数`layoutGuideX`と関数`layoutGuideY`を作ってください。

https://qiita.com/ohayota/items/df704f523b5aa4dbb180#モジュール表示に使う基準座標を定める


## 各RModuleのサイズ決定

7つのRModuleは、大きいものから小さいものまでサイズは様々です。
サイズは「S」「M」「L」として定めます。
「S」〜「L」のサイズ目安については、下記記事「モジュールの高さと幅を定める」をご参照ください。

https://qiita.com/ohayota/items/f52a10660009794afd9f#モジュールの高さと幅を定める


RModuleのサイズを返してくれる関数を作りましょう。

``` java:DigitalSignage.pde
// 各RModuleのサイズを返す。
Size moduleSize(RModule module) {
  if (module == RModule.Weather) return Size.M;
  if (module == RModule.Bus) return Size.L;
  if (module == RModule.Gomi) return Size.M;
  if (module == RModule.Twitter) return Size.L;
  if (module == RModule.OpenClose) return Size.M;
  if (module == RModule.Temperature) return Size.S;
  if (module == RModule.Brightness) return Size.S;
  return Size.S;
}
```

次に、各サイズごとの「幅」を返してくれる関数`moduleWidth`、「高さ」を返してくれる関数`moduleHeight`を作りましょう。

``` java:DigitalSignage.pde
// RModuleのサイズごとの大きさを返すための関数。RModuleの幅を返す。
int moduleWidth(Size size) {
  if (size == Size.S) return 405;
  if (size == Size.M || size == Size.L) return 850;
  return 0;
}

// RModuleのサイズごとの大きさを返すための関数。RModuleの高さを返す。
int moduleHeight(Size size) {
  if (size == Size.S || size == Size.M) return 400;
  if (size == Size.L) return 830;
  return 0;
}
```


## 各RModule用関数の実装

7つのRModule用に、`RM_Weather.pde`などの、`RM_`から始まるファイルを作ってあります。
それぞれのファイルに、`draw`から始まる描画用関数を作成していきます。
関数の中には、「RModuleのスクリーンショットを読み込み、表示する」処理を記述してあります。

``` java:RM_Brightness.pde
void drawBrightnessRModule(Area area) {
  RModule module = RModule.Brightness;
  Size size = moduleSize(module);
  
  int x = layoutGuideX(area);
  int y = layoutGuideY(area);
  int w = moduleWidth(size);
  int h = moduleHeight(size);
  
  image(loadImage(BRIGHTNESS_PATH + "sample.jpg"), x, y);
}
```

``` java:RM_Bus.pde
void drawBusRModule(Area area) {
  RModule module = RModule.Brightness;
  Size size = moduleSize(module);
  
  int x = layoutGuideX(area);
  int y = layoutGuideY(area);
  int w = moduleWidth(size);
  int h = moduleHeight(size);

  image(loadImage(BUS_PATH + "sample.jpg"), x, y);
}
```

``` java:RM_Gomi.pde
void drawGomiRModule(Area area) {
  RModule module = RModule.Brightness;
  Size size = moduleSize(module);
  
  int x = layoutGuideX(area);
  int y = layoutGuideY(area);
  int w = moduleWidth(size);
  int h = moduleHeight(size);
  
  image(loadImage(GOMI_PATH + "sample.jpg"), x, y);
}
```

``` java:RM_OpenClose.pde
void drawOpenCloseRModule(Area area) {
  RModule module = RModule.Brightness;
  Size size = moduleSize(module);
  
  int x = layoutGuideX(area);
  int y = layoutGuideY(area);
  int w = moduleWidth(size);
  int h = moduleHeight(size);
  
  image(loadImage(OPENCLOSE_PATH + "sample.jpg"), x, y);
}
```

``` java:RM_Temperature.pde
void drawTemperatureRModule(Area area) {
  RModule module = RModule.Brightness;
  Size size = moduleSize(module);
  
  int x = layoutGuideX(area);
  int y = layoutGuideY(area);
  int w = moduleWidth(size);
  int h = moduleHeight(size);
  
  image(loadImage(TEMPERATURE_PATH + "sample.jpg"), x, y);
}
```

``` java:RM_Twitter.pde
void drawTwitterRModule(Area area) {
  RModule module = RModule.Brightness;
  Size size = moduleSize(module);
  
  int x = layoutGuideX(area);
  int y = layoutGuideY(area);
  int w = moduleWidth(size);
  int h = moduleHeight(size);
  
  image(loadImage(TWITTER_PATH + "sample.jpg"), x, y);
}
```

``` java:RM_Weather.pde
void drawWeatherRModule(Area area) {
  RModule module = RModule.Brightness;
  Size size = moduleSize(module);
  
  int x = layoutGuideX(area);
  int y = layoutGuideY(area);
  int w = moduleWidth(size);
  int h = moduleHeight(size);
  
  image(loadImage(WEATHER_PATH + "sample.jpg"), x, y);
}
```

## 描画用関数の呼び出し

RModuleなどを呼び出すときに使う関数`drawModules()`を作り、`draw()`内で呼び出すようにします。

``` java:DigitalSignage.pde
void draw() {
  if (nowPageID == -1) {
    
  } else {
    drawModules();
  }
}

void drawModules() {
  if (nowPageID == 0) {
    drawWeatherRModule(Area.area1);
    drawBusRModule(Area.area3);
    drawGomiRModule(Area.area5);
  } else if (nowPageID == 1) {
    // ModuleやRModuleの描画用関数を呼び出す
    drawTemperatureRModule(Area.area1);
    drawBrightnessRModule(Area.area2);
    drawTwitterRModule(Area.area3);
    drawOpenCloseRModule(Area.area5);
  } else if (nowPageID == 2) {
    // ModuleやRModuleの描画用関数を呼び出す
  }
}
```

上記の記述のうち、`drawWeatherRModule(Area.area1);`のように書いている部分があります。
記述の意味は、「area1の左上を基準座標にして、WeatherRModuleを描画する」ということになります。


# ページ切り替えの実装

現時点のコードでは、画面が切り替わらず、スクリーンショットが見えない状態になっています。
画像読み込みなどの初期化がおわったときや、特定の秒数経過したときに自動で切り替わるよう、あたらしく関数を作り呼び出します。


## ページ切り替え用関数

`nowPageID`という変数で、どのページを表示するか管理します。
この`nowPageID`という数字を変更するための関数`updateNowPageID`をつくります。

``` java:DigitalSignage.pde
// ページ番号を更新する。次のページ／前のページかが選べる。
void updateNowPageID(boolean isIncrement) {
  if (isIncrement) {
    nowPageID = (nowPageID + 1) % PAGE_ALL_COUNT;
  } else {
    nowPageID = (nowPageID + PAGE_ALL_COUNT - 1) % PAGE_ALL_COUNT;
  }
}
```

関数`updateNowPageID`は、「初期化が終わったとき」と「特定の秒数経過したとき」に呼び出します。

``` java:Initialize.pde
void initialize() {
  initializeImage();

  updateNowPageID(true); /* 追加 */
}
```

「特定の秒数経過したとき」の処理は、新しい関数`updateDatas`を定義して実装します。

``` java:DigitalSignage.pde
void updateDatas() {
  updateDate(); // 日付はかならず先に更新する。
  
  // 各電子部品の値を取得し、正しく取得できたかも記録しておく。
  //isUpdatedOpenClose = updateOpenClose();
  //isUpdatedTemperature = updateTemperature();
  //isUpdatedBrightness = updateBrightness();
  
  // 日が変わったかを取得（1日に1回だけ実行する処理のために使う）。
  final boolean isUpdatedDay = (day != beforeDay);
  // 秒が変わったかを取得（1秒に1回だけ実行する処理のために使う）。
  final boolean isUpdatedSecond = (second != beforeSecond);
  
  // 1日に1回だけ実行する。
  if (isUpdatedDay) {
    println("日付が変わりました。");
    
    // 本日が土日祝かどうかを、祝日APIを用いて取得。
    //if (!updateIsHoliday()) {
    //  println("ネットワークに接続できていない可能性があります。接続できているか確認してください。");
    //}

    // 今日の曜日を日付から計算。
    //youbi = calcYoubi(year, month, day);
    //youbiString = youbiToString(youbi);
    
    // WebAPIから値を取得し、正しく取得できたかも記録しておく。
    //isUpdatedBus = updateBus();
    //isUpdatedGomi = updateGomi();
    //isUpdatedTwitter = updateTwitter();
    
    // 処理が全て完了したら更新。
    beforeDay = day;
  }
  
  // 1秒に1回だけ実行する。
  if (isUpdatedSecond) {
    // 特定の秒数ごとに画面を切り替える。
    if (second % STAY_SECOND == 0) {
      updateNowPageID(true);
    }
    if (minute + second == 0) {
      println(hour + "時になりました。");
    }
    
    // 処理が全て完了したら更新。
    beforeSecond = second;
  }
}
```

関数`draw`の中で`updateDatas()`を追記します。

``` java:DigitalSignage.pde
void draw() {
  if (nowPageID == -1) {
    
  } else {
    updateDatas();
    drawModules();
  }
}
```

## 現在時刻の取得用関数

現時点のコードでは、現在時刻が取得できていません。
現在時刻を取得する関数を実装します。

``` java:M_Date.pde
void updateDate() {
  year = year();
  month = month();
  day = day();
  hour = hour();
  minute = minute();
  second = second();
}
```

ここで実行すると、10秒待つごとに画面が切り替わるようになっています。


# 次回

次はPart2です。
Part2では、Part1時点で表示されていないモジュール（日付時刻表示など）を実装していきます。

https://qiita.com/ohayota/items/5f36360b03dd229ba47b