Cisco Unified IP Phone Services Application でエオルゼア時計を作る

# はじめに
唐突ですが Cisco IP Phone 7965 を買いました。
調べてみると、XMLアプリケーションを開発すればIdle画面をカスタマイズできるとのこと。
これはエオルゼア時計を作るしかないですね！(またか)

# 下調べ

まずは下記の公式資料を見てみましょう。

Cisco Unified IP Phone Services Application Development Notes
https://www.voipinfo.net/docs/cisco/xsi_dev_guide.pdf

……思ってたより実装の自由度が低いですね。
(個人の感想です)

表示にこだわりたいのでサーバ側で動的に画像を生成して、公式資料の中にある「CiscoIPPhoneImageFile: 3-9」を使って表示するのがよさそうです。

# 開発

次のものを用意しましょう。
- Linux環境
- Node.js + Express
- nginx

## Node.js+Expressで時計画像を作る
まずは簡単にラベルとエオルゼア時間表示だけで作ってみましょう。

IP Phone 7965で「WindowMode：Wide」の場合は「320 x 156」ピクセルの画像まで表示できます。
それ以上のサイズの画像ファイルを出そうとすると表示エラーになります。(なりました)
※併せてnode-canvasを利用しています。別途インストールしてください。

```javascript:web.js
// ============================================================ #
//  Copyright (c) 2024 CIB-MC
//  Released under the MIT license
//  https://opensource.org/licenses/mit-license.php
// ============================================================ #

const express = require('express');
const app = express();
const createCanvas = require('canvas')

app.get('/api/et.png', (req, res) => {
    const canvas = createCanvas(320, 156);
    const ctx = canvas.getContext('2d', {pixelFormat: 'RGB24'});
    ctx.fillStyle = "#000";
    ctx.fillRect(0,0,320,156);

    let dt = new Date();
    dt.setMilliseconds(dt.getMilliseconds() + 500);
    let edt = new EorzeaDatetime(dt);
    
    ctx.fillStyle = "#fff";
    ctx.textAlign = 'center';
    ctx.font = "normal 30px monospace";
    ctx.fillText('Eorzea Time', 160, 40);
    ctx.font = "bold 60px monospace";
    ctx.fillText(appendZero(edt.getHours(), 2) + ':' + appendZero(edt.getMinutes(),2), 160, 120);

    let buffer = canvas.toBuffer("image/png");

    res.set('content-type', 'image/png');
    res.end(buffer);
});

class EorzeaDatetime {
        weekepoch = 0;
        constructor(datetime) {
                var dt = datetime ? datetime : new Date();
                var dt_jst = new Date(dt.toLocaleString({ timeZone: 'Asia/Tokyo' }))
                var dt_ws = new Date(dt_jst.getFullYear(), dt_jst.getMonth(), dt_jst.getDate() - dt_jst.getDay(), 0, 0, 0, 0);
                this.weekepoch = (dt_jst.getTime() * (1440 / 70)) - (dt_ws.getTime() * (1440 / 70));
        }
        getHours() {
                return Math.floor(this.weekepoch / 3600000) % 24;
        }
        getMinutes() {
                return Math.floor(this.weekepoch / 60000) % 60;
        }
}

function appendZero(str, digits) {
    str = '' + str;
    while (str.length < digits) {
        str = "0" + str;
    }
    return str;
}

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});
```

## XMLアプリケーションファイルを準備する

こっちは画像を表示するだけなので短いです。

```xml:et.xml
<?xml version="1.0" encoding="utf-8"?>
<!--
============================================================
 Copyright (c) 2024 CIB-MC
 Released under the MIT license
 https://opensource.org/licenses/mit-license.php
============================================================
-->
<CiscoIPPhoneImageFile WindowMode="Wide">
<Title>Eorzea Time</Title>
<Prompt></Prompt>
<LocationX>0</LocationX>
<LocationY>0</LocationY>
<URL>http://172.16.0.●/api/et.png</URL>
<SoftKeyItem>
<Name>Cancel</Name>
<URL>SoftKey:Exit</URL>
<Position>3</Position>
</SoftKeyItem>
</CiscoIPPhoneImageFile>
```
※「172.16.0.●」はWebサーバのIPアドレスまたはドメイン等で置き換えてください。

## nginxの設定

公式資料にRefreshヘッダーを利用してXMLを自動読み直しするコード例があるのですが、Microsoft IIS用なのでnginxでは使えません。(それはそう)
代わりの設定を書いて、併せてNode.jsのAPIをproxy_passで繋いでおきましょう。

```nginx:default
# ============================================================ #
#  Copyright (c) 2024 CIB-MC
#  Released under the MIT license
#  https://opensource.org/licenses/mit-license.php
# ============================================================ #

server {
	server_name _;
        listen 80 default_server;
        listen [::]:80 default_server;

        root /home/user/web/static/;

        location ~ ^/(.*)\.xml$ {
                add_header Refresh "1";
        }

        location /api/ {
                proxy_pass http://localhost:3000;
        }
}
```

あとはPBX側で、IdleURLに作ったXMLを指定しておきましょう。

# 動作デモ動画

<iframe width="560" height="315" src="https://www.youtube.com/embed/gNEWHI1LPQc?si=OQt1kn0w5IWp4e2k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>