はじめに
Cisco IP Phone 7962を買いました。
Cisco Unified IP Phone Services Applicationで画面表示をカスタマイズできるデバイスを買ったら?そう、エオルゼア時計ですね!
問題は今まで使っていたCisco IP Phone 7965はカラー液晶だったのでnode-canvasでカラー画像を生成して表示させていましたが、7962にはグレースケールフォーマット画像を渡してあげないといけません。
…が、node-canvasは現時点でグレースケールフォーマットでの画像書き出しができないとのこと。
せっかくなので、エオルゼア時間を求めるコードをPerlに移植して画像もPerlで作ってしまいましょう。
エオルゼア時間について
(初めての方もいると思うのでまずはおさらいを)
FINAL FANTASY XIV(FF14) のゲーム中では、現実時間(日本標準時)をベースとした独自の時間、エオルゼア時間が流れています。
エオルゼア時間は現実時間の 約20.57倍
のスピードで進みます。また、日本時間日曜日0時0分には、エオルゼア時間も0時0分になります。
この特性を利用することによりエオルゼア時間は、ゲーム外でも主に下記の手法にて算出することができます。
- epoch乗算法:日本時間epoch秒を約20.57倍したものをエオルゼア時間epoch秒と扱い、エオルゼア時分を求める
- 週epoch乗算法:日本時間の日曜日0時0分を起点とする現実時間epoch秒を約20.57倍したものをエオルゼア時間週epoch秒と扱い、エオルゼア時分を求める
- ルックアップテーブル法:エオルゼア時間の周期性をまとめたルックアップテーブルをもとにエオルゼア時分を求める
ちなみに手法の名称は今私が勝手に付けました。
開発環境等
- Debian GNU/Linux 12
- Perl v5.36.0 (System Perl)
- systemd
- nginx
- plack
- Imager
- NotoSansMono
必要条件
下記の各種パッケージ・モジュールをインストールしてください。
Debian系以外のディストリビューションをお使いの方は別途ググってください。(などと)
sudo apt install fonts-noto-mono
sudo apt install cpanminus
sudo apt install libfreetype6-dev
sudo cpanm Plack::Builder Imager
エオルゼア時間をPerlで求める
以前私が投稿した記事に掲載したルックアップテーブル法を用いたエオルゼア時間算出のC++コードを下記のようにPerlに移植しました。
ついでに(?)24時間制で時を求められるように変更しました。
use constant ET_LOOKUP_TABLE => [
[0, 12342],
[0, 24685],
[0, 37028],
[1, 6171],
[1, 18514],
[1, 30857],
[1, 43199]
];
sub get_et {
my ($tp) = @_;
my $day_of_week = $tp->day_of_week;
my $hour = $tp->hour;
my $min = $tp->min;
my $sec = $tp->sec;
my $count = $day_of_week * 144 + $hour * 6 + int($min / 10);
my $lt_index = ($count == 0) ? -1 : ($count - 1) % 7;
my $base_is_pm = ($lt_index == -1) ? 0 : &ET_LOOKUP_TABLE->[$lt_index]->[0];
my $base_et_sec = ($lt_index == -1) ? 0 : &ET_LOOKUP_TABLE->[$lt_index]->[1];
my $diff_from = int($min / 10) * 10;
my $diff = ($min - $diff_from) * 60 + $sec;
my $sec_of_et = $base_et_sec + ($diff * 20) + (($diff * 57) / 100) + ($base_is_pm ? 43200 : 0);
my $et_min = int($sec_of_et / 60) % 60;
my $et_hour = $sec_of_et / 3600 % 24;
return {
hour => $et_hour,
min => $et_min
};
}
Imagerで表示する画像をPSGIアプリケーションで生成する
開発中 Imager::Font
でフォントをロードする際にエラーが発生しました。
libfreetype6-dev
パッケージがないと FreetypeFontの読み込みがうまくいかないようです。
当該パッケージインストール後 sudo cpanm --reinstall Imager
とすると、読み込みができるようになります。
この箇所のコードは Appendix:Perlコード全文
でご確認ください。
お気づきかと思いますが、いわゆるペライチPSGIアプリケーションです。
静的XMLを用意する
画像だけ変わればよいので、Application XMLは固定です。
※画像のURLでアクセスできるよう、後ほどnginxを設定します。
<?xml version="1.0" encoding="utf-8"?>
<CiscoIPPhoneImageFile WindowMode="Wide">
<Title>Cisco IP Phone Image File</Title>
<Prompt></Prompt>
<LocationX>0</LocationX>
<LocationY>0</LocationY>
<URL>http://○○.○○.○○.○○/api2/cisco_et.png</URL>
<SoftKeyItem>
<Name>Cancel</Name>
<URL>SoftKey:Exit</URL>
<Position>3</Position>
</SoftKeyItem>
</CiscoIPPhoneImageFile>
nginxを設定する
すみません、Node.jsで作ってあるところに入れこんだので若干おかしなことになっています。
/api2/
以下にアクセスがあった場合、PSGIアプリケーションに渡されます。
server {
listen 80 default_server;
listen [::]:80 default_server;
root /home/user/web/static/;
server_name _;
location ~ ^/(.*)\.xml$ {
add_header Refresh "1";
}
location /api/ {
proxy_pass http://localhost:3000;
}
location /api2/ {
proxy_pass http://localhost:5000;
}
}
PSGIアプリケーションをデーモン化する
[Unit]
Description=Cisco Eorzea Image App Server
[Service]
User=user
PIDFile=/run/cisco_et.pid
WorkingDirectory=/home/user/sysperl/
Type=simple
ExecStart=/usr/local/bin/plackup /home/user/sysperl/cisco_et.psgi
ExecReload=/bin/kill -s HUP $MAINPID
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
実際の動作等
ブラウザで /api2/cisco_et.png
にアクセスすると、次のような画像が表示されます。
また、PBXサーバー側でXML ApplicationのURLを設定すると実機がidle中、下記のような表示がなされます。
さらに改造後
私の所属団体のRainyVillageのメンバーに見せたところ『こうしたらもっと面白くない?』となり、最終的にこうなりました。
おわりに
明日の担当は papixさん です。お楽しみに!
Appendix:Perlコード全文(改造前)
#!/usr/bin/env perl
# ====================================================== #
# Copyright (c) 2024 CIB-MC #
# Released under the MIT license #
# https://opensource.org/licenses/mit-license.php #
# ====================================================== #
use strict;
use warnings;
use utf8;
use Time::Piece;
use Time::Seconds;
use Plack::Builder;
use Imager;
use Imager::Color;
our $VERSION = '0.1';
use constant BLACK => Imager::Color->new(gray => 0);
use constant WHITE => Imager::Color->new(gray => 255);
use constant IMG_WIDTH => 320;
use constant IMG_HEIGHT => 144;
use constant FONT => Imager::Font->new(
file => '/usr/share/fonts/truetype/noto/NotoSansMono-Bold.ttf',
aa => 1
);
use constant FONT_SIZE_LABEL => 18;
use constant FONT_SIZE_TIME => 42;
use constant ET_LOOKUP_TABLE => [
[0, 12342],
[0, 24685],
[0, 37028],
[1, 6171],
[1, 18514],
[1, 30857],
[1, 43199]
];
sub get_et {
my ($tp) = @_;
my $day_of_week = $tp->day_of_week;
my $hour = $tp->hour;
my $min = $tp->min;
my $sec = $tp->sec;
my $count = $day_of_week * 144 + $hour * 6 + int($min / 10);
my $lt_index = ($count == 0) ? -1 : ($count - 1) % 7;
my $base_is_pm = ($lt_index == -1) ? 0 : &ET_LOOKUP_TABLE->[$lt_index]->[0];
my $base_et_sec = ($lt_index == -1) ? 0 : &ET_LOOKUP_TABLE->[$lt_index]->[1];
my $diff_from = int($min / 10) * 10;
my $diff = ($min - $diff_from) * 60 + $sec;
my $sec_of_et = $base_et_sec + ($diff * 20) + (($diff * 57) / 100) + ($base_is_pm ? 43200 : 0);
my $et_min = int($sec_of_et / 60) % 60;
my $et_hour = $sec_of_et / 3600 % 24;
return {
hour => $et_hour,
min => $et_min
};
}
sub cisco_et_png {
my ($tp) = @_;
my $et = get_et($tp);
my $img = Imager->new(xsize=> IMG_WIDTH, ysize => IMG_HEIGHT, channels=>1);
$img->box(color => WHITE, xmin => 0, ymin => 0, xmax => IMG_WIDTH, ymax => IMG_HEIGHT, filled => 1);
$img->align_string(
string => 'Eorzea Time',
color => BLACK,
x => IMG_WIDTH * 0.27,
y => IMG_HEIGHT * 0.4,
halign => 'center',
valign => 'bottom',
font => FONT,
size => FONT_SIZE_LABEL
);
$img->align_string(
string => sprintf("%02d:%02d", $et->{hour}, $et->{min}),
color => BLACK,
x => IMG_WIDTH * 0.27,
y => IMG_HEIGHT * 0.5,
halign => 'center',
valign => 'top',
font => FONT,
size => FONT_SIZE_TIME
);
$img->align_string(
string => 'Local Time',
color => BLACK,
x => IMG_WIDTH * 0.73,
y => IMG_HEIGHT * 0.4,
halign => 'center',
valign => 'bottom',
font => FONT,
size => FONT_SIZE_LABEL
);
$img->align_string(
string => sprintf("%02d:%02d", $tp->hour, $tp->min),
color => BLACK,
x => IMG_WIDTH * 0.73,
y => IMG_HEIGHT * 0.5,
halign => 'center',
valign => 'top',
font => FONT,
size => FONT_SIZE_TIME
);
$img->write(data => \my $data, type => 'png') or die $img->errstr;
return $data;
}
sub api2_cisco_et {
my $tp = localtime;
my $data = &cisco_et_png($tp);
return ["200", ["Content-Type" => "image/png"], [$data]];
}
builder {
mount '/api2/cisco_et.png' => \&api2_cisco_et;
}