これまでのあらすじ:
2016年3月、フェルト生地を手で裁断している際にレーザーカッターがあれば複雑なカットが容易にできるなあと思って、安価になってきたレーザーカッターを購入しようと思ったのがきっかけ。調べていくうちに、合板も切れたほうがいいと思うようになって、CNCルーター(CNCミリング)についても考えるようになった。
Arduinoは以前から使っており、CNCシールドがあると気付いて自作も可能と思うようになった。当初はShapeOkoやX-CARVEを参考にMakerSlide、OpenRail、V-Wheel、2GTタイミングベルトなどで5万円くらいで自作しようと思っていた。AliExpressでも部品が安く買えることが分かって、しばらくは部品探し。探せば探すほど安くて本格的な部品も見つかってくるので、そんなにケチらなくてもいいのではないかと徐々にスペックアップ。最終的には剛性や精度のことも考えてボールスクリューやリニアスライドを使うことになり、予想以上に重厚な3軸CNCマシンをつくることに(約7万円)。
構想から約5週間(制作約3週間)でルーターとレーザーともに使えるようになり、現在はgrbl1.1+Arduino CNCシールドV3.5+bCNCを使用中(Macで)。余っていたBluetoothモジュールをつけてワイヤレス化。bCNCのPendant機能でスマホやタブレット上のブラウザからもワイヤレス操作可能。
その他、電子工作・プログラミング、最近は機械学習などもやっています。基本、Macを使っていますが、機械学習ではUbuntuを使っています。


CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:



*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。

2018年6月11日月曜日

Ubuntu 18.04:Gnome Shell Extensions自作:クリックでnvidia-smiの結果を表示させる

前回サンプルを改造して簡単なGnome Shell Extensionを自作しましたが、今回はトップバーからのドロップダウンメニュー表示にチャレンジしてみました。

今回つくったもの:
GPUの状態をチェックするための「nvidia-smi」というターミナルコマンドの結果をドロップダウンメニュー上に表示させる(結果は以下)


これまでは、わざわざターミナルを起動して「nvidia-smi」コマンドを入力して表示させていましたが、トップバー上のアイコンをワンクリックすればこのように表示されます。さらに表示中は1秒ごとに内容を更新するようにします。

今回ドロップダウンメニューやポップアップメニューのプログラムについては、このチュートリアルを参考にしました。どうやらトップバーからのドロップダウン形式のメニューをつくるには、もう部品化されたクラスを使うといいようです。
追記:
クラスを使わないコードを新たに追加しました。
追々記:
さらなる改良版を追加しました(最後の方)。


まずは前回同様ターミナルで、

gnome-shell-extension-tool --create-extension

を入力して対話形式でタイトルなど決めながら雛形を用意します。
そして先程のポップアップメニューのサンプルをコピペすると、


こんな感じの4種類のメニューを含んだドロップダウンメニューができあがります。
このサンプルは一通りのメニューパターンを網羅しているので今後も使えそうです。

しかしながら今回は、これらのメニューは必要なくて単なるドロップダウン形式で「nvidia-smi」の結果をテキスト表示させたいだけなので、この表示ウィンドウだけ利用させてもらいます。主には、下から3番目の文字表示してある部分「PopupMenuItem」以外消してしまいます。
それからトップバーにも虫眼鏡アイコン、文字、▼の3種類表示していますが、できれば自作アイコンだけを表示させたいと思います。


自作アイコンの表示:
そのままアイコンのパスを書けばいいのかと思いましたが、ここにやり方が書いてありました。チュートリアルやサンプルが少ないためにあいかわらず探すのが大変です。
自作アイコンはメインスクリプトと同じディレクトリに入れておいた場合、

const St = imports.gi.St;
const Me = imports.misc.extensionUtils.getCurrentExtension();
const Gio = imports.gi.Gio;

let gicon=Gio.icon_new_for_string(Me.path + "/my_icon.png");

const icon = new St.Icon({ gicon: gicon, style_class: 'system-status-icon'})

となるようです(結構面倒)。そのままだと歪んでしまうので、最後に「style_class: 'system-status-icon'」を付け加えておくとトップパーにフィットしてくれます。


「nvidia-smi」のコマンドを送るコード:
コマンドを送るコードについては前回も使った、

let smi = GLib.spawn_command_line_sync("nvidia-smi").toString();

になります。そのまま「nvidia-smi」というコマンドを()内に書けばいいだけです。このコマンドを送れば、その結果として変数smiに文字列が代入されます。得られた文字列から必要な値だけを正規表現で抜き出して新たに書き直してもいいのですが面倒なので、そのまま結果を表示させることにしました。
追記:
そのまま「nvidia-smi」の返り値を「smi」に代入すると、先頭に「true,」と最後に「,,0」という文字が挿入されてしまうので、以下のように変更しました。
let smi = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();

返り値は配列になっており、配列[0]には応答が成功した場合の「true」が含まれるようです(ダメなら「false」)。同様に[2]以降の「,,0」も不要なことから、[1]が実際に必要な文字列となるため上記のように変更しました(以下のコードも変更しておきました)。


このように余計な返り値を取り除き、必要な部分だけ表示されるようになりました。細かい部分ですが、少しずつわかってきたという感じです。


文字列を表示させるコード:
「nvidia-smi」で得た文字列を表示するには、

this.menuitem = new PopupMenu.PopupMenuItem(smi, {style_class:'smi-label'});

今回はこの部分に代入してみました。()内の「smi」が文字列になります。同時にCSSのスタイルを反映させるために「style_class」も記入しています。「smi-label」というのは、外部ファイル「stylesheet.css」内に記入してあるクラスです。

この「stylesheet.css」には、フォントの指定とフォントサイズの指定が書いてあります(以下)。

.smi-label {
  font-family: monospace;
  font-size: 15px;
}

フォントを等幅フォントのmonospaceなどに指定しないと、「nvidia-smi」から得た文字列が崩れてしまうので要注意(以下、崩れてしまった場合)。


表形式で文字列が出力されるために等幅フォントを使用しなければいけないということです。
背景色などはシステムに使用されているものと同じものが反映されるので、あとはフォントサイズを調整するくらいです。


1秒ごとに表示内容を更新させる:
このままだとクリックした瞬間の結果表示となるので待っていても内容は更新されません。そのため内容を更新させるプログラムを追加します。
先程、以下のように、

this.menuitem = new PopupMenu.PopupMenuItem(smi, {style_class:'smi-label'});

「nvidia-smi」から得た文字列をPopupMenuのオブジェクト生成と同時に渡しましたが、このオブジェクトへ後からテキストの値だけを渡すにはどうすればいいのか?いろいろ探しましたが、なかなか見当たりません。
ようやくGnome Shellのソースが見つかったので、なんとか解決しました。PopupMenuItem.jsに関してはこのページの239行目(以下)。

var PopupMenuItem = new Lang.Class({
    Name: 'PopupMenuItem',
    Extends: PopupBaseMenuItem,

    _init(text, params) {
        this.parent(params);
        this.label = new St.Label({ text: text });
        this.actor.add_child(this.label);
        this.actor.label_actor = this.label
    }
});

ここから察すると、どうやら「PopupMenuItem.label.text」で文字列を渡せそうです。メニュークラスなので文字列はラベル扱いで、そのラベルの下にテキストがあるようです。
ということから、アップデート関数をつくって、

update: function() {
    let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () { 
        this.menuitem.label.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        return true;
    }));
},

このようにすれば、一秒ごとに内容更新されるはずです。これで主要なところはほぼ完成。
以下が、コード全体。

const Lang = imports.lang;
const St = imports.gi.St;
const GLib = imports.gi.GLib;

const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;

const Me = imports.misc.extensionUtils.getCurrentExtension();
const Gio = imports.gi.Gio;
let gicon=Gio.icon_new_for_string(Me.path + "/gpu-icon.png");

let smiPanel;

const SmiPanel = new Lang.Class({
    Name: 'SmiPanel',
    Extends: PanelMenu.Button,

    _init: function() {
        this.parent(0.0, "smi panel", false);

        const icon = new St.Icon({ gicon: gicon,style_class: 'system-status-icon'})
        
        this.actor.add_actor(icon);       
        this._box = new St.BoxLayout();
        this.actor.add_actor(this._box);
  
        let smi = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        this.menuitem = new PopupMenu.PopupMenuItem(smi,{style_class:'smi-label'});
        this.menu.addMenuItem(this.menuitem);
    },

    update: function() {
        let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () { 
            this.menuitem.label.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
            return true;
        }));
    },
 
    destroy: function() {
        this.parent();
    }
});

function init() {}

function enable() {
    smiPanel = new SmiPanel();
    smiPanel.update();
    Main.panel.addToStatusArea("nvidia_smi", smiPanel, 0, "right");
}

function disable() {
    if (smiPanel) {
        Main.panel._rightBox.remove_actor(smiPanel.container);
        Main.panel.menuManager.removeMenu(smiPanel.menu);
        smiPanel.destroy();
        smiPanel = null;
    }
}

BoxLayoutは使う必要があるかわかりませんが、一応入れておきました。
それにしても、相変わらず手がかりが少ないのでわかりづらい。それでも、なんとか目標にしていた表示方法に達成できました。毎回Alt+F2を押してr(リターン)でのExtension再起動もキー操作が面倒なのでスクリプト化してしまいました(xdotool使用)。
しばらくはGnome shellのソースと他のExtensionsを参考に調べていくしかないという感じです。
とりあえず、わからなくなったらこのReferenceを見るといいのかもしれません。
WindowやMenuの種類については、ここに画像とともに説明してあります。ここで見てみると、PopupMenuクラスではなく、PanelMenuクラスを使えばよかったのかもしれません。次回はこの辺の違いを検証していきたいと思います。

しかしながら、徐々にカスタマイズされてきたので使いやすくなってきました。


追記(シンプル版):
その後、もう少し単純なコードで書けないか試してみました。今回はサンプルの「Hello World」のようにクラスを使わずfunctionだけでできるかどうか?
サンプルにあるように、

init関数
enable関数
disable関数

それと、表示内容を毎秒更新するための、

update関数

これらの4つで構成します。
それから表示する文字列の体裁を整えるために「stylesheet.css」も利用します。
全体コード「extension.js」は以下。

const St = imports.gi.St;
const GLib = imports.gi.GLib;
const Main = imports.ui.main;
const Lang = imports.lang;
const Button = imports.ui.panelMenu.Button;

let button;

function update() {
    let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () {
        //メニューが開いているときだけコマンド送信ならびに文字列を更新する
        if(button.menu.isOpen){
            button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        }
        return true;
    }));
}

function init() {
    //ボタンオブジェクト生成
    button = new Button(0, 'button', false);
 
    //ラベルで文字列アイコンをつくり、ボタンへ追加
    let icon = new St.Label({style_class: 'icon-label', text:'GPU'});
    button.actor.add_child(icon);
 
    //「nvidia-smi」返り値の文字列用labelオブジェクト生成
    //外部スタイルシート「smi-label」を反映させておく
    let label = new St.Label({text:'', style_class:'smi-label'});

    //ボタンにlabelを追加
    button.menu.box.add_child(label);
    //labelをlabel_actorにしアップデートできるようにしておく
    button.menu.box.label_actor = label;
}

function enable() {
    update();
    Main.panel.addToStatusArea('Popup', button, 0, 'right');
}

function disable() {
    if(button){
        Main.panel._rightBox.remove_actor(button.container);
        Main.panel.menuManager.removeMenu(button.menu);
        button.destroy();
        button = null;
    } 
}

またスタイルシート「stylesheet.css」の内容は以下。

.icon-label {
    padding-top: 0.3em;
}

.smi-label {
    font-family: monospace;
    font-size: 15px;
    padding-left: 1em;
    padding-right: 1em;
}


今回は、PanelMenu.jsのButtonクラスを利用しました。「Menu」という文字列だけを表示させれば以下のようなシンプルなプルダウン形式のウィンドウです。


この「Menu」という文字列の部分に「nvidia-smi」の表示内容を入れるだけです。

ボタンオブジェクトを生成したあと、アイコンや表示する文字列をLabelオブジェクトとして追加していき、update関数で表示文字列だけを更新するというシンプルな内容ですが、またもや新たに「nvidia-smi」コマンドで受け取った文字列を渡す際に手間取りました。
特に、init関数の最後の部分です。前回PopupMenuItemの場合は、

PopupMenuItem.label.text = '表示させたい文字列';

で更新する文字列を渡すことができたのですが、今回の場合は同じようにやってもだめで、

button.menu.box.label_actor = label;

このようにlabel_actorを定義しておいてから、

button.menu.box.label_actor.text = '表示させたい文字列';

という感じで渡さないとだめでした。
こっちのほうがきちんとした書き方なのかもしれないけれども、Referenceやソースを見てもわかりにくい。手がかりとなったのは、前回も確認した(ソース)、

var PopupMenuItem = new Lang.Class({
    Name: 'PopupMenuItem',
    Extends: PopupBaseMenuItem,

    _init(text, params) {
        this.parent(params);
        this.label = new St.Label({ text: text });
        this.actor.add_child(this.label);
        this.actor.label_actor = this.label
    }
});

この最後に書いてある部分を参考に試してみたら文字列を渡すことができました。
まだまだ分からないことばかりで、以下気になった部分を列挙しておきます。


ボタンオブジェクトを生成の際のパラメータ:

button = new Button(0, 'button', false);

()内の最初の「0」はドロップダウンメニューの位置のようで、0:右寄り、1:左寄り、0.5:中央となるようです。ただし、画面からはみ出ないように上書き調整されるようです。

「1」にした場合は、クリックするトップバーアイコンを基準に左側に表示されます。

「0.5」にした場合は、トップバーアイコンを基準に中央に表示されます。

基本は「0」のまま(右寄り)でいいかと。

それから()内中央はおそらくオブジェクトの名称だと思うので適当な文字列を記入。()内右側の「false」は「dontCreateMenu」というパラメータであり、「True」にしてしまうとアイコンそのものも消えてしまうようで、通常は「false」のままでいいかと。


アップデート関数:
今回のアップデート関数は何を参考にしたか忘れてしまいましたが(ここを参照)、

function update() {
    let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () { 
        button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        return true;
    }));
}

この書き方の場合、最後に「return true」を入れないと更新されないようです。
また別の書き方だと、

const Mainloop = imports.mainloop;
let timeout;


function update() {
    button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
    timeout = Mainloop.timeout_add_seconds(1, function(){
        update();
    });
}

mainloopライブラリをインポートしておき、ループ内でこのupdate()自体を再度呼び出して関数自体をぐるぐる回す感じ。しかし、disable()の中に以下を書いて破棄しなければだめなのかも。

Mainloop.source_remove(timeout);
timeout = null;

GLib.timeout_add_seconds()の場合も、ループ内に自身をループさせるようにすれば、「return true;」なしでも動きます。
このあたりの違いは、徐々に調べながらでしょうか。
後から気づきましたが、このままだとメニューを閉じた状態でも、常に毎秒「nvidia-smi」のコマンドを送り続けているので「button.menu.isOpen」でメニューが開いているときだけコマンドを送ったほうがよさそうです。ということから以下のように変更しました。

function update() {
    let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () {
        if(button.menu.isOpen){ 
            button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        }
        return true;
    }));
}



文字列をトップバーアイコンにする場合:
アイコンの代わりに文字列をトップバーに埋め込むには、Labelオブジェクトに文字列を渡してからボタンに追加していましたが、そのままだと、


このように上端寄りになってしまいます(「GPU」の部分)。
何かパネル用のスタイルがあるのかもしれませんが、わからないので外部CSSファイル(stylesheet.css)に、

.icon-label {
    padding-top: 0.3em;
}

を追加記入して反映させることにしました。
そうすると、


このように他と同じよう表示されました。

ということで、まだまだ未知の部分が多くちょっとしたことでも一苦労という感じです。



追々記(さらに改良):
とりあえず動くことを目標としていましたが、一旦動き出すと気になる部分がでてきます。前回、メニューが開いているときだけ「nvidia-smi」のコマンド送信(毎秒)をさせました。しかし、それでもenable()内に書いたupdate()は空回りしながらも動き続けているので、メニューが開いているときだけupdate()が動くように変更しました。
手順として:

・待機状態
・ボタンが押された
・メニューが開いた
・ループ開始
・メニューが閉じた
・ループ終了
・待機状態に戻る

という感じ。ボタンが押されるまで待機するには、

button.connect('button-press-event', update);

「Hello World」のサンプルにも使われているこれを利用。つまりボタンが押されたら、update()を起動させるというものです。これはinit()に書いておきます。
ただし、このconnect()もどこかでループ待機しているはずなので、その辺の仕組みまでは検証しないことに。
ここで、ボタンが押されたときのフラグを用意しようかと思いましたが、

button.menu.isOpen = true/false

があるので、これを利用します(この辺はソースを見ながら、何が使えるか判断しています)。
つまり「ボタンが押された=メニューが開いた」ということなので以下のようにupdate()を変更。

function update(){
    if(button.menu.isOpen){
        let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () {
            button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
            update();
        }));
    }
}

update()内の無限ループの途中に、メニューが開いたらというフラグをつけておきます。
アイコンをクリックした後、別の場所(他のウィンドウなど)をクリックした際にメニューが閉じてしまうこともあるので、クリックしたかどうかで判定するよりも、メニューが開いているかどうかで判定させたほうがよさそうです。
よって、これまでenable()内にupdate()を配置していましたが、それを消しておきます。
全体コード:

const St = imports.gi.St;
const GLib = imports.gi.GLib;
const Main = imports.ui.main;
const Lang = imports.lang;
const Button = imports.ui.panelMenu.Button;

let button;

function update(){
    if(button.menu.isOpen){
        let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () {
            button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
            update();
        }));
    }
}

function init() {
    button = new Button(0, 'button', false);

    //トップバーにアイコン画像を使用する場合は以下 
    //let icon =  new St.Icon({ icon_name: 'system-search-symbolic', style_class: 'system-status-icon'});
    //今回は文字列をアイコン代わりに使用(スタイルシート反映)
    let icon = new St.Label({style_class: 'icon-label', text:'GPU'});
    button.actor.add_child(icon);

    let smi = '';
    let label = new St.Label({text:smi, style_class:'smi-label'});
    button.menu.box.add_child(label);
    button.menu.box.label_actor = label;

    //ボタン待機:押されたらupdate()発動
    button.actor.connect('button-press-event', update);
}

function enable() {
    Main.panel.addToStatusArea('gpu', button, 0, 'right');
}

function disable() {
    if(button){
        Main.panel._rightBox.remove_actor(button.container);
        Main.panel.menuManager.removeMenu(button.menu);
        button.destroy();
        button = null;
    }
}

こんな感じでしょうか。
「nvidia-smi」コマンドの応答に対して例外処理するほうがいいと思いますが、今回は 省略。きりがないですが、徐々に改良されてきました。
相変わらず情報源が少ないので難儀しますが、いまのところ以下を参考にしています。

(1)GNOME Shell Javascript Source Reference
どのようなUIがあるのか画像つきで説明してあるのでわかりやすい。ここで使いたいUIを探します。

(2)GNOME gnome-shell/js/ui/
ここにソースがあるので、(1)で見つけたUIのソースを開いて使えそうな関数を調べています。

(3)GNOME applications in JavaScript
また、ここに一通りのリファレンスがあるので参考にしています。

(4)St Reference
Stに関してならここでしょうか。

(5)GNOME Creating an Applet
ここにボタンやメニュー以外にもファイル入出力やHTTPリクエストなどのサンプルがあります。

0 件のコメント:

コメントを投稿

人気の投稿