記事の案内

がんばったもの

memo88.hatenablog.com memo88.hatenablog.com

カテゴリ別

Ruby

Ruby カテゴリの記事一覧

LibreOffice

LibreOffice カテゴリの記事一覧

Emacs

Emacs カテゴリの記事一覧

Emacs 関連は一部内容が古くなっている気がします。

RSフリップフロップ

さて、基本的な部品が揃ったところで、 「部品を組み合わせて何か作る」ステージに進みましょう、 まずは組み合わせ回路から……とも考えましたが、もういきなりフリップフロップを作ってしまいます。

というのは、さっさと曳光弾を通してしまいたいからです。

組み合わせ回路はなんとなく分かるんですよね。 多少複雑になっても副作用のない関数みたいなものだと思うので。 そう考えると不確実性はあまりなくて、組み合わせ回路が動かせるシミュレータはなんとなく作れそうな気がします。

一方で、よく分からなかったのが順序回路です。 本とかで回路図を見ててもいまいち実感が持てないんですよね。 「これほんとにそう動くの?」 「ていうかこれ、特に説明ないけどタイミングも考えないといけない……よね? するってぇと……んんー??(うまく言語化できない)」 みたいな。

順序回路についての理解がそんな具合なので、 「順序回路が動かせるシミュレータは作れるのか?」 もまたよく分かりません。 よく分からないのでとにかく作ってみよう、作って動かしてみよう、というのがこれまでの流れです。ここに曳光弾を通して「とにかく動いたぞ」という状態に持っていきたい。


まあ、そんなわけです。 そんなわけで、もうちょっとなので、飛ばしていきます!

細かい説明もすっ飛ばします。ググってください!


RSフリップフロップの回路図です! *1

どの子回路も電池につながっていて、 リレーもスイッチも今まで出てきたもので、どこにもマジックはないですね?

f:id:sonota88:20200328102359p:plain


動かします。最初はこんなふうに勝手に明滅を繰り返します。

f:id:sonota88:20200328102202g:plain

この現象は「発振」というそうです。


ここで、左上か左下のスイッチをクリックして ON にすると発振が止まります。 左上のスイッチを ON にすると次のような状態になります。

f:id:sonota88:20200328095152p:plain


準備ができたので左上と左下のスイッチをクリックして動作を確認します!   このとき、左上と左下のスイッチが両方 ON にならないようにします! *2

f:id:sonota88:20200328094854g:plain

おおぉ…………RSフリップフロップの動きになってるようです!!

ほんとにこれで動くんですね!

ほんとに?   も、もうちょっとゆっくり動かしてみましょうか。

f:id:sonota88:20200328095554g:plain

うーむ、やっぱりちゃんと動いてますね……。

これまで作ってきた部品の動きの組み合わせによって RS フリップフロップのふるまいが実現できています。


というわけで……

……

曳光弾が通りました!!!!

🎉🎉🎉🎉👍👍

いや〜疲れた。


以下の iframe で実際に動かせます。

※ 音量小さめにしていますが音が出ます。
スマホでは全体が表示できないかもしれません。PCブラウザなどで見てください。

こちらも同じものです。
https://sonota88.github.io/kairo-gokko/pages/31/index.html

*1:いくつかバリエーションがあって NAND で作ったりもできるようですが、 なんとなく NOR で作りました。

*2:SRフリップフロップを動かすときのきまり・お約束だそうです。

OR / NOR / XOR

基本的なところでいうと OR がまだ残っています。 作りましょう。

OR

スイッチを並列つなぎにすると OR になります。

f:id:sonota88:20200322061317g:plain

これまでグリッド線を描画していましたが、 見にくいような気がしてきたため、 コメントアウトして描画を止めてみました。


今作った OR だと、下の方のリレーを右側の子回路の内側に引き込まないといけない都合のため配線が微妙にすっきりしないのと、 2つの入力が非対称な印象になるのがちょっと気になりました。

これを解消するために、並列部分の線を交差させたものを思いついて作ってみました。 こうするとスイッチの位置を縦に揃えることができます。 こういうちまちました改良も楽しい。

f:id:sonota88:20200322061649g:plain

これは自分で思いついてやってみたものですが、 こういう工夫はきっと昔の人によって考え尽くされてるんでしょうね……。


もうひとつ、 NAND を使ったタイプも作ってみました。 NAND の入力を not リレーで両方とも反転すると OR になります。

f:id:sonota88:20200322062643g:plain

たしかに OR の挙動になってるけどちょっと不思議な感じがしますね。

NOR

これは OR の出力を反転するだけ。

f:id:sonota88:20200322062927g:plain

「入力が両方とも L のときだけ出力が H」ですね。

こういう場合慣例では L/H で表現するようなのですが、 L, H というのは low, high の略で、電圧の低い・高いを示しています。 つまり、 「(電流は流れているけど)電圧が低い」 「(電流は流れていて)電圧が高い」 なわけで、これまでの 「通電してない」「通電している」と表現してきたモデルとは食い違っています。

が、ここはあまり深く考えず

  • L (low)
    • 電圧が低い
    • 通電していない
  • H (high)
    • 電圧が高い
    • 通電している

という対応になっているのだ、ということにして進めます。

XOR

「基本的なものだからやっておこうか」というのと、 これまで出てきた部品を組み合わせた少し大きなものの例として作ってみた、というものです。

Wikipedia に載っている回路を見ながら作ってみました(英語版に載っていたものとミックスした感じになっています)。

論理回路図っぽく描くとこうです。

f:id:sonota88:20200322081424p:plain

動かした様子です。

f:id:sonota88:20200322070643g:plain

  • (1) 入力が両方とも L
    • 上の NAND の出力は H だが 下の OR の出力が L なので 出力は L
  • (2) 入力が L/H または H/L の場合
    • 下の OR 経由で出力が H になる
  • (3) 入力が両方とも L
    • (2) の場合と同じく下の OR 経由で出力が H になるかと 思いきや、この場合だけ上の NAND の出力が L になるため 一番右上にあるスイッチが OFF にされ、出力が L になる

……のように文章で説明するより、 実際に動かしたときの挙動とにらめっこしたり、 各部分がどうなるか次のように表を書いてみたりする方が分かりやすいと思います。

入力1 入力2 NAND(上) OR(下) 出力
L L H L L
L H H H H
H L H H H
H H L H L

以下の iframe で実際に動かせます。 音量小さめにしていますが音が出ます。

※ ここらへんからスマホでは全体が表示できなくなると思います。PCブラウザなどで見てください……。

こちらも同じものです。
https://sonota88.github.io/kairo-gokko/pages/30/index.html

AND / NAND

さて!

ようやく準備が整いました。ここからがお楽しみですよ!

AND

NOT はすでに作ったので次は AND を作ります。

スイッチを直列つなぎにして equal リレーで切り替え。

f:id:sonota88:20200321080124g:plain

NAND

NOT と AND ができたということは……NAND が作れます!!!!

AND の出力を反転すればよいので、こうですね!!!!

f:id:sonota88:20200321081159g:plain

動きました!!!!   🎉🎉🎉🎉  

やったーこれでコンピュータが作れる *1 !!!!

(プログラムの修正もないし正直書くことがあまりないのですが、 NAND が動くとめでたいのでむりやり盛り上げました! 🎉)


以下の iframe で実際に動かせます。 音量小さめにしていますが音が出ます。

こちらも同じものです。
https://sonota88.github.io/kairo-gokko/pages/29/index.html

*1:たぶんそこまではやりません……

回路の選択

早く先に進みたいのですが、今回も主にブログに貼る都合による修正です。

f:id:sonota88:20200321052209p:plain

今までの作りだと実行時に 1つの回路だけしか動かすことができませんでしたが、 上の画像のようにドロップダウンで複数の回路から選択して切り替えられるようにしたい。

そこで、次のようにしました。

  • 指定した fodg ファイル内のページをすべて data.rb に出力する
  • data.rb から読み込んだら
    • まず $circuits にセットする
    • $circuits の情報を使ってドロップダウンを組み立て
      • 選択肢には LibreOffice Draw のページ名がそのまま表示されるようにする
    • 切替時にそこから選んで $circuit (これまでの circuit に相当)にセットする

ひさしぶりに libo_draw.rb を修正。 ページ名はこれで取れますね。

# module LiboDraw
  # class Page

    def name
      @el["draw:name"]
    end

Circuit#name も追加して、取得したページ名を回路名としてセットします。


data.rb に出力する部分。 doc.pages をすべて Circuit オブジェクトに変換してファイルに出力。

# preprocess.rb

circuits =
  doc.pages.map { |page|
    Circuit.create(
      page.name,
      page.lines,
      page.rectangles
    )
  }

plain = circuits.map { |circuit| circuit.to_plain }

puts "$data_json = <<EOB"
print JSON.pretty_generate(plain)
print "\n"
puts "EOB"

data.rb を読んで復元。

# main.rb

$circuits =
  parse_json($data_json)
    .map { |plain| Circuit.from_plain(plain) }

ドロップダウンを組み立ててイベントハンドリング。

def init_circuit_list(circuits)
  get_els(".circuit_list_container")[0].style.display = "block"

  select_el = get_els(".circuit_list")[0]

  (0...circuits.size).each { |ci|
    circuit = $circuits[ci]
    option_el = Native(`document`).createElement("option")
    option_el.value = ci.to_s
    option_el.textContent = "(%d) %s" % [ci + 1, circuit.name]
    select_el.appendChild(option_el)
  }

  select_el.addEventListener(
    "change",
    lambda { on_select_circuit() },
    false
  )
end

# ...

init_circuit_list($circuits) if browser?

ここは %x{...}JavaScript を書くのではなく、 せっかくなので Opal の練習をしようと思って Ruby に寄せるスタイルで書いてみました。

  • エディタの文法ハイライトが効いて良い
  • Ruby で書けるのは良い。Opal すごい。
    • ただ、「今書いているのどっちだっけ?」となる感じも若干ある。 込み入ってくると混乱するかも。

そもそも main.rb に DOM まわりの記述を書かない方がいいかなという気持ちもあるので、 気になったら後で JavaScript で書き直して分離したりするかもしれません。


というわけで、 data_03.fodg で試すとこのようになりました。

f:id:sonota88:20200321061253g:plain

equal リレー

equal リレーを追加します。 not リレーのときとほとんど同じです。

テスト用回路はこう。

f:id:sonota88:20200320082948p:plain

動かした様子です。

f:id:sonota88:20200320083147g:plain

以下の iframe で実際に動かせます。 音量小さめにしていますが音が出ます。

こちらも同じものです。
https://sonota88.github.io/kairo-gokko/pages/27/index.html


equal リレーと not リレーは重複する処理が多いので、 Unit::Relay という親クラスを作って以下のような継承階層にしました。

- SingleCell
  - Relay
    - EqualRelay
    - NotRelay

あとは描画部分の外枠を描く部分を共通化したりといったところですね。

状態更新処理の部分は強引に共通化すると分かりにくくなりそうな気がしたため、 いったん様子見します。

(……これ書いてるときに気づきましたが、これはポリモーフィズムで書き直せるパターンですね。うーん、ちょっと悩みますが、リファクタリングは後回しにして先に進みます。)


通化以外では、スイッチの状態変化フラグを扱う部分が下記のようになりました。

--- a/main.rb
+++ b/main.rb
@@ -82,7 +82,12 @@ def update_tuden_relay_switch_lamp(circuit)
   circuit.last_update = Time.now
 
   circuit.update_tuden_state()
-  circuit.switch_changed = circuit.update_not_relays_state()
+
+  switch_changed_eq = circuit.update_equal_relays_state()
+  switch_changed_not = circuit.update_not_relays_state()
+  circuit.switch_changed =
+    switch_changed_eq || switch_changed_not
+
   circuit.update_lamps_state()
 
   Sound[:relay].play if circuit.switch_changed

equal リレーと not リレーで異なる音が出るようにしてもおもしろいかもしれませんね。

見た目の修正など

先に進む前に見た目に関する修正をここでやっておきます。

今やらなくてもいいものばかりなのですが (実際にプロトタイプの時はどれも後回しにしていました)、 ブログ記事を書く都合による修正と、 ついでなので見た目まわりをいくつか修正します。


ブログ記事を書く都合というのは、 GIF画像のことです。

f:id:sonota88:20200317053554g:plain

実際に動かして自分で操作する場合はクリック時に効果音によるフィードバックがありますが、 GIF画像だと音が出なくてクリックしたことが分かりにくいなと。

そこで、視覚的なフィードバックも追加します。

# main.rb

class PushHistory
  DURATION_SEC = 0.4
  @@history = []

  def self.add(pos)
    @@history << [pos, Time.now]
  end

  def self.sweep(now)
    @@history = @@history.select { |pos, time|
      now - time <= DURATION_SEC
    }
  end

  def self.get_for_draw(now)
    @@history.map { |pos, time|
      ratio = (now - time) / DURATION_SEC
      [pos, ratio]
    }
  end
end
# class View

  def draw_push_reaction(pos, ratio)
    x = pos.x
    y = pos.y
    alpha = 127 * (1 - ratio)
    r = 0.9 + ratio * 0.3

    @drawer.draw_circle_fill(
      x + 0.5, y + 0.5,
      r,
      [alpha, 150, 150, 150]
    )
  end

クリックされた位置と時刻を記憶しておく PushHistory というクラスを追加し、 そのデータを使って描画するようにしました。

こういうの追加していくとどんどん本質的でないコードで膨れていくので、全体の曳光弾が通るまでは控えめにしておきたいという気持ちもありますが、 「ブログに書く都合」「GIFでは音が出ないので〜」というのはそれなりにリーズナブルな要件なので、まあいいかという感じです。

f:id:sonota88:20200319071438g:plain

ちょっと分かりやすくなったのではないでしょうか?


次に、描画のぼやけ対策です。 これもブログに貼る都合ですね。 それと色を決める際の都合(狙った色にならないことの方が困る)。 Plumo のときは気にせずやっていましたが。

これまでセルのピクセルPPC (pixels per cell) = 30 として作ってきました。 これまでは特に問題はありませんでしたが、 回路が大きくなってくるとスペースの問題で これまでよりも小さめに表示したくなります。

このとき、単純に座標を変換して縮小表示しようとすると、ピクセル座標が半端な値になり、線がぼやけてしまいます。

f:id:sonota88:20200319071835p:plain

そこで、描画処理の際に座標を補正(整数化)することにしました。

( ※ DXOpal では座標を整数で指定した場合にくっきり表示されるようになっています。 参考: DXOpalのWindow.draw_boxのバグを修正した - Qiita

dxopal_sdl.rb を使っているときは補正は不要です(開発用なので適当でよい)。 ブラウザ+DXOpal な環境で動かすときだけ補正されればよいので、 drawer_dxopal.rb で吸収することにしました。 こうすると View クラスでは座標補正について意識せずに済みます。

このような補正用のメソッドを用意して、

# drawer_dxopal.rb
# class Drawer

  def adjust_px(raw_px)
    raw_px.round
  end

描画の直前で補正します。 例として draw_line。 ちょっとごちゃごちゃした感じになってしまいますが、しょうがないかな……。

# drawer_dxopal.rb
# class Drawer

  def draw_line(x1, y1, x2, y2, color)
    Window.draw_line(
      adjust_px(x1 * @ppc), adjust_px(y1 * @ppc),
      adjust_px(x2 * @ppc), adjust_px(y2 * @ppc),
      color
    )
  end

PPC = 25 の場合の比較です。

修正前:
f:id:sonota88:20200319071835p:plain

修正後:
f:id:sonota88:20200319071944p:plain

くっきりしました!


ここまでがブログのための修正。

次に、スイッチの描画がちょっと気になっていたのでちょっといじりたい。 これは単純に「思いついたのでやってみたかった」系のやつですね。

何かというと、現状ではたとえばエッジが OFF で スイッチが ON のとき 次のような見た目になりますが、 エッジが通電していないのになんでスイッチは光っているの?   と不自然に感じます。

f:id:sonota88:20200319073051p:plain

そこで、外枠はエッジと同じ色にしてはどうかと考えました。

つまり、 スイッチの状態は中の部分の位置と色で表現されるのはこれまで通りで、 外枠がエッジの一部っぽく見えるようにします。

f:id:sonota88:20200319073325g:plain

どうでしょうか。 リレーと組み合わせた場合はこんな感じになります。

f:id:sonota88:20200319072326g:plain

機能として不可欠というわけではないのでこの修正はなくても先に進めますが、 見た目がちょっとおもしろくなるので、 これに関してはおもしろさ優先でギミックとして採用してみました。

うーん、こうして見ると、緑色は通電状態に対応しているので中の部分の色は変えた方がいい?   ……とか考えてしまいますが、きりがないのでいったん放置して進みます。


あと、気になっていたのが main.rb の draw メソッドです。

# main.rb

def draw(view, ...)
  view.draw_foo
  view.draw_bar
  ...
end

このパターン、いかにもこれは View の責務でしょという雰囲気が漂っていますね。 この際 View に移動しましょう。

# main.rb
def main_loop(circuit, view)
  # ...

  view.draw(circuit, ...)
end

MVC で言えば circuit がモデル相当でしょう。 モデルをそのままビューに渡して「後はやっといてね」 と丸投げする形になりました。