t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

自宅DHCP兼DNSサーバー障害対応

今日20時頃、自宅DHCPサーバーが応答しなくなった。

半年ほど前にもトラブルで再構築した気がするが、これは設定ミスによるパスワードトラブルだったので実際に本番機の再構築一歩手前で気づいて修正した。
thom.hateblo.jp

ということで前回実際に故障したのは9か月前となる。
thom.hateblo.jp

前回もSDカード故障だったが、今回もSDカード故障。
もともと16GB 10枚入りで5,000円という激安のSDカードを使っていたので安かろう悪かろうということかもしれない。
実際にこのSDカードはラズパイデスクトップを入れるとプチフリーズが大きくてサーバー専用で使っていたのだが、本当はサーバーこそ信頼性の高いものを使う必要がある。

急遽トラブルを解消しないと在宅勤務も厳しいのでとりいそぎそのあたりに転がっていたSDカードをピックアップしてラズパイOS Lite をクリーンインストールした。

実はこんなこともあろうかと自宅のGitbucketサーバーにDNS/DHCPサーバーのコンフィグを退避してあり、設定のデプロイもスクリプト化してある。
thom.hateblo.jp

これのおかげで復旧は余裕!と思っていたんだけど、肝心のGitbucketのIPアドレスがDHCPによる固定IP払い出し。。DHCPリース期間は残っているはずだけどそのIPアドレス情報が故障したDHCPサーバーもしくはGitbucketサーバーのリポジトリに保管されていて思い出せない。。しかも最近アクセスしていなかったせいでPCからのDNSキャッシュも切れており、故障したDHCPサーバーはDNSサーバーも兼ねているという八方ふさがりの状況になってしまった。

DHCP側に依存するデメリットは前回の故障時に痛感しているはずなんだけどやはり管理が楽なので復旧に緊急を要するサーバー以外はこれに頼ってしまう。今回の失敗は、構築当時は緊急性がなかったはずのGitbucketサーバーが、DHCP/DNSコンフィグが保管されることによって復旧緊急性が跳ね上がってしまったのにそのときに固定IP設定をしなかったことだと思う。

結局どうやったかというと、Redmine上のCMDBでGitbucketサーバーのMACアドレスを調べて新規構築したDHCPサーバー上にGitbucketサーバーの固定払い出し設定を適当なIPアドレスで記述し、GitbucketサーバーにキーボードをつないでCtrl+Alt+Delでリブートした後GitbucketからDNS_DHCP設定をクローンした。あとはDHCP/DNSサーバーのデプロイスクリプトを走らせてリブートかければ復旧完了。
あとGitbucketは適当なIPを設定してしまったので再度リブート※して正しいIPを再取得。

※ミニHDMIディスプレイとキーボードを繋いで直接端末にログインを試みたのだがキーマップがENになっているためかパスワードが通らずリブートくらいしかDHCPでIP再取得の手段が無かった。

その後Gitbucketサーバーは固定IP化し、これにて一見落着。

ネットワーク障害はかなり焦るけども障害のたびに色々と反省点も見えてくるし、むしろ定期的に何かが壊れた方が構成を思い出すきっかけになるので良い気がしてきた。この障害がたとえば5年後とかだったらもはや何も覚えてなくて更に影響が大規模になっていた可能性がある。

最近安定稼働していたので放置気味だった自宅ネットワークだが、これを機に再度障害への備えについて見直そうと思った。

以上。

10/1追記

スマホからローカルのファイルサーバー(Samba)への接続に妙に時間がかかるようになり、ホームゲートウェイからローカルDNSへの転送を忘れてたことに気づいたので追記。

プロバイダとIPv6通信を契約にしてるとホームゲートウェイのDNSにはIPv6しか指定できないのでローカルDNSのglobal IPv6アドレスを指定しておく。

これでホームゲートウェイがlocal.thom.jp宛ての名前解決を受け取るとローカルDNSに転送してくれる。

E24系列の金属皮膜抵抗セットを1万円で購入

最近とある電子工作の実験で微妙抵抗値が必要になった。

最初はよく分かってなかったのでAmazonで適当な抵抗セットを買ってこれまでそれを使っていたんだけど、どうやら私の買ったのはE3系列というものらしく、ネットのサンプルで330Ωや510Ω等の抵抗値が使用されていると複数を組み合わせて近似値を作らないといけないので面倒だった。

E3系列というのはベースとなる3種類の数字(1・2.2・4.7)を使って、10Ω、22Ω、47Ω、100Ω、220Ω、470Ω、1kΩ、2.2kΩ、4.7kΩ…という風に組み合わされた抵抗のセットである。

対してE24系列はベースの数字が24種類あるというもの。
最大でE192系列まであるようだが、普通はE24まで揃えれば趣味の範囲では十分すぎる。(というかオーバーキルかも。。)
detail-infomation.com


今回買ったのは以下の製品。
eleshop.jp

金属皮膜抵抗はカーボン抵抗と比べて温度変化に強く誤差も少ないが、その分値段が張る。
カーボン抵抗の誤差を気にするほどのレベルの工作をしているわけではないが、想定通りに動かないときに抵抗を疑ってハマるのは避けたいので一応金属皮膜を選択した。

100本セットだと約4万円なので実はちょっと迷った。値段4倍で量が10倍なのでだいぶディスカウントが効いている。
ただどのみちよく使う抵抗値は決まってくるだろうし個人で使う量じゃないのでやめておいた。

117種類10本ずつのセットでこれくらいの箱で届く。

これの10倍とかどこに仕舞うつもりだったんだ。。10本セットで良かった。


さて、ホーザンのパーツキャビネットに収納したいのだがここで一つ問題が。

立てると飛び出し、寝かすとかさばる。

そこで思い出したのが昔買ったクリップシーラー。

袋のサイズを変えたりするのに便利で昔よく使っていたんだけど最近は出番も無く眠っていた。

一旦袋を開封してギリギリのサイズでシールした後、余りをカットしてから再度テープで閉じる。

ちょっとシールが途中で切れてて汚いけど抵抗値は読める。

作業を続けること約1.5時間。3つの引き出しに綺麗に収まった。

とりあえずこれで当面抵抗に困ることはなくなった。
あとは使って減った分を買い足す感じで運用しようと思う。

以上

PID制御でDCモーターのスピードコントロール

今回はPID制御の学習のためDCモーターの回転スピードが一定になるように制御をしてみた。

制御とは

言葉のイメージどおり、対象物を思い通りに操ることを制御と呼ぶ。

DCモーターの場合は電圧に応じて回転スピードが変化するので、回転スピードを計測して遅すぎる場合は電圧を上げ、速すぎる場合は電圧を下げれば良さそうだ。ところが実際にやってみるとこのアイデアはうまくいかない。

実は電圧を上げてもその電圧に応じた回転スピードに達するまでにはタイムラグがあるため、センサーで判定するとまだ出力不足と判断して電圧を上げすぎてしまう。逆に電圧を下げてもすぐに回転スピードは落ちないので、今度は電圧を下げすぎてしまう。これを繰り返すのでモーターの速度はフラフラと上がったり下がったりを繰り返すことになる。

実際に試してみたのがこちら。

想定どおり、電圧変動から少し遅れて回転速度が変わっている。

また、モーターは何かしらの仕事をさせる目的で使う為、普通は負荷がかかる。負荷が変動しても一定スピードで動かそうと思うと工夫が必要になる。

PID制御とは

PID制御とは、Proportional(比例)-Integral(積分)-Differential(微分)の頭文字をとった制御方法である。
数学嫌いにとってはうげっとなるワードかもしれないが概念を理解するだけならシンプルなので安心して欲しい。

P制御(比例)

P制御は基準値からの誤差に比例して操作量を増減させる手法である。
要するに、HIGH(下げる) or LOW(上げる)という単純処理じゃなくて、ちゃんと程度問題として扱いましょうということだ。

ちょっと頑張れば誰でも思いつきそうなアイデアなのでここはそれほど難しい話ではない。
ただP制御は基準値に近づけば近づくほど操作量を減らしていくので、実はいつまでたっても基準に到達しない可能性があるという弱点があるらしい。
※曖昧な書き方なのは、今回私が行った実験では特にその弱点が露呈することが無かったためである。

I制御(積分)

I制御は基準値からの誤差の累積に比例して操作量を増減させる手法である。
累積なんてしたらすぐに基準値を飛び越えてしまいそうだけど、うまく調整しないと実際にその通りになる。
ただ飛び越えた分はマイナスの累積となるため段々波が小さくなるように安定してくる。

I制御は安定するまでに時間がかかるというデメリットがあるが、どんなに小さな誤差でもずっと続くと累積によって大きな力を生むため、P制御と組み合わせるとちょうどお互いの弱点を補完しあうことができる。
PIを組み合わせる場合は基本的にはP制御をメインに据えて、誤差のところだけI制御による累積の力をほんの少し借りるというイメージ。

D制御(微分)

D制御は基準値からの誤差の急激な変化を打ち消すような力を働かせる手法である。
そもそも変化が無ければ働かないのでこれ単体では制御手法として成立せず、PIと組み合わせる安定剤に近い役割。

例えば急激に負荷が増えた時などは通常のP制御ではそのまま失速してしまうことがある。どかっと負荷が増えた際には、瞬間的に電圧を特盛にして早急にリカバリーしたい。そこで、変化の傾きを測定して操作量を柔軟に決めるのがこのD制御だ。


PID制御

それぞれの制御をどれくらい取り入れるかは、それぞれゲインと呼ばれる定数を掛け合わせることで行う。

ゲインの調整は以下のサイトに詳しく書いかれているのでとご参考までに。
controlabo.com

私は今回適当なサンプルをベースに試行錯誤するという方法をとったけど、本当はちゃんとやらないとハードを壊しかねないので貴重な機材や危険な機材でやるときはきちんと理解して適切な値の設定が必要なようだ。

今回は模型用モーターを適切な保護機構がついたモータードライバーで制御してるのでまぁ多少間違えてもそんな大層なことにならない。
またPWMで制御してるのでは最小0、最大255までしか入らない。10,000とかPWMで指定してしまったところで255と解釈されるのでせいぜいモーターが100%出力になって煩い程度である。

実験のセットアップ

セットアップはこんな感じ。

準備物

部品取り用ギア―ドモーター

速度検出センサーのスリット付き円盤に合わせるシャフトアダプタが無かったので購入。

バラしてアダプタ部分だけ取り外し。ネジザウルスとかラジオペンチでねじりながら引っ張る感じだった。

最初は3Dプリンターでの印刷を試したんだけど、今はある程度大きなものを短時間で印刷する為にノズル径を0.8mmに変更してて、スピード設定なんかも最適化してるので小さいパーツの精密な印刷は出来なかった。毎回米粒みたいになる。小物の精密印刷用に調整しなおすのも面倒なので。。

その他

  • Arduino入りブレッドボード
  • モーター駆動用の直流安定化電源

コード

#define AIN1 3
#define AIN2 5
#define ROTARY_ENCODER 2
#define SPAN 250 // milliseconds
#define REFERENCE 20 // rolls per SPAN
const double Kp = 2;
const double Ki = 0.1;
const double Kd = 0.5;
long rolls = 0;

// エンコード用の円盤のスリット数が20なので20カウントしたら1回転。
void detect_turn() {
  static int cnt = 0;
  if(cnt++ >= 20){
    rolls++;
    cnt = 0;
  }
}

void setup() {
  pinMode(AIN1,OUTPUT);
  pinMode(AIN2,OUTPUT);
  pinMode(ROTARY_ENCODER, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ROTARY_ENCODER), detect_turn, RISING);
  Serial.begin(9600);
  analogWrite(AIN1,0);
  analogWrite(AIN2,0);
}

void loop() {
  static double pwm = 0;
  static double e = 0;
  static double e1 = 0;
  static double ie = 0;
  double M;

  static unsigned long last_millis = millis();
  if(millis() - last_millis >= SPAN) {
    e1 = e;
    e = REFERENCE - rolls; 
    ie = ie + (e + e1)*0.25/2;
    M = Kp*e + Ki*ie + Kd*(e-e1)/0.25;

    // 回転数が2未満になると、止まっているか、速すぎて測定できていないかどちらかなので
    // PWM値で状態を判定して停止中なら始動用に300ミリ秒間255(100%)を出力し、そのあと2秒間出力を20にして安定させる。
    // 単に速すぎる場合はそのまま2秒間出力を20にして安定させたのち、pwm値30から再開。
    // 回転数が2以上の場合は通常のPID計算に基づいてpwmを決定。
    if(rolls<2){
      if(pwm<60){        
        analogWrite(AIN1,255);
        delay(300);
      }
      analogWrite(AIN1,20);
      delay(2000);
      pwm = 30;
    } else {
      pwm += M;
    }
    analogWrite(AIN1,int(pwm));
    Serial.print("ROLLS:");
    Serial.print(rolls);
    Serial.print(",");
    Serial.print("PWM:");
    Serial.println(pwm);
    rolls = 0;
    last_millis = millis();
  }  
}

実行結果

実行の結果は以下のようになった。

負荷をかけた瞬間にPWM出力が大きく変動することで逆に回転数の変動は最小限に抑えられ、負荷が無くなった瞬間にPWMも急激に下げることで回転数の急激な上昇を防いでいる。しっかりとD操作が効いている。

苦労した点

今回一番苦労したのはPIDとは関係なくモーターの始動用コードの調整である。
モーターは始動時に大きな電力を必要とするため、一時的に最大出力で始動させる必要があるがそうすると回転数もMAXで始動してしまう。
ただ今回使った速度検出センサーは8,000rpm程度の計測が限界で、それを越えると出力が0になって停止状態と判別できなくなってしまう。
待機秒数を間違えるとうまく始動しなかったり、暴走して延々とpwmが上がり続けたりとなかなかうまくいかない。ここでまる2日ハマった。

所感

私がPID制御に興味を持ったのが2022年6月。以下の記事が初めてである。
thom.hateblo.jp

数学を真面目にやってこなかったので微分も積分もできない状態から1年経過し、ようやくモータースピードの調整までたどり着いた。
結局、微分も積分もできないのは変わっていないけど、言ってることは理解できるようになってきたので実際のコードでやってる近似処理もなんとなく頭の中で結びついてきたといったところ。

やはり何かやりたいことを胸に抱きつづけて少しずつでも、途切れながらでも挑戦をしつづければやがては到達できるんだなと思った。

以上

参考サイト

controlabo.com

LPI Linux Essentials 試験レビュー

本日 LPIが提供するLinux Essentials試験を受験してきた。
無事に合格したのでレビューを書こうと思う。

試験のレベル感と取得の意義

Linux認定試験の中では最もエントリーレベル。
ただし単に簡単ということではなく、Essential(本質的・根本的・必要不可欠)という名前のとおり、Linux触るなら最低限このくらいは知っておきましょうという内容が学習範囲となるため本職以外でLinux知識が必要な方には必要十分な内容だと思う。

さすがに本格的なサーバーエンジニアになるとこの出題範囲では物足りないが、オペレーターなら十分かなと思う。

特に趣味でラズパイサーバーを立てている私みたいな人間が、スキルアピールとしてLinux触ったことありますと主張するにはちょうど良い手ごろなレベル感。

逆に、Linuxをまったく触らない人が単に教養として認定取得する意義は薄い気がする。

受験料と受験方法

通常だと税込 11,000円。
ping-tという学習サイトで割引チケットを買うと税 9,900円。

試験はピアソンVueで提供されているので全国のテストセンターで受験することになる。

学習前の私のレベル感と学習方法、学習期間

学習前の私のスキルレベル

もともと10年ほど前にLinuxカルチャーに傾倒した時期があり、ファイル・ディレクトリの操作やcron設定、パッケージインストール、viでのテキスト編集など、そもそも知らないとLinuxのコマンド操作がままならないような内容に関する知識は持っていた。
ただ所詮趣味で触っている都合、ユーザー管理・パーミッション管理・アーカイバーなどは必要に迫られたことが無いため弱い。

基本情報技術者・応用情報技術者を持っているのでIT全般の基礎知識はあり。
ネットワークも以前は必要に迫られなかったのでもともと苦手分野だけどラズパイの登場以降は管理デバイスが増えすぎてCiscoスイッチを導入し、自宅ネットワークをいじり倒したので現在は苦手意識はない。

私の学習方法

以下の書籍が5編に分かれているので1編やってはping-tで対応する問題を解いた。

LPI公式認定 Linux Essentials 合格テキスト&問題集

LPI公式認定 Linux Essentials 合格テキスト&問題集

  • 作者:長原 宏治
  • 日本能率協会マネジメントセンター
Amazon

ping-tは登録無料でLinux Essentialsの学習範囲はすべて無料なので特に費用は掛かっていない。

私の学習期間

体感的には1日30分~1時間くらいで概ね1か月程度。

Redmineで学習管理していたので具体的に示すと、書籍とping-tの1周目が4/10~4/23

ping-tの2周目と全問題コンボ※までが4/24~5/8 (ソート順が逆)

※同じ問題を2回連続で正解するとコンボ

模擬試験機能もあるので計画はしていたんだけどフルコンボしたしどうにかなるだろってことで最後はダレたまま試験日を迎えた。

問題数と試験時間

60分で40問を解くのだが、所詮は知識問題なのでサクサク解けないものは考えても解けない。
従って実質所要時間は15分くらい。

総括

所詮エントリーレベル認定なので権威性は全くないが、学習期間・テスト時間ともに短い割にLinux運用の中核となる知識が習得できるので、Linuxをなんとなく使っている人がもうすこしちゃんと知識を補完したいというケースでは比較的コスパが良い資格だと思う。

普段からLinuxを触っている人なら自分の知識範囲と出題範囲が重なれば無勉強で挑んでも運よく取れるかもしれないが、コレクション価値のある認定ではないのでそういう取り方は時間と金の無駄だと思う。ちゃんと網羅的に学習を進めるのがおススメ。

日本では高難度認定ばかりがもてはやされてエントリーレベルの認定を無意味と一蹴する人も多い。
しかしエントリーレベルの認定は通常その分野で最も重要な知識をカバーしているため、学習時間に対する得られる知識という意味で最もコスパが良い。
専門分野ならともかく周辺分野の知識を固めるのに高難度認定はオーバーキルなので今後もこうしたエントリーレベルの認定を積極的に活用していきたい。

Python・数学・Bing AIでそれぞれSatisfactoryの代替レシピガチャの確率を求めてみた。

今回は私が遊んでいるゲーム Satisfactoryのなかのガチャ要素で狙ったものを引きあてる確率を求めてみた。

Satisfactoryは異星に工場を発展させていく生産シミュレーション系のゲームである。
詳しくは以下の記事で紹介しているので気になる方はチェックしてみて欲しい。
thom.hateblo.jp
ただし面白すぎて生活に支障が出ても責任は負いかねるのであしからず。

さて、このゲームでは部品を組み合わせて製品を作っていくのだが、通常のレシピとは別に代替レシピというものが存在する。

代替レシピはこの世界に転がっている墜落した降下ポッドから入手できるハードドライブというアイテムを分析器にかけることで手に入る。

このように3つの代替レシピが提示され、そのうち1つを選択して入手する形である。

ハードドライブの分析結果は87通り存在し、うち85種類がレシピ、のこり2つはインベントリスロットの増加(要するにキャラ強化)であるが、プログラムを書く上では便宜上レシピ87個としておこう。

つまり87個のうち、3個がランダムで提示されるので、そのうち狙った1点を引き当てる確率がどれくらいかという話になる。
そこでまずはPythonのrandomモジュールを使ってこのガチャをひたすら回すシミュレーションを書いてみた。
ゲームの仕様上、入手済みレシピは提示されなくなるため未開放レシピ数に応じた確率も調べてみた。

from random import randint

for z in range(87,2,-5):
    lookfor = randint(0, z-1)
    number_of_trial = []
    for y in range(10000):
        n=0
        hit = False
        while not hit:
            n+=1
            recipes = list(range(z))
            hit = False
            for x in range(3):
                item = recipes.pop(randint(0,len(recipes)-1))
                if item == lookfor:
                   hit = True
        number_of_trial.append(n)
    
    print("未開放レシピ:" + str(z) + "個",end=" ")
    print("最大:" + str(max(number_of_trial))+"回",end=" ")
    print("平均:" + str(sum(number_of_trial)/len(number_of_trial))  + "回")

実行結果は次のとおり。
例えば87個の未開放レシピが残っている状態で狙った1点を入手するためにリセマラ※した場合、最大270回、平均28.7882回やり直しが必要ということ。
※リセットマラソン=思い通りの結果になるまで挑戦前のセーブデータを読み直す行為。

未開放レシピ:87個 最大:270回 平均:28.7882回
未開放レシピ:82個 最大:311回 平均:27.4874回
未開放レシピ:77個 最大:253回 平均:25.6612回
未開放レシピ:72個 最大:203回 平均:23.8749回
未開放レシピ:67個 最大:219回 平均:22.5432回
未開放レシピ:62個 最大:206回 平均:20.6362回
未開放レシピ:57個 最大:186回 平均:18.7125回
未開放レシピ:52個 最大:179回 平均:16.9635回
未開放レシピ:47個 最大:138回 平均:15.4295回
未開放レシピ:42個 最大:141回 平均:13.8436回
未開放レシピ:37個 最大:105回 平均:12.2546回
未開放レシピ:32個 最大:121回 平均:10.7985回
未開放レシピ:27個 最大:93回 平均:9.0674回
未開放レシピ:22個 最大:87回 平均:7.3508回
未開放レシピ:17個 最大:44回 平均:5.616回
未開放レシピ:12個 最大:30回 平均:3.9715回
未開放レシピ:7個 最大:18回 平均:2.3499回

ハードドライブ1個につき分析に10分かかるので平均して5時間近くかかる見込み。
で、5時間あれば世界中駆け回ってハードドライブ87個集まるので、このゲームでは特にリセマラする意味はないということが分かった。


ちなみにリセマラ回数ではなくて1発で引き当てる確率を求める場合はこのようなコードになる。

from random import randint
lookfor = randint(0, 86)
n=0
for y in range(1000000):
    recipes = list(range(87))
    for x in range(3):
        item = recipes.pop(randint(0,len(recipes)-1))
        if item == lookfor:
            n+=1
print(n/1000000)

3回実行したところ、いずれも3.4%となった。

0.034789
0.034437
0.034695

ちなみに数学的にはこういう式になる。
\displaystyle \frac{_{86}C_2 \times _1C_1}{_{87}C_3}

計算してみると、こちらも3.4%と出た。

>>> (((86*85)/(1*2))*(1/1))/((87*86*85)/(1*2*3))
0.034482758620689655

このようにプログラムによるシミュレーションでも十分な回数を繰り返せば数学的に求めた理論値に近似するので私のように数学が苦手な方はプログラミング力を磨いておくと何かと便利だと思う。

数学・プログラミングどちらも苦手という場合、これからの時代はAIによるサポートを受けることができる。
このように直接聞けば色々教えてくれる。
Powered by Bing

ただ問題を一般化して言語で表現することでこのように計算根拠まで確認することができるのでより的確な語彙を選択できる方が有利だとは思う。

Bingが出した式をPythonで計算してみるとBingが出した答えとちょっと違うのでそこらへんは鵜呑みには出来なさそうだけど。

>>> 1 - (86/87)**3
0.03408792366929225

てことで数学・ブログラミングが苦手なら国語力を鍛えるのがおススメ。

全部苦手って方は。。
うん、大丈夫。デキる友達に飯をごちそうすれば万事解決だ。

以上

電動エアダスターレビュー

ヨドバシポイントが貯まったので以前から気になっていた電動エアダスターを買ってみた。

購入したのはコンセント式のこちらの製品。

実際に使ってみるまでは、所詮エアーの勢いはガス圧にはかなわないだろうと思っていたんだけど電動の圧勝だった。
3メートル離れた位置からカーテンを窓に押し付けることができるくらいの勢いがある。

といってもピンとこないと思うのでデジタルスケールを使ってエアダスターと風圧を比較してみた。


実は手持ちのガス缶は少し使った後なので上記の画像では公平な比較は出来ていないんだけどまだまだエアダスターとして十分使える状態のものだ。

まさに圧勝。

動画に撮ったので気になる方は音も聞いてもらえればと思う。
youtube.com
音は完全に掃除機だ。

部屋の隅にたまりがちな埃もこれで巻き上げて表にだしてから掃除すれば掃除機が届かないところの埃を一掃できる。
私はこれで数か月分の埃を処理したので部屋が大惨事になったけど。。マメにやると良さげ。

あとはエアコンフィルターを水洗いした後に水滴を飛ばしたり、キーキャップ洗浄後に裏側の水滴を吹き飛ばしたりするのに使う。

注意点は消費電力が400Wあるのでコンセントの許容量を超えないように気を付けることと、すぐ温風が出るほどモーターが発熱するため説明どおり15分以上連続稼働させないことくらいかな。

結構な値段はしたけどもうガス缶を買わなくてよくなるので良い買い物だったと思う。

以上。

GUI開発が面倒ならCUIで作れば良いという気付き~Pythonリマインダー管理用コマンドツールを作成

今回は、以前作成したリマインダーシステムの管理ツールを作成した話。
thom.hateblo.jp

あれからリマインダーシステムはますます重要度を増し、周期的な作業も含めると既に40近くのTo Doが登録されている。

最初のうちは手修正で良かったが、これだけ数が増えると管理が煩雑なので別途仕組みを考えた。

考察

ドラッグ&ドロップで1日あるいは1週間延期できるような仕組みを作ったものの、これだけ項目が多いと間違って延期してしまったら埋もれてしまい気づかないリスクがある。

また、細かい調整はどうしても手動でファイル名を変えていたのだが、文字が小さいので入力ミスに気付かなかったり、存在しない日付を入力してしまったり、日を変えたつもりで月を変えてしまったりといったリスクも生じる。

そろそろもう少し安心して利用できる管理ツールが欲しいと思ってはいたのだが、画面レイアウトの設計と実装が面倒くさくて長らく放置していた。

そしてついに閃いた。

。。。CUIでいいのでは?

経緯

これは私の偏見も多分に混じっているかもしれないが、Windows文化においてはGUIが正義で今どきCUIアプリなんて見向きもされない。
例えばプログラミング入門書は大抵CUIを前提に解説しているのだが、黒画面にテキストを表示させるだけのアプリなんて作っても何も嬉しくないと感じる人が大半だと思う。

ところがしばらくUnix/Linux文化にどっぷりつかるとこの考えは180°変わる。
もう10年以上前になるけど、しばらく私はLinux文化に傾倒していた時期がある。当時はコマンドラインこそが正義という考えのものとWindowsを捨てて1年間ほどCUIのみのDebianノートをメインPCとしていた。
そのきっかけとなったのはこの書籍。

過去に記事も書いていたようだ。
thom.hateblo.jp

しばらく忘れていた感覚ではあるが、確かにコマンドにはコマンドの良さがあるのだ。
このことを再度思い返すきっかけとなったのが最近やっているLinux Essentialsの学習である。

ということでCUIアプリを作ることにした。

要件

機能としてほしいのは閲覧・登録・延期・削除である。

機能 動作イメージ
閲覧 showで10件の閲覧、nextで次の10件、prevで前の10件。
登録 create タスク名で1時間後を期限として新規タスクを作成。
延期 selectで複数選択し、extend n dでn日延期。extend n hでn時間延期。
削除 selectで複数選択し、removeで削除。

実装

取り急ぎpythonで作ったmanage.pyが以下のコード。

import os
import re
import datetime
from datetime import datetime as dt

def get_tasks():
    tasks = os.listdir(os.path.join(os.getcwd(), "tasks"))
    return [s for s in tasks if s.endswith(".txt")]

tasks = get_tasks()
selection = set()

def show_tasks(i):
    for x in tasks[i:i+10]:
        print(tasks.index(x), x)


cmd = ""
i=0
while cmd != "exit":
    cmd = input("$ ")
    if cmd == "show":
        show_tasks(i)

    if cmd == "next":
        if i + 10 < len(tasks):
            i = i + 10
            show_tasks(i)
        else:
            print("EOF")

    if cmd == "prev":
        if i - 10 >= 0:
            i = i - 10
            show_tasks(i)
        else:
            print("BOF")

    if cmd == "reload":
        i = 0
        tasks = get_tasks()
        selection = set()
        show_tasks(i)

    pattern = re.compile("^select( [0-9]+)*$")
    if pattern.match(cmd) is not None:
        for x in sorted(set(map(int,cmd.strip().split(" ")[1:]))):
            if len(tasks) >= int(x):
                selection.add(x)
        for y in sorted(selection):
            print(y, tasks[y])
    
    pattern = re.compile("^unselect( [0-9]+)*$")
    if pattern.match(cmd) is not None:
        for x in sorted(set(map(int,cmd.strip().split(" ")[1:]))):
            if len(tasks) >= int(x):
                selection.discard(x)
        for y in sorted(selection):
            print(y, tasks[y])

    pattern = re.compile("^extend -?[1-9][0-9]* (d|h)$")
    if pattern.match(cmd) is not None:
        for x in sorted(selection):
            exstr = cmd.strip().split(" ")[1:]
            if exstr[1] == "d":
                td = datetime.timedelta(days=int(exstr[0]))
            else:
                td = datetime.timedelta(hours=int(exstr[0]))

            newtime = (td + dt.strptime(tasks[x][0:16], "%Y_%m_%d_%H_%M")).strftime("%Y_%m_%d_%H_%M")
            old_path = os.path.join(os.getcwd(), "tasks", tasks[x])
            new_path = os.path.join(os.getcwd(), "tasks", newtime + tasks[x][16:])
            os.rename(old_path, new_path)
        tasks = get_tasks()
        selection = set()

    pattern = re.compile(r'^create [^ .\\/:*?"<>|]+$')
    if pattern.match(cmd) is not None:
        fname = (dt.now()+datetime.timedelta(hours=1)).strftime("%Y_%m_%d_%H_%M_") + cmd.split(" ")[1] + ".txt"
        fpath = os.path.join(os.getcwd(), "tasks", fname)
        open(fpath,"w").close()
        print(fpath)
        tasks = get_tasks()
        selection = set()

    if cmd == "remove":
        for x in sorted(selection):
            print(tasks[x])
        if input("Are you sure? ") == "sure":
            for y in sorted(selection):
                rm_path = os.path.join(os.getcwd(), "tasks", tasks[y])
                os.remove(rm_path)
                print("[Removed]" + rm_path)
        else:
            print("Cancelled.")
        tasks = get_tasks()
        selection = set()

雑な説明

python manage.pyとして実行するとプロンプトとして$マークを表示しコマンドを受け付ける。
コマンドを受け付けると処理に応じて出力し、次のコマンドループに入る。exitが入力されたらプログラムを終了する。

show等の引数無しコマンドはif文で比較し、extendなどの引数付きコマンドは正規表現でコマンドと引数の正しさをチェックしている。
selectコマンドは実行する度に選択アイテムが追加される仕組みなのでshow・next・prev等で別ページにあるアイテムを追加するのも簡単。
引数無しでselectを実行すると現在選択されているアイテムがずらっと表示される。
removeコマンドは確認が入るので、sureと入力すると削除、それ以外が入力されたらキャンセルされ、アイテムの選択が解除される。
※今のところunselect allは実装していないのでunselectで個別指定するかremoveをキャンセルするかexitしてやり直すことになる。

終わりに

CUIで面白いプログラムを書けないならGUIでも書けないという言葉をどこかで読んだ記憶がある。
その言葉を知った当時は少々過激な表現だなと思っていたけど、CUIでも十分に使えるプログラムを書けるということを改めて実感できたので、あながち間違いでもない気がしてきた。

プログラミング入門者が挫折するひとつの要因は、入門書で学習してもなかなか実用的なプログラムが作れるところまでたどり着かない点にあると思う。
実用的なプログラム=GUIアプリだという思い込みがこの傾向に拍車をかけているのかもしれない。
初心者はGUIアプリの作成に憧れを抱きがちだ。しかし初心者が作るとなるとなかなか難しい。

ひょっとするとその憧れの向き先をGUIではなくCUIに向けることができれば、案外すんなりとプログラミング言語を習得できるかもしれない。
確かにGUIの方が直感的に使えるけれど、慣れればCUIの方が効率的というケースはよくある。

それにマウスでポチポチ操作するよりも黒画面でカタカタやってるほうがなんとなく玄人っぽくて格好良い※。最初はそういう適当な憧れで良いんだと思う。

皆さんも自分だけのCUIツールを作ってみてはいかがだろうか。

※個人の主観です。PC音痴の知人によると黒画面で何かしてる人はみんなサイバー犯罪者に見えるようですが。。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。