クソ雑魚エンジニアのメモ帳

学んだことを書くところ

低レイヤチョットワカル(5章)

こんにちは。ようやくコンピュータの核の部分を実装する章までやってきた。。

地味に長かった。がなんとか終わりそう

www.oreilly.co.jp

記録用git

vol.1

vol.2

vol.3

vol.4

vol.5 いまここ

5章 コンピュータアーキテクチャ

理論(一般的なコンピュータ)

ノイマンアーキテクチャ

CPUが中央にあり、メモリ・入力デバイス・出力デバイス等と通信する。

メモリ

データメモリ・命令メモリの2種類に分けられる。アーキテクチャによっては別のメモリユニットになり得る。

どちらにせよバイナリで保存される

データメモリ

プログラムで利用するデータを保存することができる

3章で学んだメモリのように、読み込み・書き込みができる

命令メモリ

読み込みのみである

命令メモリとCPUは、以下のようなやりとりを行う

  1. 命令メモリを一つフェッチ
  2. CPUが命令をデコード・実行・次のアドレス計算
  3. 1に戻る(次のアドレスに対して)

CPU

CPUは、ALUとレジスタと制御ユニットで構成されている

ALU

3章で作ったもの。汎用的な演算装置

レジスタ

データメモリよりも高速にデータの保存・取得ができる領域

ただしデータメモリに比べはるかにサイズが小さい

データレジスタ=はやくて便利なメモリ?

アドレスレジスタ=読み書きに利用するためのアドレスを保存するレジスタ

プログラムカウンタレジスタ=現在実行している命令メモリのアドレスを保存するレジスタ

制御ユニット

CPUが命令をデコードするために必要なもの。

命令メモリを一つフェッチCPUが命令をデコード・実行・次のアドレス計算

ここまでを踏まえて、CPUの処理は、以下のように分割できる

  1. 命令メモリを一つフェッチ
  2. 制御ユニットが命令をデコード
  3. 命令を実行
  4. 次の命令メモリアドレスを計算

入力出力デバイス

全ての入出力デバイスはメモリマップドI/Oという仕様に従っている

キーボード=ベースアドレスが決められており、キーボードは常に入力値をベースアドレスに格納している。したがってCPUはベースアドレスを取得するだけでよい

スクリーン=ベースアドレスから描画領域分のアドレスの領域のメモリの値が画面に描画されると決められており、CPUはその領域のメモリの値を変更するだけで良い

理論(Hackハードウェア)

A命令

前章で紹介したが、

0xxxxxxxxxxxxxxxというフォーマットに沿う。

xxxxxxxxxxxxxxxの値がアドレスレジスタに保存される

C命令

前章で紹介したが、

111accccccdddjjjというフォーマットに沿う。

このフォーマットはALUのフォーマットと非常に近い。それに気づけるかどうか少し難しいところ。

Hackハードウェアの構成

  • 命令メモリ
  • CPU
  • データメモリ
    • RAM
    • メモリマップ(スクリーン)
    • メモリマップ(キーボード)

実装

データメモリ

データメモリは、RAMとスクリーンとキーボードで構成されている。

キモは、入力アドレスaddress[15]の値が、RAMかスクリーン(16384~24575)か24576(キーボード)かの判定部分。3パターンなのでDMux4Wayが使える

    // アドレス13,14で判断
    // address[14]=1 かつ address[13]=1 はキーボード
    // address[14]=1 かつ address[13]=0 はスクリーン
    // それ以外はRAM
    DMux4Way(in=load, sel[0]=address[13], sel[1]=address[14], a=isload-ram1, b=isload-ram2, c=isload-screen, d=isload-keyboard);

👇以下全文

CHIP Memory {
    IN in[16], load, address[15];
    OUT out[16];

    PARTS:

    // アドレス13,14で判断
    // address[14]=1 かつ address[13]=1 はキーボード
    // address[14]=1 かつ address[13]=0 はスクリーン
    // それ以外はRAM
    DMux4Way(in=load, sel[0]=address[13], sel[1]=address[14], a=isload-ram1, b=isload-ram2, c=isload-screen, d=isload-keyboard);

    Or(a=isload-ram1, b=isload-ram2, out=isload-ram);

    // RAM
    RAM16K(in=in, load=isload-ram, address=address[0..13], out=out-ram);

    // スクリーン
    Screen(in=in, load=isload-screen, address=address[0..12], out=out-screen);

    // キーボード
    Keyboard(out=out-keyboard);

    Mux4Way16(a=out-ram, b=out-ram, c=out-screen, d=out-keyboard, sel[0]=address[13], sel[1]=address[14], out=out);
}

CPU

最初はスイスイ行ったけどところどころハマった。👇以下全文

CHIP CPU {

    IN  inM[16],         // M value input  (M = contents of RAM[A])
        instruction[16], // Instruction for execution
        reset;           // Signals whether to re-start the current
                         // program (reset==1) or continue executing
                         // the current program (reset==0).

    OUT outM[16],        // M value output
        writeM,          // Write to M? 
        addressM[15],    // Address in data memory (of M)
        pc[15];          // address of next instruction

    PARTS:

    // CPU制御ユニット
    // a命令かc命令か
    // a-instruction
    // c-instruction
    Or(a=instruction[15], b=instruction[15], out=c-instruction);
    Not(in=c-instruction, out=a-instruction);

    // Dレジスタの保存はC命令の場合のみ
    And(a=instruction[4], b=c-instruction, out=isload-d);

    // instruction[4]はd2=>Dに保存するかどうか
    DRegister(in=alu-out, load=isload-d, out=out-d);

    // Aレジスタが保存するのは、A命令時の定数かC命令時の計算結果
    Mux16(a=alu-out, b=instruction, sel=a-instruction, out=in-a);

    // A命令でもC命令でも、結局のところAレジスタをloadするかどうか
    Or(a=instruction[5], b=a-instruction, out=isload-a);

    // instruction[5]はd1=>Aに保存するかどうか
    // outを切り取るのは問題ない??
    ARegister(in=in-a, load=isload-a, out[0..15]=out-a, out[0..14]=addressM);

    // instruction[12]はa。ALUのyはa=1ならM,a=0ならA
    // ALUにMを使うかAを使うか
    Mux16(a=out-a, b=inM, sel=instruction[12], out=aluin-y);

    // メモリの演算
    // 命令がそのまま流れる?
    ALU(x=out-d, y=aluin-y, zx=instruction[11], nx=instruction[10], zy=instruction[9], ny=instruction[8], f=instruction[7], no=instruction[6], out=alu-out, out[0..7]=alu-out1, out[8..15]=alu-out2, out[15]=alu-minus, out=outM, zr=alu-zr, ng=alu-ng);


    // Mに書き込み行うかどうか
    // CPUの領域外?
    // C命令の時のみ出力
    And(a=instruction[3], b=c-instruction, out=writeM);
    
    // jmp判定
    // 負の数はa-out[15]=1
    // 正の数はa-out[15]=0
    // 以下の変数を計算
    // alu-minus
    // alu-plus
    // alu-iszero
    Not(in=alu-minus, out=alu-plusorzero); //正の数または0
    Or8Way(in=alu-out1, out=temp1); // 0判定その1
    Or8Way(in=alu-out2, out=temp2); // 0判定その2
    Or(a=temp1, b=temp2, out=temp3); // 0判定その3
    Not(in=temp3, out=alu-iszero);
    And(a=alu-plusorzero, b=temp3, out=alu-plus);

    // カウンタ
    // C命令の場合のみ飛ぶのを忘れずに
    // j1, j2, j3の計算
    And(a=alu-minus, b=instruction[2], out=is-j1);
    And(a=alu-iszero, b=instruction[1], out=is-j2);
    And(a=alu-plus, b=instruction[0], out=is-j3);
    // 条件に合致するか
    Or8Way(in[0]=is-j1, in[1]=is-j2, in[2]=is-j3, out=is-jump);
    // なおかつ、C命令か
    And(a=is-jump, b=c-instruction, out=can-jump);
    // PC処理
    // incは基本true
    PC(in=out-a, load=can-jump, inc=true, reset=reset, out[0..14]=pc);
}

ハマったこと

j1, j2, j3の判定

alu-out(aluの出力結果)が[15]=trueの場合、alu-outが負の数と判断できる

=>Notをとれば、正の数かどうか判断できる

==> Not(in=alu-out[15], out=alu-plus)

これは正しくなく、alut-out[15]のNotを取ると、正の数か0かになります。なので、

Not(in=alu-out[15], out=alu-plusorzero)として

And(a=alu-plusorzero, b=temp3, out=alu-plus)で正の数と判定しないといけないので注意

DRegister/ARegisterのload

DRegisterloadは、 instruction[4]を見がちですが、A命令の場合も考慮して、以下のようにしましょう

    // Dレジスタの保存はC命令の場合のみ
    And(a=instruction[4], b=c-instruction, out=isload-d);

    // instruction[4]はd2=>Dに保存するかどうか
    DRegister(in=alu-out, load=isload-d, out=out-d);

ARegisterもだいたい同じ。isloadの条件が少し違うので注意

    // A命令でもC命令でも、結局のところAレジスタをloadするかどうか
    Or(a=instruction[5], b=a-instruction, out=isload-a);

    // instruction[5]はd1=>Aに保存するかどうか
    // outを切り取るのは問題ない??
    ARegister(in=in-a, load=isload-a, out[0..15]=out-a, out[0..14]=addressM);

Sub bus of an internal node may not be used

ALU(... , out=alu-out, out=outM, zr=alu-zr, ng=alu-ng);として

Or8Way(in=alu-out[0..7], out=temp1); // 0判定その1
Or8Way(in=alu-out[8..15], out=temp2); // 0判定その2
Or(a=temp1, b=temp2, out=temp3); // 0判定その3

とすると変数が使われてないかもよと怒られる。変数ちゃんと使ってるのにと思ったら同様のエラーの質問発見。

http://nand2tetris-questions-and-answers-forum.32033.n3.nabble.com/Sub-bus-of-an-internal-node-may-not-be-used-td4031198.html

正しくは、ALU(... , out=alu-out1, out[0..7]=alu-out1, out[8..15]=alu-out2, out=outM, zr=alu-zr, ng=alu-ng);として

    Or8Way(in=alu-out1, out=temp1); // 0判定その1
    Or8Way(in=alu-out2, out=temp2); // 0判定その2
    Or(a=temp1, b=temp2, out=temp3); // 0判定その3

としないといけない

PCのincはtrue?

ハマったわけではないけど、PCのincは常にtrueにしてるがこれでいいものか。。。

   // PC処理
    // incは基本true
    PC(in=out-a, load=can-jump, inc=true, reset=reset, out[0..14]=pc);

低レイヤチョットワカル(4章)

こんにちは。敗北を知った4章です

アセンブリのとこまでやってきたけど心が折れそう

www.oreilly.co.jp

記録用git

vol.1

vol.2

vol.3

vol.4 いまここ

4章 機械語

理論

アドレッシングモード

要求されたメモリのワードに対してそのアドレスを指定する方法

直接アドレッシング

引数にアドレスを直接渡してアクセスすること

LOAD R1, 67
LOAD R1, bar

イミディエイトアドレッシング

引数の値を直接渡す

LOADI R1, 67

関節アドレッシング

Hack機械語の仕様

メモリアドレス空間

  • 命令メモリ

CPUは命令メモリを読み込み実行する。リードオンリー

  • データメモリ

レジスタ

D/Aの2種類。

D=データ値を保存する。

A=データ値とアドレスレジスタと解釈される=値として利用したりアドレスとして利用したりできる

M=Aのアドレスが参照している値(仮想の値。内部でAレジスタが示すアドレスの値を取得するルール

命令

命令は、16bitである。そのうち、先頭の1bitはA/Cの選別に利用する

A命令

Aレジスタに15bitの値を設定する命令

@100 // 100をAレジスタに保存

@500 // 500をAレジスタに保存

@R0  // 先頭のレジスタをAレジスタに保存

C命令

dest = comp;jampで構成された命令。

@sum   //A命令
M=D    //C命令

@index //A命令
D=M    //C命令
D;JEQ  //C命令

シンボル

  • R0~R15は、 アドレス0~16のレジスタとして使用可能
  • @SCREENは、スクリーンの左上のアドレスを示す
  • @KBDは、キーボードの出力アドレスを示す。値は未入力なら0,入力しているなら入力値である

変数シンボル

ユーザーが定義できるシンボル。アドレスは16から始まる

@sum  // A=16である
M=1   // アドレス161が設定されている

実装

まずは掛け算 を実装。

加算減算しかできないので、愚直に実装

@sum
M=0

// 残りカウントをR1で初期化
@R1
D=M
@count 
M=D

(LOOP)
    // チェック
    @count
    D=M
    @END
    D;JEQ

    // 合計値計算
    @R0
    D=M
    @sum
    M=M+D

    // カウントを減らす
    @count
    M=M-1

    // ループ繰り返す
    @LOOP
    0;JMP
(END)

// R2に記録
@sum
D=M
@R2
M=D

@END
0;JMP

次に、スクリーンの入力に合わせて画面を白黒切り替える実装。くそほど詰まった :innocent:

// 初期化
// 最大スクリーンアドレスの計算
@8192 //256*32
D=A
@SCREEN
D=D+A
@MAXADDRESS //最大スクリーンアドレス
M=D

// 以下、無限ループ
(KEY)

@SCREEN
D=A
@address // スクリーンアドレス初期化
M=D

@KBD //キーボード取得
D=M

// 振り分け
@WHITE
D;JEQ
@BLACK
0;JMP

(WHITE)
@color
M=0
@LOOP
0;JMP

(BLACK)
@color
M=-1
@LOOP
0;JMP

// スクリーン書き換え
(LOOP)
@color
D=M

@address
A=M // 値のアドレスに移動
M=D //アドレスの値をcolorに

D=A+1 //アドレスに1加えたところに移動
@address // 必要!!!
M=D // 次に移動するために新たなアドレスを値として保存

@MAXADDRESS
D=M-D //Dが0かどうか

@LOOP
D;JNE

@KEY
0;JMP
(END)

👇この部分で2時間ほどつまった。

@addressには現在のアドレスを入れているが、A=A+1とすると同時に@addressも一つずれると思い込んでいた(実際は、@addressは元のアドレスのまま。動かない。値が動くだけ)

@address
A=M // 値のアドレスに移動
M=D //アドレスの値をcolorに

D=A+1 //アドレスに1加えたところに移動
@address // 必要!!!
M=D // 次に移動するために新たなアドレスを値として保存

低レイヤチョットワカル(3章

こんにちは。3章まできました。

www.oreilly.co.jp

記録用git

vol.1

vol.2

vol.3 いまここ

3章 順序回路

理論

組み合わせ回路

時間に依存しない回路。論理演算や算術演算。

順序回路

時間に依存する回路。一つ以上のDFFが組み込まれている回路。組み合わせ回路が含まれていることもある。

クロック

継続的にマスタクロックが送信する信号。プラットフォームの全ての順序回路に対して送信される。周期。

DFF

D型フリップフロップ

順序回路の基本的要素。

out(t)=in(t-1)

簡単に言えば、クロックが進むまで待機する回路。

1ビットレジスタ

記憶装置。DFFを拡張して情報を格納できる。仕組みはこんな感じ。

f:id:Kouchannel55:20190113152623p:plain

load(ロードビット)という値がキモ。周期の前と後で考える。後を今回、前を前回ととりあえず呼ぶ。

👇load=falseの場合は、前回の自身の出力をそのまま今回の出力とする。この仕組みは、値をレジスタが記憶していると言っても良い。

f:id:Kouchannel55:20190113152629p:plain

👇load=trueの場合は、今回の入力をそのまま今回の出力とする。この仕組みは、値を書き換えると言ってもいい。

f:id:Kouchannel55:20190113152619p:plain

wビットレジスタ

通常のコンピュータであれば、1ビットレジスタでは構成されておらず、並列に処理できるwビットレジスタで構成されている。

👇loadはあくまで1ビットであることに注意

f:id:Kouchannel55:20190113152605p:plain

メモリ

レジスタの集合体。

並列にwビットレジスタが配置されている。

周期ごとに、

  • どのレジスタに対して(address
  • 読み込みか書き込みか(load
  • 入力値(in

を入力として与える。

カウンタ

他の順序回路とは少し違うみたい。

この書籍では、

  • カウンタを0にするかどうか(reset。1の場合、出力を0にする)
  • 読み込みか書き込みか(load。1の場合、前回の出力を今回の出力にする)
  • 加算するか(inc。1の場合、前回の出力に1=00....000001を加算したものを出力にする)

を入力値としている

実装

カウンタ(PC)が難しかった。

2章でじっそうしたものがつかえる

と書いてあったので、ALUが使えるかと思ったが無理だった。

以下、PCの実装。

実装後に他人の実装見てたけど、4~7行ほどが多かった。

ベストアンサー説💯

/**
 * A 16-bit counter with load and reset control bits.
 * if      (reset[t] == 1) out[t+1] = 0
 * else if (load[t] == 1)  out[t+1] = in[t]
 * else if (inc[t] == 1)   out[t+1] = out[t] + 1  (integer addition)
 * else                    out[t+1] = out[t]
 */

CHIP PC {
    IN in[16],load,inc,reset;
    OUT out[16];

    PARTS:

    //2
    // inc
    Add16(a=dffout, b[0]=inc, out=temp-out1);
    Mux4Way16(a=temp-out1, b=false, c=in, d=false, sel[0]=reset, sel[1]=load, out=temp-out2);
    Register(in=temp-out2, load=true, out=out, out=dffout);
}

低レイヤチョットワカル(2章

こんにちは。

つづけて2章いきましょう。

www.oreilly.co.jp

記録用git

vol.1

vol.2 いまここ

2章 ブール算術

オーバーフロー

nビット同士の加算によって、繰り上がりビットが発生すること。発生した繰り上がりビットをオーバーフロービットとも呼ぶ

符号付2進数

0~7を表す2進数は、3ビットで事足りる 111=7

これにマイナスを考慮する場合は、先頭に1ビットを付け加えて、そのビットが0=>正。1=>負とする

例えば、

  • 0111=7
  • 0000=0
  • 1111=-1
  • 1001=-7

正負変換

全反転させて-1

0111=>1000=>1001

半加算器

ふたつのビットの和を求める

全加算器

みっつのビットの和を求める

加算器

2つのnビットの和を求める

ALU

基本的な算術ができる汎用的な演算器。コンピュータによって実装が異なる。

掛け算・割り算などはここで実装しないが、OS側で実装する。

OSとALUがそれぞれどこの算術までカバーするかがキモ

出力は複数指定できる

これは知らなかった。こちらの記事で気づきました。ありがとうございます :bow:

http://blog.tojiru.net/article/426464326.html

実装

ALUくそ難しかった。

zrとngが厄介。

zr

計算結果が0の場合に1を返す。

out=0000000000000000の場合は、

  1. 0000000000000000に分け(out1, out2)
  2. 前半後半でそれぞれorで全て01が一つでも存在しているかチェック(temp1-zr, temp2-zr)
  3. 前半後半それぞれの結果をor(notzr)
  4. notzr=1なら0ではない、Notで反転(zr)
   Or8Way(in=out1, out=temp1-zr);
    Or8Way(in=out2, out=temp2-zr);
    Or(a=temp1-zr, b=temp2-zr, out=notzr);
    Not(in=notzr, out=zr);

ng

符号付2進数で紹介したが、先頭のビットが1なら負の数。よって、合計値を計算したさいの、out[15]=1なら負の数すなはちng=1

Mux16(a=temp-result, b=temp-noresult, sel=no, out=out, out[0..7]=out1, out[8..15]=out2, out[15]=ng);

僕の回答

// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/02/ALU.hdl

/**
 * The ALU (Arithmetic Logic Unit).
 * Computes one of the following functions:
 * x+y, x-y, y-x, 0, 1, -1, x, y, -x, -y, !x, !y,
 * x+1, y+1, x-1, y-1, x&y, x|y on two 16-bit inputs, 
 * according to 6 input bits denoted zx,nx,zy,ny,f,no.
 * In addition, the ALU computes two 1-bit outputs:
 * if the ALU output == 0, zr is set to 1; otherwise zr is set to 0;
 * if the ALU output < 0, ng is set to 1; otherwise ng is set to 0.
 */

// Implementation: the ALU logic manipulates the x and y inputs
// and operates on the resulting values, as follows:
// if (zx == 1) set x = 0        // 16-bit constant
// if (nx == 1) set x = !x       // bitwise not
// if (zy == 1) set y = 0        // 16-bit constant
// if (ny == 1) set y = !y       // bitwise not
// if (f == 1)  set out = x + y  // integer 2's complement addition
// if (f == 0)  set out = x & y  // bitwise and
// if (no == 1) set out = !out   // bitwise not
// if (out == 0) set zr = 1
// if (out < 0) set ng = 1

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute out = x + y (if 1) or x & y (if 0)
        no; // negate the out output?

    OUT 
        out[16], // 16-bit output
        zr, // 1 if (out == 0), 0 otherwise
        ng; // 1 if (out < 0),  0 otherwise

    PARTS:
    Mux16(a=x, sel=zx, out=tempx0);
    Mux16(a=y, sel=zy, out=tempy0);
 
    Not16(in=tempx0, out=notx);
    Not16(in=tempy0, out=noty);
  
    Mux16(a=tempx0, b=notx, sel=nx, out=tempx1);
    Mux16(a=tempy0, b=noty, sel=ny, out=tempy1);

    Add16(a=tempx1, b=tempy1, out=add16);
    And16(a=tempx1, b=tempy1, out=and16);

    Mux16(a=and16, b=add16, sel=f, out=temp-result);

    Not16(in=temp-result, out=temp-noresult);

    Mux16(a=temp-result, b=temp-noresult, sel=no, out=out, out[0..7]=out1, out[8..15]=out2, out[15]=ng);

    Or8Way(in=out1, out=temp1-zr);
    Or8Way(in=out2, out=temp2-zr);
    Or(a=temp1-zr, b=temp2-zr, out=notzr);
    Not(in=notzr, out=zr);
}

低レイヤチョットワカル(1章

低レイヤ

そろそろ勉強しようと思ったので、お勉強。

コンピュータシステムの理論と実装――モダンなコンピュータの作り方をやってみることに

www.oreilly.co.jp

だいぶ前に触ったアセンブリ言語よりもだいぶ低レイヤ。 章ごとにダラダラ学んだことを書いていく予定

1章 ブール論理

ブール関数

3つの表現方法がある

  1. 真理値表
  2. ブール式
  3. 正準表現

Xor = (x and Not y) or (Not x and y)

Nandのみで全ての演算が可能

xORy = (x Nand x) Nand (y Nand y)

論理ゲート=入力と出力のデバイス=ブール関数で表現できる

マルチプレクサ

複数の入力のいずれかを単一の出力にする。どちらかを選ぶかは選択制御Sによる。Sもブール値

「マルチプレクサ」の画像検索結果

Wikipediahttps://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%AB%E3%83%81%E3%83%97%E3%83%AC%E3%82%AF%E3%82%B5

デマルチプレクサ

単一の入力を複数の出力のいずれかにする。どちらかを選ぶかは選択制御Sによる。Sもブール値

多ビットのゲート

入力・出力共に配列であり、それぞれのindexに対して操作するだけ

多入力のゲート

入力が2つ以上のゲート

多入力Or=入力1…nまでのいずれかが1であれば1それ以外は0

多入力マルチプレクサ=複数の入力のいずれかを単一の出力にする。どちらかを選ぶかは選択制御Sによる。Sもブール値。入力の数がnに対して、選択制御は、log2n個必要

HDL

ハードウェアを構築するための言語。HDLで記載して各ゲートをシミュレートする宿題が出た。

HDLでloop使えない😇(単に1章だから使えないのかも?)

マルチプレクサゲート(Mux/4WayMux16)/デマルチプレクサゲート(Dmux)が一番しんどかった。

以下、僕の回答。

/** 
 * Multiplexor:
 * out = a if sel == 0
 *       b otherwise
 */
CHIP Mux {
    IN a, b, sel;
    OUT out;

    PARTS:
    Not(in=sel, out=notsel);
    And(a=a, b=notsel, out=outtemp1);
    And(a=b, b=sel, out=outtemp2);
    Or(a=outtemp1, b=outtemp2, out=out);
}

/**
 * Demultiplexor:
 * {a, b} = {in, 0} if sel == 0
 *          {0, in} if sel == 1
 */
CHIP DMux {
    IN in, sel;
    OUT a, b;

    PARTS:
    Not(in=sel, out=notsel);
    And(a=in, b=notsel, out=a);
    And(a=in, b=sel, out=b);
}

あと、配列の各要素へのアクセスにはまったが、自分と全く同じコードで全く同じ意図の質問があって助かった。

まさか右から読むとは思わなんだ。正しくはhoge = 01の場合、 hoge[0]=1hoge[1]=0らしい。

あと、記録はgitにのっけてます

MFクラウドの勤怠管理をコマンドラインから操作(puppeter)

こんばんは。スマブラ発売間近で胸がワクワクmorifujiです。

最近、MFクラウドの勤怠管理を利用することになりまして、出勤時退勤時には専用webサイトにログインして、ボタンを押さないといけなくなりました。。

めんどくさいので、ヘッドレスブラウザでサクッとつくったので知見を共有します。puppeter久しぶりすぐる

git

https://gitlab.com/morifuji/mf-auto-attendance

筆者環境

環境 バージョン
PC MacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)
仮想環境/ローカル ローカル環境
nodejs v10.7.0
yarn 0.27.5
puppeter 1.11.0

ゴール

  • 以下の手順をヘッドレスブラウザで実行すること
    • 1.ログインページからログイン
    • 2.出勤ボタンまたは退勤ボタンをクリック
    • 3.確認のダイアログに対して入力
  • 出勤・退勤の2つのスクリプトを作成
  • どちらも1コマンドで
  • ID/PASSは外部ファイルに

実装

準備

yarn init mf
yarn add puppeter

package.jsonを編集

{
  "name": "mf",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "puppeteer": "^1.11.0"
  },
  "scripts": {
    "mf-in": "node ./main.js in",
    "mf-out": "node ./main.js out"
  }
}

scriptsで出勤・退勤の2種類のスクリプトを叩きます

スクリプト

スクリプト本体作成

const puppeteer = require('puppeteer');
const config = require('./config.js')

console.log("action: " + process.argv[2])

let isInAttendance  = null
switch(process.argv[2]) {
  case "in": 
    isInAttendance = true
    break;
  case "out":
    isInAttendance = false
    break;
}

if (null === isInAttendance) {
  console.error("引数に `in` または `out` を設定してください")
  return;
}

if (!config.id || !config.pass) {
  console.error("config.jsでID/パスワードを設定してください")
  return;
}

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://payroll.moneyforward.com/session/new', {waitUntil: 'networkidle2'});

  await page.type('input[id=sign_in_session_service_email]', config.id);
  await page.type('input[id=sign_in_session_service_password]', config.pass);

  const inputElement = await page.$('input[name=commit]');
  await inputElement.click();

  await page.waitFor(2000);

  page.on("dialog", (dialog) => {
    dialog.accept();
  });

  let attendanceButton = null
  if (isInAttendance) {
    attendanceButton = await page.$('.btn-attendance');
  } else {
    attendanceButton = await page.$('.btn-leaving');
  }

  await attendanceButton.click();

  // 3秒待つ
  await page.waitFor(3000);
  await browser.close();
})();

ログイン情報を設定

module.exports = {
  id: "",
  pass: ""
}

idとpassに、自身のログイン情報を設定

実行

# 出勤
yarn mf-in

# 退勤
yarn mf-out

もうちょい楽に

毎日実行することを考えるともう少し短くしたいので、エイリアスを設定

echo -e "alias mf-in='cd ~/mf/ && yarn mf-in'\nalias mf-out='cd ~/mf/ && yarn mf-out'" >> ~/.bashrc

:warning: cd ~/mf/の部分は、自分のプロジェクトディレクトリに書き直してください

エイリアス実行

# 出勤
mf-in

# 退勤
mf-out

本体スクリプトざっくり説明

もろもろimport

const puppeteer = require('puppeteer');
const config = require('./config.js')

...

if (!config.id || !config.pass) {
  console.error("config.jsでID/パスワードを設定してください")
  return;
}

ライブラリ(puppeter)をrequireして 設定ファイルから設定値を取得。取得できなかった/false評価ならエラー

引数から、出勤/退勤を判定

console.log("action: " + process.argv[2])

let isInAttendance  = null
switch(process.argv[2]) {
  case "in": 
    isInAttendance = true
    break;
  case "out":
    isInAttendance = false
    break;
}

if (null === isInAttendance) {
  console.error("引数に `in` または `out` を設定してください")
  return;
}

process.argv[2]には、 inまたはoutが入っているので、それをもとに出勤か退勤か判定。

puppeterおまじない

(async () => {

  ...

})();

async/awaitを使いたいので、asyncつけて即時関数にしてる

ページ表示

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://payroll.moneyforward.com/session/new', {waitUntil: 'networkidle2'});

puppeterを起動して、ページを開いてます。 puppeteer.launch()の第3引数には、様々な設定ができます。例えば、{headless: false}とすると、ブラウザが表示された上で操作されます。デバッグに便利ですねー。

page.gotoの第三引数のwaitUntilは、puppeterがどの時点でページ描画完了とするかの設定値です。 ほかにも色々な設定ができます

form入力・submit

  await page.type('input[id=sign_in_session_service_email]', config.id);
  await page.type('input[id=sign_in_session_service_password]', config.pass);

  const inputElement = await page.$('input[name=commit]');
  await inputElement.click();

  await page.waitFor(2000);

クエリセレクタを書いて、そこに第二引数の文字を入力しています。 クリックは少しめんどくさい

waitForでページの描画を待機してます。

出勤ボタンクリック・ダイアログaccept

  page.on("dialog", (dialog) => {
    dialog.accept();
  });

  let attendanceButton = null
  if (isInAttendance) {
    attendanceButton = await page.$('.btn-attendance');
  } else {
    attendanceButton = await page.$('.btn-leaving');
  }

  await attendanceButton.click();

出勤ボタン/退勤ボタンのクリックは先ほどと同じ流れです。

この勤怠システムでは、出勤ボタン/退勤ボタンを押すと、確認のダイアログが表示されます。puppeterがそのダイアログを選択する必要があります。

今回は、ダイアログが表示されるとdialogイベントが発火するので、page.on('{イベント}', {発火する関数})でイベントリスなを設定してます。他にも、いろんなイベントをみることができるみたいです。

    page.on('close')v1.3.0
    page.on('console')v0.9.0
    page.on('dialog')v0.9.0
    page.on('domcontentloaded')v1.1.0
    page.on('error')v0.9.0
    page.on('frameattached')v0.9.0
    page.on('framedetached')v0.9.0
    page.on('framenavigated')v0.9.0
    page.on('load')v0.9.0
    page.on('metrics')v0.12.0
    page.on('pageerror')v0.9.0
    page.on('request')v0.9.0
    page.on('requestfailed')v0.9.0
    page.on('requestfinished')v0.9.0
    page.on('response')v0.9.0
    page.on('workercreated')v1.5.0
    page.on('workerdestroyed')v1.5.0

https://pptr.dev/#?product=Puppeteer&version=v1.11.0&show=api-class-page

終了

  // 3秒待つ
  await page.waitFor(3000);
  await browser.close();

出勤・退勤ボタンのクリックから三秒ほど待機。 そのあとヘッドレスブラウザを閉じます

Docker化

nodejsのイメージ使えばyarnがデフォで入っているのでamazonlinux2を使ってyarnのインストールしてるのは無駄でした。 というか、公式でDockerfile載せてるやん。。。 😭

以下、無駄ですがどうぞ

FROM amazonlinux:2

RUN yum update -y
RUN yum upgrade -y

RUN curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | tee /etc/yum.repos.d/yarn.repo
# nodeのバージョンに注意
RUN curl --silent --location https://rpm.nodesource.com/setup_8.x | bash -
RUN yum install -y nodejs
RUN yum install -y yarn

RUN mkdir /root/mf

WORKDIR /root/mf

ADD . /root/mf

# 環境依存なのでdockerないでyarnさせる
RUN rm -rf node_modules
RUN yarn

# 起動設定
CMD /bin/bash -c "yarn mf-in"

実行

❯ docker build -t mf .
...

❯ docker run mf
yarn run v1.12.3
$ node ./main.js in
action: in
(node:27) UnhandledPromiseRejectionWarning: Error: Failed to launch chrome!
/root/mf/node_modules/puppeteer/.local-chromium/linux-609904/chrome-linux/chrome: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory


TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md

    at onClose (/root/mf/node_modules/puppeteer/lib/Launcher.js:342:14)
    at Interface.helper.addEventListener (/root/mf/node_modules/puppeteer/lib/Launcher.js:331:50)
    at emitNone (events.js:111:20)
    at Interface.emit (events.js:208:7)
    at Interface.close (readline.js:368:8)
    at Socket.onend (readline.js:147:10)
    at emitNone (events.js:111:20)
    at Socket.emit (events.js:208:7)
    at endReadableNT (_stream_readable.js:1064:12)
    at _combinedTickCallback (internal/process/next_tick.js:139:11)
(node:27) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:27) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Done in 0.57s.

無理でした。

metabase3分クッキング

こんばんは。この季節サンダル通勤で足が冷えてくるmorifujiです

今回はmetabaseを使う機会があったのでサクッと構築したlogを置いておきます。

  • クライアントはBIツールが欲しいらしい
  • tableauは高い&運用費も高い(m4.2xlargeぐらい)
  • もっとサクッとかつ簡単なBIツールないんかなー

ということでmetabaseの登場です。

  • slack連携
  • メール連携

を使えばテーブルAのデータがある基準に達したときにslackやメールで通知ができるみたいです、すごいですね

環境

スクリプト

# 以下、ec2インスタンス内。

mkdir metabase
cd metabase
wget http://downloads.metabase.com/v0.30.4/metabase.jar

# javaバージョン確認
java -version

# 1.8>versionだったら
sudo yum update -y
sudo yum install -y java-1.8.0-openjdk.x86_64
# versionを1.8以上に切り替える
sudo alternatives --config java

# 確認
java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)


# `java -jar metabase.jar`でエラーが出たので修正
sudo echo "10.0.0.219 ip-10-0-0-219" | sudo tee -a /etc/hosts


# 実行
java -jar metabase.jar


# localhost:3000/setupにてGUIなセットアップができる!!

課題

  • metabaseのデータがインスタンスの停止などで消える可能性があるので何かしらでバックアップが必要
    • 手段1. ebsでマウントしておく
    • 手段2. metabaseのデータ保存先がデフォルトh2なので、rdsにする
    • 余裕あればまた別記事で書きます。 軽かったのでこの記事に描きました

metabaseのデータバックアップ

データをrdsに保存したいなら、以下の環境変数を設定するとdbに繋げてくれます。以下はmysqlの例です

export MB_DB_TYPE=mysql
export MB_DB_DBNAME=metabase
export MB_DB_PORT=3306
export MB_DB_USER=xxxxxxxxxxxx
export MB_DB_PASS=xxxxxxxxxxxx
export MB_DB_HOST=xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com

# この後にjavaコマンド

バックグラウンド化

java -jar metabase.jarしてもフォアグラウンドなのでssh切ったら止まります。(当たり前)

なのでバックグラウンドで起動できるようにしましょう

nohup java -jar metabase.jar > out.log &

この例だとout.logにログが測れますね

参考

https://www.yoheim.net/blog.php?q=20180101