PerlでCisco Unified IP Phone Services Application

# はじめに

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系以外のディストリビューションをお使いの方は別途ググってください。(などと)

```shell:commands
sudo apt install fonts-noto-mono
sudo apt install cpanminus
sudo apt install libfreetype6-dev
sudo cpanm Plack::Builder Imager
```

# エオルゼア時間をPerlで求める

以前[私が投稿した記事](https://qiita.com/CIB-MC/items/74ce2fbc5dd1d95fbc82)に掲載したルックアップテーブル法を用いたエオルゼア時間算出のC++コードを下記のようにPerlに移植しました。
ついでに(？)24時間制で時を求められるように変更しました。

```perl:eorzea_time
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:cisco_et.xml
<?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アプリケーションに渡されます。

```nginx:default
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アプリケーションをデーモン化する

```text:/etc/systemd/system/cisco_et.service
[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` にアクセスすると、次のような画像が表示されます。

![cisco_et.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2066798/1498ae8b-1f12-4830-fe99-906ae2c429b9.png)

また、PBXサーバー側でXML ApplicationのURLを設定すると実機がidle中、下記のような表示がなされます。

![DSC_2818.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2066798/3f369c58-a18c-ab97-5340-e2a1d8eacf1a.jpeg)

# さらに改造後

私の所属団体のRainyVillageのメンバーに見せたところ『こうしたらもっと面白くない？』となり、最終的にこうなりました。

<iframe width="459" height="816" src="https://www.youtube.com/embed/ws-fOLQ9Yt8" title="Eorzea Time Clock with dancing hrothgar anime on Cisco IP Phone #shorts" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# おわりに

明日の担当は [papixさん](https://qiita.com/papix) です。お楽しみに！


# Appendix:Perlコード全文(改造前)

```perl:cisco_et.psgi
#!/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;
}
```