hello (visible) world.

某庁の現業室にある時計を「再び」再現してみた

2025.12.06 · 3933 words · 8 minute read
この記事ではアフィリエイト広告を利用しています
dev , Android , Signage

この記事は 防災アプリ開発 Advent Calendar 2025 の6日目の記事です。

某庁のオペレーションルーム(現業室)に設置されている時計を今度はウルトラワイドAndroidサイネージ端末で再現してみたお話です。

今年5月に8.8インチの横長モニター(通称、ツイ廃液晶)とRaspberry Piを使って、某庁のオペレーションルーム(現業室)にある時計のミニチュア再現版である opr-clock を作成しました。

某庁の現業室にある時計を再現してみた · hello (visible) world.

8.8インチ版opr-clock

Raspberry Pi Zero 2 Wと8.8インチ横長モニターで作ったopr-clock

卓上に置くには最適なサイズ感であるものの、もう少し大きいと壁掛け時計としても使えるのに…と思っていたところに、29インチのウルトラワイドディスプレイにAndroid 7.1を搭載したサイネージ端末の中古品が3,980円で特価販売されていたので、これを使って より実寸に近いサイズの現業室時計を作ろう というお話です。

1. サイネージ端末の仕様

LD290EJS-FPN1の表と裏

LD290EJS-FPN1の表(画像下)と裏(画像上)。左下はサイズ比較用の Pixel 10 Pro

今回使用するサイネージ端末はLG Displayが展開しているin-TOUCH Stretch Displayシリーズの29インチモデル「LD290EJS-FPN1」で、日本国内ではPCパーツなどの代理店として有名なアスクが取り扱っていたようです。(現在は販売終了)

以下、主要な仕様を抜粋します。(販売終了につき製品ページが消える可能性があるためバックアップも兼ねて…)

ディスプレイパネル29インチ TFT-LCD(IPS)
解像度1920×540(32:9)
リフレッシュレート60Hz
輝度500nit
表示領域698.4(H)×210.03(V) mm
ベゼル幅7.2(LR)/7.7(U)/9.6(D) mm
外形寸法713(H)×215.8(V) mm
表面処理AG, 4H
タッチレポートレート60Hz
マルチタッチ10ポイント未満
遅延時間35ms以下
精度3.5mm以下
最低感知サイズ
I/OMicro USB 2.0: Host×1, OTG×1
CPUARM Coretex-A17, Quad-core, 1.6GHz
OSAndroid 7.1
メモリー1GB DDR3
RAM領域8GB eMMC, 空き領域約4.4GB
ワイヤレスWiFi2.4: 802.11 b/g/n 2.4GHz, BT4.0
消費電力30W未満
電源12V3A, Type-C
重量2.80 Kg

スペック上で気になるところをいくつか挙げてみます。

  • 解像度は1920x540の32:9で、これは1920x1080を横半分に切ったような形。最近のウルトラワイドといえば3840x1080などの1920x1080が2枚並んだような解像度が多いので、解像度的には一昔前といったところでしょうか。

  • ディスプレイの表面がAG(アンチグレア)で硬度4Hなのは嬉しいですね。実際に触った感触としても硬いので保護フィルムなども必要なさそうです。

  • I/Oは Micro USB 2.0: Host×1, OTG×1 とありますが、実際にはこれに加えてMicroHDMI端子が1つ付いています。しかしながら入力としては使えないようなので出力用でしょうか。(HDMI周りは動作未確認)

  • CPUはCoretex-A17なので32bitです。昨今は64bitのCPUが主流なのでこれもちょっと古いですね。1.6GHzのクアッドコアとなっていますが、メモリーが1GBなので重いアプリを動かすのは厳しそうです。

  • 内蔵ストレージは8GBのeMMCで空きは半分くらいなので、何らかの写真や動画などのコンテンツを再生したい場合はUSBフラッシュメモリーに入れてOTGケーブルで接続してあげる方が良さそうです。

  • 無線周りはIEEE 802.11 b/g/nなのでWi-Fi4相当、ただし2.4GHzのみで5GHzは使えません。一応Bluetoothも4.0ですが使えるようです。4.0ということはBLEが使えそうですね。

  • OSはAndroid 7.1.2(APIレベル25)です。リリースが2017年4月頃なので約9年前のOSということになります。この辺りのバージョンのAndroidは多種多様なデバイスに搭載されていて技術的に枯れているのでアプリを作りやすいです。

モバイルバッテリーで動作中

モバイルバッテリーで動作中のLD290EJS-FPN1。12.1V/1.4A=約17Wで動作中。

  • 消費電力は30W未満となっていますが、計測したところ画面の明るさ次第で10W台〜20W台と大きく変動します。電源として12V3A(=36W)の記載がありますが、昨今のUSB-PD出力対応USB充電器やモバイルバッテリーだと30W程度まで対応しているものが多いので、12Vのトリガーケーブル変換ケーブルを一緒に使うことで専用ACアダプターは無くても動きます。

  • ちなみに、ACアダプター付きの場合についてくるのは「FSP036-DHUA2」という型番のACアダプターらしい。出力は12V3AでアスクがPSE認証を取得している模様。

  • 私が購入したものはACアダプターが付いていなかったので、12V3A で DC5521(外径5.5mm/内径2.1mm)プラグACアダプターDC5521メスからUSB-Cオス への変換ケーブルを別途購入して使っています。


2. アプリの設計と実装

仕様が分かったところで、早速アプリを作っていきます。Chromeを搭載しているのでブラウザで動くアプリとして作っても良いかもと思いましたが、既にOSのアップデートなどが終わっていることや初期状態でGoogle Play Storeが入っておらずChromeのアップデートが面倒なため、今回はKotlinでネイティブアプリとして作りました。

作ったアプリのコードは opr-clock-android として公開しています。

9SQ/opr-clock-android: A digital clock for LD290EJS, inspired by the clock in the JMA’s operation room

Android Simulator上で動かしたopr-clock-android

アプリの処理の流れとしてはザックリと以下のようになります。起動後は2〜5の繰り返しです。

1. アプリ起動時の初期化 onCreate

  • カスタム描画ビュー SignageClockView を全面に設定
  • フルスクリーン設定 window.decorView.systemUiVisibility
  • 画面の常時点灯設定 FLAG_KEEP_SCREEN_ON
  • NTPサーバとの初回同期 trueTime.syncOnceAsync()

2. 正確な現在時刻を取得する SntpClient TrueTimeProvider

  • UDPでNTPサーバにパケットを送信し、応答のタイムスタンプをパース
  • NTP時刻(1900年基準)を UNIX時刻(1970年基準)に変換
  • NTPサーバからの時刻(UNIX時刻)を lastNtpTimeMillis に保存
  • 端末が起動してから経過した時刻 elapsedRealtime() を問い合わせ時 requestTime と応答時 responseTime に保存
  • responseTime から requestTime を引いてRTT roundTrip を求め、半分に割って片道の時間 ntpTimeReferenceMillis を保存
  • lastNtpTimeMillis + (elapsedRealtime() - lastNtpReferenceElapsed)で正確な現在時刻が求まる

3. 1秒ごとの画面更新タイマーを動かす updateRunnable

  • updateTime() で現在時刻を描画
    • trueTime.nowJst() で日本標準時の Calendar を生成
    • trueTime.nowUtc() でグリニッジ標準時の Calendar を生成
    • SignageClockView.setTime(jst, utc) に渡す
    • invalidate() メソッドで SignageClockView を再描画
  • 現在時刻を取得して val now = trueTime.nowMillis()
  • 次の秒までの差分を計算して val delay = 1000 - (now % 1000)
  • delay 時間後に再度 updateRunnable を実行させる updateHandler.postDelayed(this, delay)

4. 日付と時刻を整形して描画する SignageClockView

  • 1920x540 の中に座標指定でちまちま描いていく(RasPi版 opr-clock と同じような感じ)

5. 10分毎に時刻を再同期する syncRunnable

  • 10分毎に trueTime.syncOnceAsync() を呼び出す syncHandler.postDelayed(this, 10 * 60 * 1000L)

RasPi版 opr-clock では timedatectl でNTP設定を行うことで時刻同期していましたが、このアプリでは端末のシステム時計を使用(信用)せず、NTPサーバと直接同期して時刻を表示するようにしています。
当初はシステム時計を使用して時刻同期はAndroid OS側に丸投げしようと思っていたのですが、実際に運用していると筆者の環境ではシステム時計が2秒弱程度進むのを確認…
時刻同期のタイミングも不明で同期先のサーバも指定できないブラックボックスであり、これによって時計がズレるのは本来の使用用途からして許容できない為、アプリ側で同期することに。


3. opr-clock-androidを起動する

ビルドして署名したapkをGitHubリポジトリのReleaseに置いているので、LD290EJS-FPN1をお持ちの方はUSBフラッシュメモリに入れてOTGケーブルで接続してインストールすればすぐに使えます。

OTGケーブルでUSBフラッシュメモリを接続した様子

インストールすると opr-clock がランチャーに現れるので起動すると以下のような感じ。

LD290EJS-FPN1で起動したopr-clock

ちなみに このアプリ、Android 7.1以上であれば ほとんどの機種で動作すると思います。ただし、パンチホールタイプのインカメラが付いている端末だとカメラの部分が欠けます。

Pixel 10 Proで起動したopr-clock

Pixel 10 Proで起動したopr-clock。グリニッジ標準時の画面右側、「秒」の左上が欠けている。

そして、ウルトラワイドディスプレイではない端末で動かすと何も表示されないスペースが時計の下に出来てしまうので、他の機種で使う場合は適宜コードをいじって、他のタイムゾーンを追加して3段、4段にするとか、天気や気温など何か別の表示をするようにするとか、カスタマイズすると良いかなと思います。(MITライセンスなので、各々でフォークして良い感じにしてください。)

今回はあくまで LD290EJS-FPN1 で時計を表示しっぱなしにする目的で製作したため、タッチを活かした何らかの操作機能や設定画面などを省いた手抜き仕様です。気が向いたら他の機種でも使えるようなアップデートをした上でGoogle Play Storeに公開するかもしれません。


番外編:設置方法、どうしよう

運よく LD290EJS-FPN1 を2枚購入できたので、1枚は会社に持っていって壁面サイネージの仲間にしてあげようと画策しているものの、裏の穴がVESA等の規格ではないため何らかの固定金具を作る必要がありそう。(これ専用の壁掛け金具などが公式で用意されているようですが、既に本体が販売終了のため入手経路が無い)

重量が2.8Kgあるため、一般的なタブレットスタンドのようなものでは保持力が足りず、現状は壁に立て掛けて下部に耐震ジェルを貼って仮固定しているような状況…

うまく設置できたら追記しようと思います。