“use python tk and uart to pass the floor button signal to fpga board and display how the elevator moving in tk, to replace the gen of random input”
↳ replaces the random-stimulus simulation
Reference answer參考解答参考解答Jawaban acuan
Three files and a seam. elevator_core.v is the lift and knows nothing about serial ports; elevator_link.v is the wire and knows nothing about lifts; panel.py draws what it is told. The seam is the call register: the link sets bits in it, the core reads and clears them, and neither needs to know where a press came from.三個檔案,一條接縫。elevator_core.v 是電梯,完全不知道有序列埠這回事;elevator_link.v 是那條線,完全不知道有電梯這回事;panel.py 只畫別人告訴它的東西。接縫就是召喚暫存器:link 在裡面設位元,core 讀它並清掉,而兩邊都不需要知道一次按鍵是從哪裡來的。三つのファイルと一本の継ぎ目。elevator_core.v はエレベータであり、シリアルポートのことを何も知らない。elevator_link.v は線であり、エレベータのことを何も知らない。panel.py は言われたものを描くだけだ。継ぎ目は呼びレジスタで、link がそこにビットを立て、core がそれを読んで消す。どちらも押下がどこから来たかを知る必要はない。Tiga berkas dan sebuah sambungan. elevator_core.v adalah liftnya dan tidak tahu apa-apa tentang port serial; elevator_link.v adalah kabelnya dan tidak tahu apa-apa tentang lift; panel.py menggambar apa yang diberitahukan kepadanya. Sambungannya adalah register panggilan: link menyalakan bit di dalamnya, core membaca dan menghapusnya, dan keduanya tidak perlu tahu dari mana sebuah tekanan datang.
The core. Two functions carry the whole policy: any_beyond answers “is anything waiting past this floor, that way”, and stop_at turns that into “do the doors open here”. Both take the call masks as arguments rather than reading them out of scope — a function that reads a signal it was not given is not re-evaluated when that signal changes inside a continuous assignment, and the simulation then quietly disagrees with the synthesised logic.先看 core。整個排程策略就靠兩個函式:any_beyond 回答「這一層的那個方向之外還有沒有人在等」,而 stop_at 把它變成「這裡要不要開門」。兩個都把召喚遮罩當成引數傳進去,而不是直接去讀外面的訊號——一個函式如果讀了沒有傳給它的訊號,在連續指定裡就不會在那個訊號改變時重新求值,於是模擬會悄悄地和合成出來的邏輯不一致。まず core。方針のすべてを二つの関数が担う:any_beyond は「この階より先、その向きに誰か待っているか」に答え、stop_at はそれを「ここで扉を開けるか」に変える。どちらも呼びマスクをスコープ外から読まずに引数で受け取る——与えられていない信号を読む関数は、連続代入の中でその信号が変わっても再評価されず、シミュレーションが合成結果と静かに食い違う。Core-nya. Dua fungsi memikul seluruh kebijakannya: any_beyond menjawab “adakah yang menunggu melewati lantai ini, ke arah sana”, dan stop_at mengubahnya menjadi “apakah pintunya terbuka di sini”. Keduanya menerima mask panggilan sebagai argumen alih-alih membacanya dari luar lingkup — fungsi yang membaca sinyal yang tidak diberikan kepadanya tidak dievaluasi ulang ketika sinyal itu berubah di dalam sebuah continuous assignment, dan simulasinya lalu diam-diam berbeda dari logika hasil sintesisnya.
// ---------------------------------------------------------------------
// elevator_core.v -- nine floors, one car, LOOK.
//
// Calls arrive as one-cycle pulses and are latched at the full clock rate;
// the machine itself advances only on the 1 Hz tick. A stop clears the call
// that matches the direction the car arrived in. When the doors close with
// nothing ahead, nothing behind, and the opposite call still standing here,
// the car turns its arrow round and opens again -- which is what a real
// lift does when it reaches the top of a run.
//
// The helper functions take the two call masks as arguments rather than
// reading them out of scope. A function that reads a signal it was not
// given is not re-evaluated when that signal changes inside a continuous
// assignment, and the simulation then disagrees with the synthesised logic.
// ---------------------------------------------------------------------
module elevator_core (
input wire clk, // CLOCK2_50
input wire rst, // synchronous, active high
input wire tick, // one clk wide, once a second
input wire call_valid, // one clk wide
input wire call_up, // 1 = the up button, 0 = the down button
input wire [3:0] call_floor, // 1..9, already range-checked by the link
output reg [3:0] floor, // 1..9
output reg dir, // UP / DOWN
output reg [1:0] state,
output wire [9:1] up_q, // up_q[9] can never be set
output wire [9:1] dn_q // dn_q[1] can never be set
);
localparam S_IDLE = 2'd0, S_MOVE = 2'd1, S_DOOR = 2'd2;
localparam UP = 1'b1, DOWN = 1'b0;
localparam [2:0] TRAVEL = 3'd2; // ticks to cross one floor
localparam [2:0] DWELL = 3'd4; // ticks the doors stay open
reg [9:1] up_r, dn_r;
reg [2:0] timer;
assign up_q = up_r;
assign dn_q = dn_r;
wire [3:0] floor_next = (dir == UP) ? floor + 4'd1 : floor - 4'd1;
// one bit per floor, 1-based: bit_at(1) is the least significant bit
function [9:1] bit_at;
input [3:0] f;
begin
bit_at = 9'b1 << (f - 4'd1);
end
endfunction
// "is anything waiting past floor f, looking that way" -- a reduction
// over all nine floors, not nine hand-written branches
function any_beyond;
input [9:1] u;
input [9:1] v;
input [3:0] f;
input d; // 1 = look up, 0 = look down
integer k;
begin
any_beyond = 1'b0;
for (k = 1; k <= 9; k = k + 1)
if ((d ? (k > f) : (k < f)) && (u[k] | v[k]))
any_beyond = 1'b1;
end
endfunction
// "should the car open its doors at floor f while travelling d" --
// the matching call, or the opposite one when this is the last call
// left in that direction
function stop_at;
input [9:1] u;
input [9:1] v;
input [3:0] f;
input d;
begin
stop_at = d ? (u[f] | (v[f] & ~any_beyond(u, v, f, UP)))
: (v[f] | (u[f] & ~any_beyond(u, v, f, DOWN)));
end
endfunction
wire ahead = any_beyond(up_r, dn_r, floor, dir);
wire behind = any_beyond(up_r, dn_r, floor, ~dir);
wire up_there = any_beyond(up_r, dn_r, floor, UP);
wire dn_there = any_beyond(up_r, dn_r, floor, DOWN);
wire here_up = up_r[floor];
wire here_dn = dn_r[floor];
wire here_opp = (dir == UP) ? dn_r[floor] : up_r[floor];
wire any_call = (|up_r) | (|dn_r);
wire stop_nxt = stop_at(up_r, dn_r, floor_next, dir);
wire turn_nxt = ~any_beyond(up_r, dn_r, floor_next, dir);
wire [9:1] set_up = (call_valid & call_up) ? bit_at(call_floor) : 9'd0;
wire [9:1] set_dn = (call_valid & ~call_up) ? bit_at(call_floor) : 9'd0;
// what the call registers become if this cycle clears nothing
wire [9:1] keep_up = up_r | set_up;
wire [9:1] keep_dn = dn_r | set_dn;
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE; floor <= 4'd1; dir <= UP; timer <= 3'd0;
up_r <= 9'd0; dn_r <= 9'd0;
end else begin
// A press lands the moment its byte is decoded, not on the next
// tick. A clear below re-assigns the same register and therefore
// wins -- which is right: if the doors are open at your floor,
// you are already being served.
up_r <= up_r | set_up;
dn_r <= dn_r | set_dn;
if (tick) case (state)
S_IDLE: if (any_call) begin
timer <= 3'd0;
if (here_up | here_dn) begin // serve this floor
state <= S_DOOR;
if (here_up) begin
dir <= UP;
up_r <= keep_up & ~bit_at(floor);
// nothing above: the car turns round here, so this
// one stop serves the down call as well
if (!up_there) dn_r <= keep_dn & ~bit_at(floor);
end else begin
dir <= DOWN;
dn_r <= keep_dn & ~bit_at(floor);
if (!dn_there) up_r <= keep_up & ~bit_at(floor);
end
end else begin // set off
state <= S_MOVE;
dir <= up_there ? UP : DOWN;
end
end
S_MOVE: if (timer == TRAVEL - 3'd1) begin
timer <= 3'd0;
floor <= floor_next; // one floor crossed
if (stop_nxt) begin
state <= S_DOOR;
if (dir == UP) begin
up_r <= keep_up & ~bit_at(floor_next);
if (turn_nxt) dn_r <= keep_dn & ~bit_at(floor_next);
end else begin
dn_r <= keep_dn & ~bit_at(floor_next);
if (turn_nxt) up_r <= keep_up & ~bit_at(floor_next);
end
end
end else timer <= timer + 3'd1;
S_DOOR: if (timer == DWELL - 3'd1) begin // the doors close
timer <= 3'd0;
if (ahead) begin
state <= S_MOVE; // keep going
end else if (here_opp) begin // turn and reopen
dir <= ~dir; // behind == beyond here, the new way
state <= S_DOOR;
if (dir == UP) begin
dn_r <= keep_dn & ~bit_at(floor);
if (!behind) up_r <= keep_up & ~bit_at(floor);
end else begin
up_r <= keep_up & ~bit_at(floor);
if (!behind) dn_r <= keep_dn & ~bit_at(floor);
end
end else if (behind) begin
dir <= ~dir; // turn and set off
state <= S_MOVE;
end else begin
state <= S_IDLE;
end
end else timer <= timer + 3'd1;
default: state <= S_IDLE;
endcase
end
end
endmodule
The clearing rule is where this design is easiest to get wrong, and the comment in S_MOVE is the whole of it: a stop clears the call matching the direction the car arrived in, and the opposite one too when there is nothing further that way, because then the car is turning round at this floor and the one stop serves both. Clear only the arrival direction and a lift answering an up call at 2 and a down call at 8 will shuttle between them for ever without clearing either.清除規則是這個設計最容易做錯的地方,而 S_MOVE 裡那段註解就是全部:一次停靠會清掉和轎廂抵達方向相符的那個召喚,而且在那個方向上已經沒有東西時,連另一個方向的也一起清掉,因為這時轎廂就是要在這一層轉向,這一停同時服務了兩邊。如果只清抵達方向的那一個,一台同時收到 2 樓「上」和 8 樓「下」的電梯,會在兩層之間來回跑到永遠,而且兩個都清不掉。消去の規則こそ、この設計で最も間違えやすいところで、S_MOVE のコメントがその全部だ:停止はかごが到着した向きに一致する呼びを消し、その向きにもう何も無ければ反対向きの呼びも消す。その時かごはこの階で向きを変えるのだから、一度の停止が両方に応じている。到着方向の分だけ消せば、2 階の「上」と 8 階の「下」を受けたエレベータは、どちらも消さないまま二つの階を永遠に往復する。Aturan penghapusan adalah tempat desain ini paling mudah keliru, dan komentar di S_MOVE adalah seluruhnya: sebuah perhentian menghapus panggilan yang cocok dengan arah kedatangan kereta, dan yang berlawanan juga ketika tidak ada lagi apa pun ke arah itu, sebab saat itu kereta sedang berbalik di lantai ini dan satu perhentian melayani keduanya. Hapus hanya arah kedatangannya dan sebuah lift yang menjawab panggilan naik di 2 dan turun di 8 akan hilir-mudik di antara keduanya selamanya tanpa menghapus satu pun.
The link. Nothing here is clever: decode a byte, refuse the two buttons that do not exist, and shift seven bytes out whenever the picture changes or the heartbeat comes round. The one detail that matters is that the frame is latched into fr in a single cycle. Read the live registers byte by byte instead and a frame can carry the floor from before a change and the call mask from after it — which is worse than sending nothing, because the panel will believe it.再看 link。這裡沒有什麼聰明的東西:解一個 byte、拒絕那兩顆不存在的按鈕,然後在畫面有變化或心跳到期時把七個 byte 送出去。唯一重要的細節是:整個框是在同一個 cycle 裡被鎖進 fr 的。改成一個 byte 一個 byte 去讀當下的暫存器,一個框就可能帶著改變前的樓層和改變後的召喚遮罩——那比不送還糟,因為面板會相信它。次に link。ここに賢いところは何も無い:バイトを解釈し、存在しない二つのボタンを拒み、絵が変わるかハートビートが回ってくるたびに 7 バイトを送り出す。重要な細部は一つ、フレームが単一サイクルで fr にラッチされることだ。代わりに生きたレジスタを 1 バイトずつ読めば、変化前の階と変化後の呼びマスクを載せたフレームが出来てしまう——何も送らないより悪い。パネルはそれを信じるからだ。Link-nya. Tidak ada yang cerdik di sini: dekode sebuah byte, tolak dua tombol yang tidak ada, dan geser tujuh byte keluar setiap kali gambarannya berubah atau detak jantungnya tiba. Satu detail yang penting adalah frame-nya dikunci ke fr dalam satu siklus. Baca register yang hidup byte demi byte dan sebuah frame bisa membawa lantai dari sebelum perubahan dan mask panggilan dari sesudahnya — yang lebih buruk daripada tidak mengirim apa pun, karena panelnya akan mempercayainya.
// ---------------------------------------------------------------------
// elevator_link.v -- the wire, and nothing else.
//
// host -> board : 1 0 0 d f3 f2 f1 f0 d = 1 for up, floor 1..9
// 0xFF = reset
// board -> host : A5 floor state up[4:1] up[8:5] dn[5:2] dn[9:6]
//
// Every byte after the A5 carries four bits at most, so bit 7 is clear in
// all of them and the sync byte cannot appear inside a frame.
//
// uart_rx and uart_tx are the pair from task 3. uart_tx is assumed to raise
// busy within one clock of start; the !tx_start term covers that cycle.
// ---------------------------------------------------------------------
module elevator_link #(
parameter CLK_HZ = 50_000_000,
parameter BAUD = 115200,
parameter HB_DIV = CLK_HZ / 2 // heartbeat: twice a second
)(
input wire clk,
input wire rst,
input wire rx,
output wire tx,
output reg call_valid,
output reg call_up,
output reg [3:0] call_floor,
output wire host_reset,
input wire [3:0] floor,
input wire [1:0] state,
input wire dir,
input wire [9:1] up_q,
input wire [9:1] dn_q
);
// ---- receive ------------------------------------------------------
wire [7:0] rx_byte;
wire rx_valid;
uart_rx #(.CLK_HZ(CLK_HZ), .BAUD(BAUD)) u_rx (
.clk(clk), .rst(rst), .rx(rx), .data(rx_byte), .valid(rx_valid));
assign host_reset = rx_valid && (rx_byte == 8'hFF);
wire is_cmd = rx_valid && rx_byte[7] && (rx_byte[6:5] == 2'b00);
wire c_up = rx_byte[4];
wire [3:0] c_floor = rx_byte[3:0];
// The two buttons that do not exist are refused here, at the edge, so
// that nothing downstream can set a bit the car will never clear.
wire c_ok = is_cmd && (c_up ? (c_floor >= 4'd1 && c_floor <= 4'd8)
: (c_floor >= 4'd2 && c_floor <= 4'd9));
always @(posedge clk) begin
if (rst) begin
call_valid <= 1'b0;
end else begin
call_valid <= c_ok;
if (c_ok) begin
call_up <= c_up;
call_floor <= c_floor;
end
end
end
// ---- transmit -----------------------------------------------------
wire [1:0] st_code = (state == 2'd2) ? 2'd3 : // doors open
(state == 2'd1) ? (dir ? 2'd1 : 2'd2) // moving
: 2'd0; // idle
wire [23:0] snap = {floor, st_code, up_q, dn_q};
reg [23:0] shadow;
reg [24:0] hb_cnt;
wire hb = (hb_cnt == HB_DIV - 1);
reg [7:0] fr [0:6];
reg [2:0] idx;
reg sending;
reg [7:0] tx_data;
reg tx_start;
wire tx_busy;
uart_tx #(.CLK_HZ(CLK_HZ), .BAUD(BAUD)) u_tx (
.clk(clk), .rst(rst), .data(tx_data), .start(tx_start),
.busy(tx_busy), .tx(tx));
always @(posedge clk) begin
tx_start <= 1'b0;
if (rst) begin
sending <= 1'b0; idx <= 3'd0; shadow <= 24'd0; hb_cnt <= 25'd0;
end else begin
hb_cnt <= hb ? 25'd0 : hb_cnt + 25'd1;
if (!sending && (snap != shadow || hb)) begin
// Latch the whole frame in one cycle. A frame carrying the
// floor from before a change and the call mask from after it
// is worse than no frame at all.
fr[0] <= 8'hA5;
fr[1] <= {4'd0, floor};
fr[2] <= {6'd0, st_code};
fr[3] <= {4'd0, up_q[4:1]};
fr[4] <= {4'd0, up_q[8:5]};
fr[5] <= {4'd0, dn_q[5:2]};
fr[6] <= {4'd0, dn_q[9:6]};
shadow <= snap;
idx <= 3'd0;
sending <= 1'b1;
end else if (sending && !tx_busy && !tx_start) begin
tx_data <= fr[idx];
tx_start <= 1'b1;
if (idx == 3'd6) sending <= 1'b0;
else idx <= idx + 3'd1;
end
end
end
endmodule
Wiring it up. KEY0 resets the lift, and so does 0xFF from the host — but neither resets the link itself, or the reset byte would knock over the receiver that just decoded it.接起來。KEY0 會重置電梯,主機送來的 0xFF 也會——但兩者都不重置 link 本身,否則那個 reset byte 會把剛剛解出它的接收器一起打翻。配線する。KEY0 はエレベータをリセットし、ホストからの 0xFF も同じだ——ただしどちらも link 自体はリセットしない。さもないとリセットのバイトが、それを解釈したばかりの受信側を倒してしまう。Merangkainya. KEY0 mereset liftnya, dan begitu pula 0xFF dari host — tetapi keduanya tidak mereset link-nya sendiri, atau byte reset itu akan menjatuhkan penerima yang baru saja mendekodenya.
// --- top level: the tick, the two modules, and what you can see ------
wire host_rst, tick;
wire rst = ~KEY[0] | host_rst; // KEY0 is active low; 0xFF also resets
wire [3:0] floor; wire dir; wire [1:0] state;
wire [9:1] up_q, dn_q;
wire call_valid, call_up; wire [3:0] call_floor;
tick_1hz #(.CLK_HZ(50_000_000)) u_tick (
.clk(CLOCK2_50), .rst(rst), .tick(tick));
elevator_core u_car (
.clk(CLOCK2_50), .rst(rst), .tick(tick),
.call_valid(call_valid), .call_up(call_up), .call_floor(call_floor),
.floor(floor), .dir(dir), .state(state), .up_q(up_q), .dn_q(dn_q));
elevator_link #(.CLK_HZ(50_000_000), .BAUD(115200)) u_link (
.clk(CLOCK2_50), .rst(~KEY[0]), // a host reset must not reset the link
.rx(UART_RXD), .tx(UART_TXD),
.call_valid(call_valid), .call_up(call_up), .call_floor(call_floor),
.host_reset(host_rst),
.floor(floor), .state(state), .dir(dir), .up_q(up_q), .dn_q(dn_q));
seg7 u_hex0 (.val(floor), .seg(HEX0)); // the floor, without the panel
assign LEDR[0] = (state == 2'd1) && dir; // going up
assign LEDR[1] = (state == 2'd1) && !dir; // going down
assign LEDR[2] = (state == 2'd2); // doors open
The panel. The serial port lives in its own thread and hands frames over a queue; the GUI thread never blocks on a read. Lamps are set from the mask in the frame, the car is drawn at the floor the frame reported, and the only thing invented locally is the slide between two floors — which snaps to the truth the moment the next frame lands. If nothing arrives for a second the panel says so and stops moving, which is the behaviour the cable-pull in the hand-in is there to demonstrate.再看面板。序列埠活在自己的執行緒裡,用 queue 把框交出來;GUI 執行緒從來不會卡在一個 read 上。燈是由框裡的遮罩設定的,轎廂畫在框所回報的樓層上,唯一在本地「發明」出來的只有兩層之間的滑動——而且下一個框一到,它就跳回真相。一秒之內沒有東西進來,面板就這麼說,並且停止移動;交件要求裡那一下拔線,就是為了展示這個行為。次にパネル。シリアルポートは専用スレッドに住み、キューでフレームを渡す。GUI スレッドが read でブロックすることは無い。ランプはフレーム内のマスクから設定され、かごはフレームが報告した階に描かれ、ローカルで作り出しているのは階と階の間の滑りだけだ——それも次のフレームが届いた瞬間に真実へ吸い付く。一秒何も来なければパネルはそう言い、動くのをやめる。提出物のケーブルを抜く場面は、この挙動を見せるためにある。Panelnya. Port serialnya tinggal di utasnya sendiri dan menyerahkan frame lewat sebuah antrean; utas GUI tidak pernah memblokir pada sebuah read. Lampu disetel dari mask di dalam frame, keretanya digambar di lantai yang dilaporkan frame, dan satu-satunya yang dikarang secara lokal adalah luncuran antara dua lantai — yang langsung menempel pada kebenarannya begitu frame berikutnya mendarat. Kalau tidak ada yang tiba selama sedetik, panelnya mengatakannya dan berhenti bergerak, dan itulah perilaku yang pencabutan kabel pada pengumpulan ada untuk memperagakan.
"""panel.py -- the building's call panel, and the shaft you watch it in.
pip install pyserial
python panel.py COM5
The panel invents nothing. A lamp is lit because the board said that call is
pending; the car is at the floor the board reported. Pull the cable and the
panel says so instead of carrying on.
"""
import queue
import sys
import threading
import time
import tkinter as tk
import serial
BAUD = 115200
SYNC = 0xA5
FLOORS = 9
TRAVEL_S = 2.0 # seconds the board takes to cross one floor
DEAD_AFTER = 1.0 # no frame for this long and the link is down
STATE = {0: "idle", 1: "going up", 2: "going down", 3: "doors open"}
class Link(threading.Thread):
"""Owns the serial port. Nothing in here touches Tk: tkinter is not
thread-safe, and a blocking read on the GUI thread is exactly what
freezes the window when the lift gets busy."""
def __init__(self, port):
super().__init__(daemon=True)
self.port = port
self.rx = queue.Queue() # frames out to the GUI
self.tx = queue.Queue() # presses in from the GUI
self.ser = None
def press(self, floor, up):
self.tx.put(bytes([0x80 | (0x10 if up else 0x00) | floor]))
def run(self):
while True:
try:
if self.ser is None:
self.ser = serial.Serial(self.port, BAUD, timeout=0.05)
while not self.tx.empty():
self.ser.write(self.tx.get_nowait())
head = self.ser.read(1)
if not head:
continue
if head[0] != SYNC:
continue # resync: only the sync byte has bit 7 set
body = self.ser.read(6)
if len(body) != 6 or any(b & 0x80 for b in body):
continue # short read, or we came in mid-frame
self.rx.put({
"t": time.monotonic(),
"floor": body[0],
"state": body[1],
"up": body[2] | (body[3] << 4), # bit f-1, floors 1..8
"dn": body[4] | (body[5] << 4), # bit f-2, floors 2..9
})
except (serial.SerialException, OSError):
if self.ser is not None:
try:
self.ser.close()
except Exception:
pass
self.ser = None
time.sleep(0.5) # the cable is out; try again shortly
class Panel:
HEIGHT, TOP, PITCH = 520, 36, 52
def __init__(self, root, link):
self.root, self.link = root, link
self.last = 0.0 # when the last frame arrived
self.drawn = 1.0 # the floor we are drawing
self.floor, self.state = 1, 0
self.up = self.dn = 0
self.prev = time.monotonic()
root.title("Elevator")
self.canvas = tk.Canvas(root, width=190, height=self.HEIGHT, bg="white",
highlightthickness=0)
self.canvas.grid(row=0, column=0, rowspan=FLOORS, padx=10, pady=10)
for f in range(1, FLOORS + 1):
y = self.y_of(f)
self.canvas.create_line(45, y, 165, y, fill="gray80")
self.canvas.create_text(28, y, text=str(f))
self.car = self.canvas.create_rectangle(60, 0, 150, 0, fill="gray20",
outline="")
# Every floor gets its own pair of hall buttons, and the two that do
# not exist are simply never created.
self.buttons = {}
for f in range(FLOORS, 0, -1):
row = FLOORS - f
tk.Label(root, text="Floor %d" % f, width=7, anchor="e").grid(
row=row, column=1, sticky="e")
for col, (up, glyph) in enumerate(((True, "▲"), (False, "▼"))):
if (up and f == FLOORS) or (not up and f == 1):
continue
b = tk.Button(root, text=glyph, width=3,
command=lambda f=f, u=up: self.link.press(f, u))
b.grid(row=row, column=2 + col, padx=2, pady=1)
self.buttons[(f, up)] = b
self.status = tk.Label(root, anchor="w")
self.status.grid(row=FLOORS, column=0, columnspan=4, sticky="we",
padx=10, pady=(0, 10))
root.after(33, self.frame)
def y_of(self, floor):
return self.TOP + (FLOORS - floor) * self.PITCH
def frame(self):
now = time.monotonic()
dt, self.prev = now - self.prev, now
while True:
try:
f = self.link.rx.get_nowait()
except queue.Empty:
break
self.last = f["t"]
self.floor, self.state = f["floor"], f["state"]
self.up, self.dn = f["up"], f["dn"]
alive = (now - self.last) < DEAD_AFTER
if alive:
# The drawing may slide between floors, but the floor itself is
# whatever the last frame said -- never something we counted.
step = dt / TRAVEL_S
if self.drawn < self.floor:
self.drawn = min(self.floor, self.drawn + step)
elif self.drawn > self.floor:
self.drawn = max(self.floor, self.drawn - step)
for (f, up), b in self.buttons.items():
on = (self.up >> (f - 1) & 1) if up else (self.dn >> (f - 2) & 1)
b.configure(bg="gold" if on else "gray85")
self.status.configure(
text="floor %d %s" % (self.floor, STATE.get(self.state, "?")),
fg="black")
else:
gap = ("no frames yet" if self.last == 0.0
else "no frame for %.1f s" % (now - self.last))
self.status.configure(text="LINK DOWN (%s)" % gap, fg="firebrick")
y = self.y_of(self.drawn)
self.canvas.coords(self.car, 60, y - 18, 150, y + 18)
self.canvas.itemconfigure(self.car, fill="gray20" if alive else "gray75")
self.root.after(33, self.frame)
def main():
port = sys.argv[1] if len(sys.argv) > 1 else "COM5"
link = Link(port)
link.start()
root = tk.Tk()
Panel(root, link)
root.mainloop()
if __name__ == "__main__":
main()
What it looks like on the wire. Idle at floor 1, somebody on 5 presses up; then from 5, somebody on 3 presses down. Heartbeats are left out so the change-driven frames can be read:實際在線上長什麼樣子。閒置在 1 樓,5 樓有人按上;接著從 5 樓出發,3 樓有人按下。為了讓由變化驅動的那些框看得清楚,心跳沒有列出來:線の上では実際どう見えるか。1 階で待機中、5 階の誰かが上を押す。続いて 5 階から、3 階の誰かが下を押す。変化で駆動されるフレームを読めるよう、ハートビートは省いてある:Seperti apa di atas kabelnya. Diam di lantai 1, seseorang di lantai 5 menekan naik; lalu dari lantai 5, seseorang di lantai 3 menekan turun. Detak jantungnya dihilangkan supaya frame yang didorong perubahan bisa dibaca:
host -> board 95 UP at floor 5
board -> host A5 01 01 00 01 00 00 floor 1 going up
board -> host A5 02 01 00 01 00 00 floor 2 going up
board -> host A5 03 01 00 01 00 00 floor 3 going up
board -> host A5 04 01 00 01 00 00 floor 4 going up
board -> host A5 05 03 00 00 00 00 floor 5 doors open <- lamp goes out here
board -> host A5 05 00 00 00 00 00 floor 5 idle
host -> board 83 DOWN at floor 3
board -> host A5 05 02 00 00 02 00 floor 5 going down
board -> host A5 04 02 00 00 02 00 floor 4 going down
board -> host A5 03 03 00 00 00 00 floor 3 doors open
board -> host A5 03 00 00 00 00 00 floor 3 idle
Read it against the traps. The lamp for floor 5 is on from the second frame and gone in the frame that reports the doors open — the panel never decided that, the board did. Every byte after the A5 is below 0x80, so a receiver that joins mid-stream only has to wait for the next A5. And the floor moves one step per frame while the panel draws it sliding, because the picture is allowed to interpolate and the state is not.對照上面的陷阱來讀。5 樓的燈從第二個框開始亮,在回報開門的那個框裡消失——這不是面板決定的,是板子決定的。A5 之後每一個 byte 都小於 0x80,所以一個中途接上來的接收端,只要等下一個 A5 就好。而樓層是一個框一步地變,面板卻把它畫成滑動的,因為可以內插的是畫面,不是狀態。落とし穴と照らし合わせて読むこと。5 階のランプは二つめのフレームから点き、扉が開いたと報せるフレームで消えている——決めたのはパネルではなくボードだ。A5 以降のバイトはすべて 0x80 未満なので、途中から入った受信側は次の A5 を待てばよい。そして階はフレームごとに一段ずつ動くのに、パネルはそれを滑らかに描く。補間してよいのは絵であって、状態ではないからだ。Bacalah dengan jebakannya di tangan. Lampu lantai 5 menyala sejak frame kedua dan hilang di frame yang melaporkan pintunya terbuka — panelnya tidak pernah memutuskan itu, papannya yang memutuskan. Setiap byte setelah A5 di bawah 0x80, jadi penerima yang bergabung di tengah aliran hanya perlu menunggu A5 berikutnya. Dan lantainya bergerak satu langkah per frame sementara panelnya menggambarnya meluncur, karena yang boleh diinterpolasi adalah gambarnya, bukan keadaannya.