echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【node.js】Google Home Notifierを使ってGoogle Homeにしゃべらせる

実は昨年末に、Google Homeを衝動買いしました。

始めは「なんか色々使えないかなあ...」と思っていましたが、購入してしばらくは、そんなに使用していませんでした。

が、先日Twitterで、マイクロソフトエバンジェリスト、千代田まどか(ちょまど)さんがこんなツイートをしているのを見まして。

「やっぱりこういうことができるんだ!面白そう!」って思い、さっそく(二番煎じもいいとこ)実行してみました。(僕は女の子が大好きなので、男の声にはしないですが…)

※これ以降の作業は、Windowsにインストールする前提で記載しています。

1.インストール

■node.jsのインストール
Google Homeにしゃべらせるには、Google Home Notifierというjavascriptのプログラムが必要です。
そしてGoogle Home Notifierを動かすには、node.jsが必要です。
というわけで、node.jsのインストールから始めます。

といっても、node.jsの公式サイト(https://nodejs.org/ja/)からインストーラーをダウンロードして、実行するだけですので、難しいことはないです。
※「推奨版」と「最新版」がありますが、「推奨版」が無難。

インストール後、下記コマンドをコマンドプロンプトで入力して、バージョンが表示されればOK。(ここを含めた全手順で、コマンドプロンプトは「管理者で実行」してください。)

> node --version

■node-gypのインストール
node-gypは「C++を用いた拡張機能を作るためのビルドツール的なもの」らしく、Google Home Notifierインストールに必要になります。
ここをちゃんとやらないと、Google Home Notifierのインストールでエラーが出まくるので、要注意です。(てか、僕はそれで1週間近くハマった…)

※ここはnode-gypのgithubのドキュメント(https://github.com/nodejs/node-gyp)に沿って説明をします。

まずインストールは、コマンドプロンプトで下記コマンドを入力すればOK。(ここは問題ないと思います)

> npm install -g node-gyp

そして公式サイトの「Option 1」に、
Install all the required tools and configurations using Microsoft's windows-build-tools using npm install --global --production windows-build-tools from an elevated PowerShell or CMD.exe (run as Administrator).
(訳:「PowerShellコマンドプロンプトで'npm install --global --production windows-build-tools'というコマンドを使用して、Microsoft's windows-build-toolsを介して必要なツールや設定情報をインストールしてください。(管理者権限で実行してね))

とある通りに、コマンドプロンプトから

> npm install --global --production windows-build-tools

と入力して、この作業を完了させます。

※なお「Option 2」の手順は「必要なツールや設定情報」を自分でインストールする方法です。「Option 1」の手順はそれをすべて上記コマンド一発で行えるので、「Option 1」の手順のほうが楽です。(実施するのはいずれか一方でよい)

最後に、
If node-gyp is called by way of npm and you have multiple versions of Python installed, then you can set npm's 'python' config key to the appropriate value:
(訳:node-gypをnpm経由で実行する場合で、pythonの複数バージョンがインストールされている場合、(下記コマンドのように)npmの'python'コンフィグの値を適切な値にしてください。)

> npm config set python /path/to/executable/python2.7

とあるので、この通りに設定します。(なお、node-gypはpython3.xでは動作しませんが、Option1の手順を実施すれば、別途2.xをインストールしてくれますので、問題ないはずです。)

※なお、公式ドキュメントに
If you have multiple Python versions installed, you can identify which Python version node-gyp uses by setting the '--python' variable:
(訳:もし複数バージョンのpythonがインストールされている場合、node-gypに下記コマンドでnode-gypで使用するバージョンを設定してください)

> node-gyp --python /path/to/python2.7

とありますが、僕が実施した際は、「--pythonオプションなんてないよ」と言われ、これは実行できませんでした。


次に使用方法ですが、
To compile your native addon, first go to its root directory:
(訳:あなたの環境のアドオンをコンパイルするために、まず初めに(プロジェクトの)ルートフォルダに移動してください。)

> cd my_node_addon

とあるので、まずはコマンドプロンプトの'cd'コマンドで、*.jsファイルを作成するフォルダに移動します。

次に、
The next step is to generate the appropriate project build files for the current platform. Use configure for that:
(訳:次に、プラットフォーム毎に適切なプロジェクトのビルドファイルを作成します。次の'configure'コマンドを使用します。)
> node-gyp configure

Auto-detection fails for Visual C++ Build Tools 2015, so --msvs_version=2015 needs to be added (not needed when run by npm as configured above):
(訳:VC++ビルドツールを自動検出ができなかった場合、'--msvs_version=2015'というオプションを付けてください。(上のコマンドでうまくいけば不要です))

> node-gyp configure --msvs_version=2015

とある通り、'node-gyp configure'コマンドを実行します。それで「VC++ビルドツールがない」という旨のエラーが表示されたら、下の'node-gyp configure --msvs_version=2015'コマンドを実施すればOKです。

なお、
Note: The configure step looks for the binding.gyp file in the current directory to process. See below for instructions on creating the binding.gyp file.
Now you will have either a Makefile (on Unix platforms) or a vcxproj file (on Windows) in the build/ directory. Next invoke the build command:
(訳:'configure'コマンドは'binding.gyp'というファイルをカレントフォルダ内から検索します。'binding.gyp'ファイルを作成するには、「build」フォルダ内に'vcxproj'ファイルがあると思うので(Unixでは'Makefile'ファイル)、下記の’node-gyp build’コマンドを実行してください。)

> node-gyp build

という記載もあるので、もし「binding.gypがない」とかいう旨のエラーが表示されたら、'node-gyp build'コマンドを一度実行してから、再度'configure'コマンドを実行すればよいと思います。

Google-Home-Notifierのインストール
で、ようやくメインのGoogle-Home-Notifierのインストールです。
といってもこれ自体は単純で、下記コマンドを実行するだけです。

> npm install google-home-notifier

※ここで「dns_sd.h」ファイルがない、というエラーが表示された場合、先に下記「Bonjour SDK for Windowsのインストール」を実行してから、再度上記コマンドを実行します。(エラーが出る場合、大量に出るはずです)

Bonjour SDK for Windowsのインストール
dns_sd.h」ファイルがないというエラーは、「Bonjour SDK for Windows」をインストールすることで解決できるので、これをインストールします。
これはApple Developerにあるので、ここからダウンロードしてインストーラを実行すればOKです。

【参考サイト】
http://kghr.blog.fc2.com/blog-entry-118.html
http://blog.livedoor.jp/sce_info3-craft/archives/9706909.html


2.プログラミング(ようやく)
で、実際のプログラムですが、公式サイト(https://github.com/noelportugal/google-home-notifier)のソースをそのままコピペで大丈夫かと思います。
でも英語なので、試しに上記【参考サイト】内のサンプルソースを「sample.js」とかいう名前でファイルに保存して、下記コマンドで実施させてみます。

※コード自体は非常に短いですが、特に問題なく動作すると思います。(同じネットワークにGoogle Homeがある事が前提。なお文字コードを「UTF-8」にする必要があるので、それだけは注意。)

> node sample.js

var googlehome = require('google-home-notifier');
var language = 'ja'; // ここに日本語を表す ja を設定

// ネットワーク内からGoogle Homeを見つけてくれる
googlehome.device('Google Home', language); 
// もし Google Home のIPアドレスを指定するなら、以下のスクリプトに置き換える
// googlehome.ip('xxx.xxx.xxx.xxx', language);

googlehome.notify('こんにちは', function(res) {
  console.log(res);
});

OK、問題ないです。

■続けてしゃべらせる
最後に「続けてしゃべらせる」ですが、たぶん最初に下記のようなプログラムを思いつくかと思います。

// ネットワーク内からGoogle Homeを見つけてくれる
googlehome.device('Google Home', language); 
// もし Google Home のIPアドレスを指定するなら、以下のスクリプトに置き換える
// googlehome.ip('xxx.xxx.xxx.xxx', language);

googlehome.notify('こんにちは', function(res) {
  console.log(res);
});

googlehome.notify('こんばんは', function(res) {
  console.log(res);
});
makeSpeak('こんにちは');
makeSpeak('こんばんは');

function makeSpeak(sentence) {

    // ネットワーク内からGoogle Homeを見つけてくれる
    googlehome.device('Google Home', language); 
    // もし Google Home のIPアドレスを指定するなら、以下のスクリプトに置き換える
    // googlehome.ip('xxx.xxx.xxx.xxx', language);

    googlehome.notify(sentence, function(res) {
        console.log(res);
    });
}

が、残念ながら、これはどちらもエラーになります。(下記のようなエラーが出ると思います。)
Error: mdns service already started
at Browser.start (c:\dev\js\google_home\node_modules\mdns\lib\mdns_service.js:30:11)
at Object.notify (c:\dev\js\google_home\node_modules\google-home-notifier\google-home-notifier.js:28:13)
at Timeout.makeSpeak [as _onTimeout] (c:\dev\js\google_home\google_home.js:37:16)
at ontimeout (timers.js:475:11)
at tryOnTimeout (timers.js:310:5)
at Timer.listOnTimeout (timers.js:270:5)

といっても1行目に思いっきり「mdns service already started」とある通り、要は最初の命令がすでに実施中なので、次の命令を実行できない、というだけです。

なので、例えば下記のように、前の命令が終了してから次の命令を実行するようにすれば問題ないです。(タスクキューに追加するとか、もっといい方法はあると思いますが、ここではそれは置いときます)

// 前のmakeSpeak()の終了を待つためにsetInterval()をする。
var count = 0;
var val = setInterval(makeSpeak, 7500);

function makeSpeak() {

    // Google Home Notifierの設定
    var googlehome = require('google-home-notifier');
    var language = 'ja'; // ここに日本語を表す ja を設定

    var sentence = '';

    switch(count)
    {
        // しゃべらせる内容
        case 0:
            sentence = 'あくしろよ';
            break;
        case 1:
            sentence = 'おう考えてやるよ';
            break;
        case 2:
            sentence = 'すいません許してください。何でもしますから';
            break;
        default:
    }

    count++;

    // ネットワーク内からGoogle Homeを見つけてくれる
    googlehome.device('Google Home', language);

    // もし Google Home のIPアドレスを指定するなら、以下のスクリプトに置き換え
    // googlehome.ip('xxx.xxx.xxx.xxx', language);

    // 実際にしゃべらせる
    googlehome.notify(sentence, function(res) {
        console.log(res);
    });

    // 全部しゃべったら終了。
    if(count > 2) {
        clearInterval(val);
    }
}

※なお、上記プログラムを実際に実行した動画が、下記になります(手ブレはご勘弁を。)

いやー、苦労しましたね。
でもこれで、Google Homeの使用幅が広がりました。
これからは、Google Homeどんどん使いたいと思います。

…てか、我ながら長い文章だなあ。

【Linux】ワイルドカードを変数に代入する方法

おととい(2018/01/22)は全国的に大雪で、東京や関東で、ものすごい数の人が駅で立ち往生を食らっている、という映像をニュースやSNSで見ました。

幸い、愛知県平野部は雪の影響はなかったですが、あの人だかりを見ると、やはり東京(というか関東)は僕にとって「(リモート等で)仕事する場所」ではあっても、「住む場所」ではないな、と改めて感じました。


で、本題

例えば、下記のような内容のテキストファイルから「ファイルパス&ファイル名を1行ずつ読み込んで、何か処理する」とします。

/mnt/c/users/makky12/,sample.txt
/mnt/c/users/makky12/,*
...

で、これを

while read line; do
    ##${line}は「ファイル一行の内容」。
    declare -a array
    array=(`echo ${line} | tr ',' ' '`)
    dir=$array[0]
    file=$array[1]
    echo "dir=${dir}"
    echo "file=${file}"
done < ${sample} 

とすると、例えばテキストの2行目の内容なら普通、${file}には'*'が入ると思いますよね。
ところが、実際は、

dir='/mnt/c/users/makky12/'
file='abc.txt'  ##←なにこれ?

ということが起こります。

僕も最初、意味が全く分からなかったんですが、色々調べてみて、ようやく分かりました。


ワイルドカードはまず展開される

と、いきなり結論を書きましたが、要はワイルドカード文字(「*」「?」など)があると、それを「文字列」ではなく「ワイルドカード」として認識してしまうので、この場合「(カレントディレクトリの)条件に一致するファイル」を取得してしまいます。(要は下記と同じ意味)

file=`ls *`

なので先述の例では「カレントディレクトリ内にあるファイル名」が変数fileに代入されてしまうのです。

※これは各種コマンドの引数に「ワイルドカードでファイル名を指定した場合」も同じです。例えば'cp'コマンドの場合

cp /mnt/c/users/makky12/* /mnt/c/users/makky12/sample/
## 例えば'a.txt,b,txt,c.txt'の3つがあった場合、下記と同じような意味になる。
cp [/mnt/c/users/makky12/a.txt /mnt/c/users/makky12/b.txt /mnt/c/users/makky12/c.txt] /mnt/c/users/makky12/sample/

なので、条件に一致した全ファイルを処理対象としてくれるわけですが、上記のように該当するファイルをすべて引数に設定してしまうので、

if [ ${#} -gt 2 ]; then
    echo "引数は2つまでです。"
    exit 1
fi

declare file=${1}
declare mode=${2}

上記のような処理があるシェルスクリプトで、

bash 〇〇.sh /mnt/c/users/makky12/* -r

と指定すると、一致したファイル数によっては(「〇〇.sh」以外の)引数が2個以上、となり「引数は2つまでです」と表示されてしまいます。


対策方法

■1:""で囲む

先程の例では$array[1]を「""」で囲みませんでしたが、これを

file="$array[1]"

と、「""」で囲ってあげると、ワイルドカード文字であっても、正しく変数fileに代入されます。

■2:'set -f'を使用する

'set -f'コマンドは「ワイルドカードの自動展開を無効にする」オプションです。
これを利用して

set -f
file=$array[1]
set +f

とすれば、やはりワイルドカード文字であっても、正しく変数fileに代入されます。
ただし、当然'set -f'コマンド実行後はずっと「ワイルドカードの自動展開が無効」なので、使用後はすぐに'set +f'コマンドで元に戻さないと、予期せぬバグの原因になったりするので、要注意です。

■ちなみに...
上記の処理をしても、僕はまだ不安なところがあったので、実際のソースでは

set -f
file=`echo "$array[1]"`
set +f

としました。(意味があるかは不明ですが、念のため)

・先述の例は「変数に代入する場合」の話ですが、直接コマンド(の引数など)で実行する場合、先述の方法の他に

## 「''」で囲む方法
$ echo '*'

## 「\」を使う方法
$ echo \*

なども使用できます。

参考URL:
qiita.com
・sh での変数とワイルドカードの落とし穴(https://www.ecoop.net/memo/archives/2006-02-02-1.html


シェルスクリプトは便利な反面、こういう一見分かりにくい動作をする事もあるので、気を付けなくてはなりませんね。


終わり!閉廷!以上!解散!

【Linux】パーミッションによるコマンドやスクリプトの動作の違い

昨日(2018/01/20)、豊橋市で行われた「ブイアールサンダー」というVR(XR)関連のイベントに参加しました。
XR関連の知識が得られたのも大きいですが、何より大学時代の思い出の地、豊橋でIT関連のイベントが開かれたというのが嬉しかったです。

僕は豊橋市民ではないですが、豊橋市のこういうイベントには積極的に参加&協力したいな、と心から思いました。(てか、懇親会に参加できなかったのが、今でも悔やまれる...)
uzura.doorkeeper.jp

さて、10月から参画しているプロジェクトでガッツリUnixLinuxに触れることになり、その関係でシェルスクリプト(shやbash)もバリバリ書いています。
最初は戸惑いましたが、決して難易度は高くないし、慣れると快適ですね。
これからWindows Subsystem for Linuxもあることだし、Linuxに触れる機会もさらに多くなるでしょう。

で、先日仕事でシェルスクリプトを書いてて、下記の処理を書きまして(てかはてなブログって、'bash'はないのか…)

if [ -d ${dir} ]; then
    ## ディレクトリがある場合は書き込み権限のチェック
    if [ -w ${dir} ]; then
        echo "書き込み権限がある"
    else
        echo "書き込み権限がない"        
    fi
else
    ## ディレクトリがない場合
    echo "ディレクトリがない"
fi

これをあるディレクトリ(もちろん実在)で動かしたら、まあ上二つのどちらかだと思ったんですが、結果は

"ディレクトリがない"

だったんです。

で、「???」と思って調査したところ、どうやら'-d'や'-f'などはパーミッション次第では、実際のディレクトリ有無と違う動作になるらしく。
もっと調べると、ファイル&ディレクトリ関連のコマンドも、パーミッション次第で動作結果が異なる事がわかりました。

じゃあ...ってわけで、仕事が暇だったので実際に調べてみました。

その1.コマンド
※調べたコマンドは下記の通り

  • ls(フォルダなどの一覧表示)
  • cp(ファイルのコピペ)
  • mv((今回は)ファイルのカット&ペースト)
  • rm(ファイル削除)
  • touch(ファイルの新規作成)


【結果】

f:id:Makky12:20180121181816p:plain
lsコマンド
f:id:Makky12:20180121183141p:plain
cpコマンド
f:id:Makky12:20180121183151p:plain
mv&rm&touchコマンド

【結果】

  • 新規作成&削除は、対象ファイル自体の権限は全く関係なし。(ディレクトリの権限は「中のファイル自身」ファイルの権限は「そのファイルの中身」を扱う権限、ということなのかも)
  • 'cp'コマンドは、ちゃんとファイルの'r'権限も必要(「中身をコピーするから」という意味合い?)
  • 'mv'コマンドは、最初は'cp'と全く同じと思ったけど、「切り取り」は'rm'、「貼り付け」は'cp'と同様と考えると、確かに納得。


その2(本題).シェルスクリプト
※調べたスクリプトは「-d(ディレクトリ有無)」と「-e(ファイル有無)」で、下記スクリプトで確認。
※${dir}や${file}は確実に存在する、という前提。

if [ -d ${dir} ][ -e ${file} ]); then
    echo "true"
else
    ## ない場合
    echo "false"
fi


【結果】
※どちらも「対象ディレクトリ/ファイルの親フォルダ」の権限

f:id:Makky12:20180121181805p:plain
ファイル&ディレクト
【結論】

  • こちらも、そのディレクトリ&ファイル自体の権限は全く関係なし。(親ディレクトリの権限に依存)
  • '-e'は、0(≒true) or 1(≒false)だからまだいいけど、'-d'は権限がないとエラーになるから、判定が大変かも。(初めに'-d'('-e')の${?}(=ステータス)でエラー判断→エラーがなければ、次に実際の有無判断?でも'-d'('-e')を2回実行するのは、あまりスマートじゃないような...)
  • いずれにしろ、適切な権限がないと、実際のファイル有無と違う判定をする可能性がある。


結論として、ファイル関連の各種コマンドやシェルスクリプトの実行時には、パーミッション(特に親フォルダの)には要注意、という話でした。

終わり!閉廷!以上!解散!


■余談
 冒頭に書いた「ブイアールサンダー」の件ですが「半田市から…」と言ったら、めちゃくちゃ驚かれました。
豊橋の人からしたら、そんなに遠いですかね、半田~豊橋って。(今では一般道でも約1.5時間ですからね。昔はr41がなく、R247で行くしかなくて、2時間かかってましたから。早くなったものです。)

お久しぶりです。

いや、本当にお久しぶりです。
てかこれの前に書いた記事って、去年の5月なんですね…サボりすぎ。

といっても、別にサボってたわけじゃないんです。
ブログを書けなかったのには、理由がありまして...

忙しかった

ええ、忙しかったんです...

って、理由になりませんよね。
だって、普通に技術者として働きながら、ブログ書きまくってる人だって多いんですから。

ネタがない

まじめな話、これがメインでした。
いや、もちろん仕事ではいろいろ扱っているんですよ。確か去年の5月の時は、
Microsoft Dynamics CRMを扱ってたかな。
でも、仕事で扱う技術って、業務上の情報を扱うだけあって、
ネタにしにくいんです...
てかDynamics CRMって、個人で「今日は休みだし、勉強するか」っていうソフトでもない(と思う)し...

あと、はてなブログって「技術のこと」を書かないといけない、って思い込みが強かったんです。
別にそんな規約はないんですが、「はてなブログ」って言ったら、やはり「技術系」っていうイメージでしょ?
技術以外のことでのネタはあったのですが、はてなブログに書くのもちょっとな...って感じで

そんなこともあって、ちょっと(ていうか、かなり)ブログから離れてました。

発信なくして成長なし

ですが、やはり周り(主にTwitterのフォロワーさん)が技術系ブログ書いて情報発信しているのを見て、「今の自分って、なんか”口だけ野郎”みたいだ...」って、最近すごく思ったんです。
そういう人は、仕事での技術じゃなくても、積極的に自分で勉強して、記事にしている訳ですから。

それに、フリーランスのエンジニアである以上「自分らしい働き方」をするためには、自分から「自分はこんなに色々出来るんですよ!」ってのを、(多少強引にでも)アピールしていかなきゃいけない。
そうじゃなきゃ、いつまでたっても「自分らしい働き方」を実現できない。
ずっと常駐勤務でプロジェクト先へ...って、
それSESと何か違うの?って話ですし。
そのためには、ブログなり自分のサイトとかから、
いろいろ発信していくことも大事。
(リアルで会って、人脈を増やすことも、もちろん大事ですけどね)

ネタが増えてきた

それに、最近は(10月から新プロジェクトに移って)ネタも増えたし、休日にもちょくちょく勉強するようになったので、いろいろ発信できるネタも増えました。
それにツイッターでは、「働き方」「自己実現」とか、その辺についても結構ツイートするようになりましたし。
そうなると、やはりブログでも発信したい!...って思ってしまうわけです。


そんなわけで、「口だけ野郎」だった去年の自分への戒めも込めて、今年は少しでも多く、ブログを書いていこうと思います。
よろしくお願いいたします。

蛇足1:去年の出来事(技術系)

・Dynamics CRMやknockout.jsのスキルを習得した
・10月から新プロジェクトに移って、Linuxシェルスクリプト(sh、bash)を
 割と書けるようになった。
 ワタシLinuxチョットデキル
・モバイルPC、(Acerの)VRヘッドセット、そして遂にMacBookを購入。
(昔から「アンチマック」派だったのにね)
・VRヘッドセットを実際に動かして、3D酔いに極度に弱いことが発覚。
 もって10分がやっと...

蛇足2:去年の出来事(プライベート)

・去年5月からダイエットを始め、約10か月で20kgのダイエットに成功!
・その影響で、筋トレ(ボディメイク)が趣味になる。
(今は週6で筋トレしてます)
・去年9月に日光東照宮に旅行に行ったことがきっかけで、寺社仏閣巡り
ご朱印集め)にハマる。
 鬼怒川も最高だった。栃木いいとこ!

Masto.netを使った簡単なMastodonのテスト

GWは9連休!…とか思っていたら、気づいたらもう半分過してしまい、若干凹んでいます。
働きたくないでござる

…さて、最近ネットやらTwitterで、やたら「Mastodon」という言葉を目にするようになりました。
ちなみに「Mastodon」の特徴を簡単に説明すると

てな感じです。

で、APIも公開されていて、いろんな人が各環境でのライブラリをすでに作っているわけで、あすかさんのブログにも、こんなものが紹介されていました。
kmycode.hatenablog.jp

「Mastonet」という、Guillaume Lacasa氏というフランス人の方が作成したC#用のMastodonライブラリです。
github.com

というわけで、どうせ寝てるだけだしMastodonの理解もかねて、僕も作ってみようと思いました。


Mastonetのインストール
Visual Studio使ってるなら、NuGet経由でインストールできます。
ただしGithubのREADME.md内にも「A preview version is available on Nuget : https://www.nuget.org/packages/Mastonet
とある通り、「プレリリースを含める」チェックボックスにチェックを入れないと検索されないので、それだけは注意です。

とりあえずビューとソース本文
f:id:Makky12:20170503192330p:plain

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using Mastonet;
using System.Diagnostics;

namespace MastodonApp
{
    public partial class Form1 : Form
    {
        private String timeLine = String.Empty;
        private TimelineStreaming stream = null;

        private enum StreamStetus
        {
            Not_Login = 0,
            Not_Start = 1,
            Now_Starting = 2,
        }

        private StreamStetus stream_status = StreamStetus.Not_Login;

        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            var id = textBox1.Text;
            var pass = textBox2.Text;

            if (String.IsNullOrEmpty(id) || String.IsNullOrEmpty(pass))
            {
                MessageBox.Show("IDまはたパスワードが未入力です。");
                return;
            }

            try
            {
                var ret = await this.MastoNet_Run(id, pass);

                timer1.Interval = 5000;
                timer1.Enabled = true;
                timer1.Tick += new System.EventHandler(timer1_Tick);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }

        private Task<int> MastoNet_Run(String id, String pass)
        {

            return Task.Run(async () =>
            {
                if (stream_status != StreamStetus.Now_Starting)
                {
                    if (stream_status == StreamStetus.Not_Login)
                    {
                        var authClient = new AuthenticationClient("friends.nico");
                        var appRegistration = await authClient.CreateApp("MastodonApp", Scope.Read | Scope.Write | Scope.Follow); 
                        var auth = await authClient.ConnectWithPassword(id, pass);

                        var client = new MastodonClient(appRegistration, auth);
                        stream_status = StreamStetus.Not_Start;

                        stream = client.GetPublicStreaming();

                        var dispString = String.Empty;

                        stream.OnUpdate += (sender, e) =>
                        {
                            var dispName = e.Status.Account.DisplayName;
                            var content = e.Status.Content;

                            dispString = dispName + "\r\n" + content + "\r\n" + "\r\n";
                            this.timeLine += dispString;
                        };
                    }

                    stream.Start();
                    this.stream_status = StreamStetus.Now_Starting;
                }
                return 0;
            });
        }

        private void timer1_Tick(object sender, System.EventArgs e)
        {
            this.textBox3.Text = this.timeLine;
        }

        protected void button2_Click(object sender, EventArgs e)
        {
            if (stream != null && this.stream_status == StreamStetus.Now_Starting)
            {
                stream.Stop();
                this.stream_status = StreamStetus.Not_Start;
            }
        }
    }
}

と、ビューとソースを記載しましたが、重要なのはソースの「MastoNet_Run()」の内部です。

  • アプリ登録
var authClient = new AuthenticationClient("friends.nico");
var appRegistration = await authClient.CreateApp("MastodonApp", Scope.Read | Scope.Write | Scope.Follow); 

クライアント作成には「ClientID」「ClientSecret」という2つの値が必要で、これを取得するためにアプリ登録を行います。
アプリ登録は上記の通り、MastodonインスタンスのAuthenticationClientクラスを作成した上で、そのCreateApp()メソッドを実行すればOKです。

  • 認証とクライアント作成
var auth = await authClient.ConnectWithPassword(id, pass);
var client = new MastodonClient(appRegistration, auth);

認証は、OAuthを使用する方法と、メアドとパスワードで行う方法があり、後者は上記の通りauthClient.ConnectWithPassword()メソッドを実行すればOK。
※ただし後者は作者曰く「非推奨」とのことなので、問題なければOAuthを使ったほうがよいみたいです。

前者の場合、作者のREADME.mdによれば

  1. AuthenticationClientクラスのOAuthUrl()メソッドを実行して、urlを取得
  2. 1で取得したurlにアクセスして、APICodeを取得
  3. 2で取得したAPICodeを引数にして、AuthenticationClientクラスのConnectWithCode()メソッドを実行

を実行すればOK…のはずなんですが、なぜか僕の環境ではうまくいかなかったので、今後の宿題にします。*1

で、クライアント作成ですが、ここまでで実行したCreateApp()メソッドの戻り値とConnectWith***()メソッドの戻り値を引数にして、MastodonClientクラスのインスタンスを作成するだけです。

  • タイムラインのストリーミングと更新通知
stream = client.GetPublicStreaming();
var dispString = String.Empty;

stream.OnUpdate += (sender, e) =>
{
    var dispName = e.Status.Account.DisplayName;
    var content = e.Status.Content;
};

タイムラインのストリームは、先程作成したクライアントに各タイムラインストリーム取得用のメソッドが用意されているので、それを使用します。
(今回はGetPublicStreaming()メソッドで、連合タイムラインのストリームを取得しています。)
で、更新された場合、ストリームのOnUpdate()イベントハンドラーが呼ばれるので、その中に追加でやりたい処理を記載します。

なお、上記の通り、引数e(StreamUpdateEventArgs)の

  • Status.Account.DisplayNameに「ユーザー名」
  • Status.Contentに「本文」

が取得できます。

最後に、ストリームのstart()メソッドで、実際にストリーミングを開始します。(なおご想像の通り、stop()メソッドでストリーミングの停止ができます。)
※この時「この呼び出しを待たないため、現在のメソッドの実行は…」という警告が出るかもしれませんが、無視してOKです。

なお、上記ソースの実行結果はこちら。*2
f:id:Makky12:20170503200012p:plain
まあ、いろんな方がブログなどで書いていたので知っていましたが、本文がHTMLタグ付きなんですよね…
本来はその対処も追加しないといけないですが、今回はまあいいでしょう。

というわけで、Masto.netを使用した、簡単なMastodonアプリの作成でした。
せっかくのGW、いろんな方がMastodonライブラリを作成されているので、機会があればMastodonアプリやライブラリの作成に挑戦するのもいいかもしれません。

さて明日からは、そろそろ本腰入れてXamarinプロジェクトを再開しないと。

…なんか普段より、むしろGWのほうがコーディング時間が長いような???

*1:これに限らず、インスタンス環境に依存する?事項がなんか多かった気がします。
初めに使用したインスタンスでは、url&API Codeの取得は問題なく出来たんですが…

あと、初めに使用したインスタンスではCreateApp()メソッド実行時に、強制的に接続が切断されていたのですが、friends.nicoインスタンスにしたら、ソースは1行も変えてないのに、全く問題なくなった…とか。これらは今後、要調査です。

*2:他の方がコンソールアプリでやっていたため、僕はフォームアプリで作成しましたが、手っ取り早く試すだけならコンソールアプリがおすすめです。
フォームアプリだどスレッドセーフとか非同期処理時のフォームコントロールプロパティ設定とかの関係で、コンソールアプリより少々厄介です。

knockout.jsでMVVMを実装 その1

4月から新しいプロジェクトにアサインしたのですが、早くも「C#やれるって話だったのに全然やれないじゃん!話が違う!」みたいな事になってます…

でも「Xamarinやりたい!XAMLみたいにMVVMやりたい!」とか思ってたら、(C#ではないですが)「knockout.js」というJavaScriptフレームワークでMVVMを使用することになったので、復習がてらメモ。

knockout.jsとは?

  • JavaScriptのクライアントサイド MVVMフレームワーク
  • AngularJSに比べて、簡潔でとっつきやすく、敷居が低い…かも。(ただし簡潔だから良いというわけでもないので、「AngularJSより優秀だ!」なんて言うつもりはない。)
  • jQueryとは違い、HTML内の各コントロールはDOMで操作する。

 ※5/12訂正:DOMを操作するのではなく、バインドした変数を使って操作します。すいません。

公式サイト
日本語版ドキュメント(非公式)


インストールと実行
…とは書いたものの、別に「インストール」って程でもなく、jQueryなどと同様、knockout.js本体にscriptタグでリンクするだけ。
リンクもCDN形式でも良いし、公式サイトからダウンロードしたファイルへのパスを通してももちろんOK。

では、百聞は一見にしかずって事で、ソースをば。
なお面倒くさいソースが短かったので、*.htmlファイル内に直接スクリプトも書いてますが、本当はファイルを分けたほうが良いと思います。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="./knockout-3.4.2.js"></script>
<script>
window.onload = function() {

    var self = this;

    var myViewModel = function() {
        self.myText = "Hello kockout.js!";
        
        onClick = function() {
            alert('Button is clicked.');
        };
    };

    var vm = new myViewModel();
    ko.applyBindings(vm);
};
</script>
</head>
<body>
<input type="text" data-bind="value: myText"><button data-bind="click: onClick">button</button><br>
<span data-bind="text: myText"></span><br>
</body>
</html>

とりあえず、HTMLタグ内に「data-bind」とかいう、明らかに見慣れない属性が目につきますが、これがknockout.jsのデータバインドの仕組みです。
この「data-bind」内で指定したプロパティ(value,text等)に、data-bind内で指定した変数(ここではmyText)に値を入れて、データバインドを実装しています。

で、JavaScriptの方を見ると、まず

var myViewModel = function()

という文がありますが、この「myViewModel」がいわばMVVMのViewModelの本体で、この中でHTML内の変数など、色々な定義をします。

そしてその中に

self.myText = "Hello kockout.js!";

とありますが、ここで「HTML内のmyTextに'Hello kockout.js!'を代入しなさい」という処理を実行しているわけです。

そしてmyViewModelの定義が終わったあとで、最後に

var vm = new myViewModel();
ko.applyBindings(vm);

として、myViewModelをapplyBindingsメソッドの引数に指定してますが、こうすることで実際にバインディングが実行されます。
f:id:Makky12:20170428204825p:plain

なお、HTMLの部品をDOMで操作するので、スクリプトは「window.onload()」や「document.ready()」など「すべての部品が読み込まれた」段階で実行するようにして下さい。
あと、applyBindingsメソッドの前の「ko」ですが、これはknockout.jsのグローバルオブジェクトで、knockout.js固有の処理は、この「ko」を介して実行します。


もちろん双方向バインディングも可能
ただ、鋭い方は気づいたかもしれませんが、これだとVM→Viewの一方通行です。
実際、このあとテキストボックスの値を変えても…
f:id:Makky12:20170428204832p:plain

はい。
テキストボックスは'Hello Xamarin!'に変わったのに、ラベルは'Hello knockout.js'のままです。
つまりView→VMバインディングが行われていません。

といっても、別にchangeイベントとかは全く必要なく、スクリプトを1行変えるだけで解決します。

window.onload = function() {

    var self = this;

    var myViewModel = function() {
        // self.myText = "Hello kockout.js!";
        self.myText = ko.observable("Hello kockout.js!");
        
        onClick = function() {
            alert('Button is clicked.');
        };
    };

    var vm = new myViewModel();[f:id:Makky12:20170428210141p:plain]
    ko.applyBindings(vm);

    alert("loaded.");
};

上記の通り、「self.myText =」の右辺を「ko.observable("Hello kockout.js!");」に変えるだけで、双方向バインディングの完成です。
ko.observable()メソッドの戻り値を変数に入れる事で、その変数はどこで変更されても、その変更がリアルタイムにViewやVMなどに反映されます。
※ちなみに、引数は初期値になりますので「デフォルトは未入力」という場合は、引数に何も指定しなければOKです。

実際、今度はちゃんとテキストボックスの値とラベルの値が連動します。
f:id:Makky12:20170428210141p:plain

ちなみに、ko.observable()を実行した変数の値の取得と設定(=getter,setter)ですが

var getter = vm.myText();  // getter
vm.myText('Hello C#!');    // setter

て感じで、[ViewModelの変数名].[変数名(引数なし)]だとgetter、[ViewModelの変数名].[変数名(引数あり)]だとsetterです。
(setterの場合、引数に指定した値が代入される。)

イベントについて
最後にイベントですが、まあ'button'コントロールにこれ見よがしに「data-bind="click: onClick"」なんて書いてるので、おおよそ見当はついたと思います。
まあそんな感じで「data-bind」に「イベント('click'など):関数 or 変数名」として、スクリプト内でそれに対応した関数を作成するだけです。

ちなみに(もうお分かりとは思いますが)、実行結果はこんな感じ。
f:id:Makky12:20170428211550p:plain

ただし、公式サイトを見る限り、こんな感じで直接イベント名を記載できるのは、'click'だけのようです…残念。(というか、それなら「イベント」じゃなくて「クリックイベント」としたほうが良かったような…)

※ただしchangeなど、その他のイベントも、実装する方法はもちろんありますので、ご安心を。(それについては、後日記載予定です。)

とまあ、かなり駆け足でknockout.jsの概要を記載しましたが、いかがですか?
せっかくのGW、ちょっと気になった方がいたら、試してみるのも良いかもしれません。(割と導入は簡単ですから。)


…でもやっぱり、C#は…いいぞ。(結局それ)

Visual Studio 2017でXamarinを動かしてみた。

ていうか、前回の記事から50日…
いくら仕事で体調が優れないからって、ちゃんと定期的に何か書かないと…

だれか、終業後も元気でいられる方法、教えてください…

Visual Studio2017正式リリース
先日、マイクロソフト本社で開催された、Infragistics Day 2017 Springに参加して、改めて「Xamarinは、いいぞ」って再認識したのですが、同時期にVisual Studio 2017が公開されました。

そして昨日、エクセルソフトの田淵さんが、こんな記事を公開されました。
ytabuchi.hatenablog.com

そこで、この記事を参考に、Visual Studio 2017でXamarinを動かしてみました。


ダウンロードとインストール
まずは公式サイトからインストーラーをインストールします。(今回は無料のCommunity版を選びましたが、もう少しビジネスが軌道に乗ったら、Pro版買いたいなあ。)

で、インストーラーを起動すると、下記画面になります。
f:id:Makky12:20170318212441p:plain

この中の「.netによるモバイル開発」にチェックを入れ、右の「概要」では下記項目にチェックを入れました。(個々の詳細については、先程の田淵さんの記事に詳しく説明がありますので、そちらを参考にされるとよいと思います。)

  • Xamarin Workbooks
  • Android SDK セットアップ (レベル23)
  • Java SE Development Kit (8.0.xxx.xx)
  • F# 言語サポート
  • Xamarin用ユニバーサルWindowsプラットフォーム

※F# 言語サポートは、必要ないなら不要かも。(僕は一応入れました)
※「Xamarinインストール&実行が重い!」と言われる根源である「Google Android エミュレーター」ですが、Android実機があり、Visual Studioで実機デバッグできる環境がある(作る)なら不要です。(実機デバッグ環境についてはこちらの「2.実機デバッグ環境構築」を参照)
またこれを入れないなら「Intel Hardware Accelerated Execution Manager (HAXM)」も必須ではないです。


実際に動かしてみる
インストールが完了したら、早速Visual Studio 2017を起動。(結構スタートページのデザインは変わっています。)
「新しいプロジェクト」から、[Visual C#]-[Cross-Platform>-[クロスプラットフォームアプリ (Xamarin.Forms またはネイティブ)]を選択します。

で、とりあえず何もせずにビルド&デバッグ
f:id:Makky12:20170318212502j:plain

動いた!

あれ?でも以前と違って、ContentPageをNavigationPageに(引数で)渡してないぞ?
というか、MainPageクラスのコンストラクタに、引数自体がない…

…という理由で焦ったので、少しソースコードを追ってみた所、違いが判明。
(上が前のソース、下が今回のソース)
f:id:Makky12:20170318212520p:plainf:id:Makky12:20170318212528p:plain

なるほど、今はContentPageを直書きではなく、MainPageクラス側でXAMLなどでガッツリContentPageを作り込んで、それをMainPageに渡すのか。
確かに、XAMLで作り込める分、この方が良いかも。
Infragisticsさんから4月に便利なツールも発売されるみたいですし。

※もちろん以前のように、ContentPageの直書きでもOKです。下の画像でコメントアウトしたソースのように、MainPage変数にContentPageのインスタンスを渡してあげるだけです。(↓その結果)
f:id:Makky12:20170318212512j:plain


Visual Studio 2015のプロジェクトを動かす
最後に、Visual Studio 2015のXamarinプロジェクトを直接開くと、こんなメッセージが表示されました。
f:id:Makky12:20170318212537p:plain

気にせずそのまま[OK]をクリックしたら、こんなレポートが表示されました。
f:id:Makky12:20170318212549p:plain

どうやら、Windows Phoneプロジェクトの移行に失敗したようですね。でも肝心のAndroidiOSプロジェクトは成功したようなので、問題ないでしょう。

このプロジェクトをビルド&デバッグすると、何も問題なく実機で動作しました。
(結果はさっきの画像と同じなので省略。)

あ、ちなみに実機デバッグ環境ですが、Visual Studio 2015で設定済ならば、特に何もしなくても、Visual Studio 2017で何の問題も無く動作しました。
(実機デバッグ環境についてはこちらの「2.実機デバッグ環境構築」を参照。←2度め)


それではみなさん、素敵なXamarinライフを!

最後に一言。


「Xamarinは、いいぞ。」

【2017/3/19追記】
このあとIoT(ラズパイ3)のプロジェクトをVisual Studio 2017で動かしたのですが、これについては変換のメッセージすら表示されることなく、Visual Studio 2015で作成したプロジェクトが全く問題なく動きました。
(「ユニバーサルWindowsプラットフォーム開発」や「Windows Core開発」あたりにチェックを入れればいいのかな?ちなみに「概要」はデフォルトのまま変えてません。)

今までのVisual Studioは、大抵リリース直後は何かあったのでインストールを見送っていたのですが、今回は今のところいい感じです。

ちなみに、合計40GB強のインストールに、下記スペックのPCで2時間ほどかかりました。参考までに。(Xamarinだけなら、こんなに容量ありません。)

■インストール先のスペック

  • OS:Windows10 Pro(64bit)
  • CPU:Intel Core i7-7500U
  • メモリ:8GB
  • HDD:1TB(約850GB空き)