프레이밍 방식 비교 문서의 그림 생성 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
# 프레이밍 방식 비교 문서의 그림을 만든다: 46_framing-compared_fig1.svg
# 실행: ~/.venvs/vault-plots/bin/python 46_framing-compared_plot.py (matplotlib, numpy 필요)
# 프레임 네 개를 이어 보낸 줄에서 첫 프레임의 끝 표시(또는 Count)가 깨질 때, 받는 쪽이 어디서 자르는지 규칙대로 계산해 그린다.
import os
import matplotlib
matplotlib.use("svg")
import matplotlib.pyplot as plt
from matplotlib import font_manager
from matplotlib.patches import Rectangle
HERE = os.path.dirname(os.path.abspath(__file__))
PREFIX = "46_framing-compared"
INK = "#888888" # 밝은 테마와 어두운 테마 모두에서 보이는 회색
C = ["#3b82c4", "#d9622b", "#3a9a5b", "#9b59b6", "#c49a1b"]
_fonts = {f.name for f in font_manager.fontManager.ttflist}
FONT = next((n for n in ["Apple SD Gothic Neo", "NanumGothic", "Noto Sans CJK KR"] if n in _fonts), "DejaVu Sans")
plt.rcParams.update({
"font.family": FONT, "mathtext.fontset": "dejavusans",
"font.size": 11, "axes.unicode_minus": False, "svg.fonttype": "path", "svg.hashsalt": PREFIX,
"axes.edgecolor": INK, "axes.labelcolor": INK, "xtick.color": INK, "ytick.color": INK,
"text.color": INK, "legend.frameon": False,
"axes.facecolor": "none", "figure.facecolor": "none", "savefig.transparent": True,
"axes.spines.top": False, "axes.spines.right": False,
})
def save(fig, k):
fig.savefig(os.path.join(HERE, f"{PREFIX}_fig{k}.svg"), bbox_inches="tight", metadata={"Date": None})
plt.close(fig)
SYN, SOH, STX, ETX, DLE, CLASS = 0x16, 0x01, 0x02, 0x03, 0x10, 0x81
FLAG = "01111110"
BODIES = [[0x41, 0x42, 0x43, 0x44, 0x45], [0x46, 0x47, 0x48], [0x49, 0x4A, 0x4B, 0x4C], [0x4D, 0x4E]]
def crc16(data):
"""CRC-16/ARC. 예시의 CRC 두 바이트를 만드는 데만 쓴다."""
crc = 0
for b in data:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc
def crc_bytes(body):
c = crc16(body)
return [c >> 8, c & 0xFF]
# ---------------------------------------------------------------- 보내는 쪽: 프레임 네 개를 이어 붙인 줄
def bisync_stream():
s, frames, marks = [], [], []
for i, body in enumerate(BODIES):
st = len(s)
s += [SYN, SYN, SOH, 0x31 + i, STX]
for b in body:
if b in (ETX, DLE):
s.append(DLE)
s.append(b)
marks.append((len(s), 1)); s.append(ETX)
s += crc_bytes(body)
frames.append((st, len(s)))
return s, frames, marks
def ddcmp_stream():
s, frames, marks = [], [], []
for i, body in enumerate(BODIES):
st = len(s)
s += [SYN, SYN, CLASS]
marks.append((len(s), 1)); s.append(len(body))
s += [0x31 + i] + body + crc_bytes(body)
frames.append((st, len(s)))
return s, frames, marks
def bits_of(data):
return "".join(f"{b:08b}" for b in data)
def stuff(bits):
out, run = [], 0
for b in bits:
out.append(b); run = run + 1 if b == "1" else 0
if run == 5:
out.append("0"); run = 0
return "".join(out)
def hdlc_stream():
s, frames, marks = "", [], []
for i, body in enumerate(BODIES):
st = len(s)
marks.append((len(s), 8)); s += FLAG
s += stuff(bits_of([0x31 + i] + body + crc_bytes(body)))
marks.append((len(s), 8)); s += FLAG
frames.append((st, len(s)))
return s, frames, marks
# ---------------------------------------------------------------- 받는 쪽: 규칙대로 잘라 (시작, 끝, 상태) 목록을 만든다. 상태는 ok | bad | skip
def bisync_receive(s):
out, i = [], 0
while i < len(s):
if s[i:i + 3] != [SYN, SYN, SOH]:
j = i
while j < len(s) and s[j:j + 3] != [SYN, SYN, SOH]:
j += 1
out.append((i, j, "skip")); i = j; continue
st, j = i, i + 3
while s[j] != STX: # 헤더
j += 1
j += 1
body = []
while True: # 본문: DLE 다음은 데이터, DLE 없는 ETX에서 끝
if s[j] == DLE:
body.append(s[j + 1]); j += 2
elif s[j] == ETX:
j += 1; break
else:
body.append(s[j]); j += 1
ok = s[j:j + 2] == crc_bytes(body)
j += 2
out.append((st, j, "ok" if ok else "bad")); i = j
return out
def ddcmp_receive(s):
out, i = [], 0
while i < len(s):
if s[i:i + 2] != [SYN, SYN]:
j = i
while j < len(s) and s[j:j + 2] != [SYN, SYN]:
j += 1
out.append((i, j, "skip")); i = j; continue
st, n = i, s[i + 3]
body = s[i + 5:i + 5 + n]
j = i + 5 + n
ok = s[j:j + 2] == crc_bytes(body)
j += 2
out.append((st, j, "ok" if ok else "bad")); i = j
return out
def hdlc_receive(s):
"""깃발 사이를 한 프레임으로 본다. 깃발 하나는 앞 덩어리의 끝이자 다음 덩어리의 시작이다. 빈 덩어리(깃발 두 개가 붙음)는 건너뛴다."""
out = []
first = s.index(FLAG)
pos = first + 8
cur_start = first
while pos < len(s):
data, run, j = [], 0, pos
while True:
b = s[j]
data.append(b); run = run + 1 if b == "1" else 0
if run == 5:
if s[j + 1] == "0":
j += 2; run = 0; continue
if s[j + 1:j + 3] == "10":
break
raise ValueError("1이 일곱 개")
j += 1
flag_start, flag_end = j - 5, j + 3
bits = "".join(data)[:-6]
if bits:
nb = len(bits) // 8
by = [int(bits[8 * k:8 * k + 8], 2) for k in range(nb)]
ok = len(bits) % 8 == 0 and nb >= 3 and by[-2:] == crc_bytes(by[1:-2])
# 정상 덩어리는 끝 깃발까지, 버린 덩어리는 그것을 끝낸 깃발(다음 프레임의 앞 깃발) 직전까지 그린다
out.append((cur_start, flag_end if ok else flag_start, "ok" if ok else "bad"))
cur_start = flag_start
pos = flag_end
return out
def lost(frames, chunks):
"""정상으로 받은 덩어리와 시작·끝이 같지 않은 프레임 번호(1부터)."""
good = {(a, b) for a, b, st in chunks if st == "ok"}
res = []
for k, (a, b) in enumerate(frames):
if not any(a <= ga and gb <= b and gb - ga >= (b - a) - 8 for ga, gb in good):
res.append(k + 1)
return res
# ---------------------------------------------------------------- 네 경우
BS, BF, BM = bisync_stream()
B_BAD = list(BS); B_BAD[BM[0][0]] = 0x07 # 첫 프레임의 ETX 03 → 07 (한 비트 뒤집힘)
B_CH = bisync_receive(B_BAD)
DS, DF, DM = ddcmp_stream()
D_SMALL = list(DS); D_SMALL[DM[0][0]] = 3 # Count 5 → 3 (101 → 011)
D_BIG = list(DS); D_BIG[DM[0][0]] = 5 ^ 16 # Count 5 → 21 (다섯 번째 비트 뒤집힘)
DS_CH = ddcmp_receive(D_SMALL)
DB_CH = ddcmp_receive(D_BIG)
HS, HF, HM = hdlc_stream()
h_end = HM[1][0] # 첫 프레임의 끝 깃발
H_BAD = HS[:h_end + 3] + ("0" if HS[h_end + 3] == "1" else "1") + HS[h_end + 4:] # 깃발의 네 번째 비트 뒤집힘
H_CH = hdlc_receive(H_BAD)
ROWS = [
("BISYNC", "첫 프레임의 ETX가 깨짐", BF, BM[:1], B_CH, len(B_BAD)),
("DDCMP", "Count가 5에서 3으로", DF, DM[:1], DS_CH, len(D_SMALL)),
("DDCMP", "Count가 5에서 21로", DF, DM[:1], DB_CH, len(D_BIG)),
("HDLC", "첫 프레임의 끝 깃발이 깨짐", HF, [HM[1]], H_CH, len(H_BAD)),
]
# ---------------------------------------------------------------- 그림 1
W = 10.0
fig, ax = plt.subplots(figsize=(7.0, 5.2))
for r, (name, what, frames, broken, chunks, total) in enumerate(ROWS):
y = -r * 2.05
sc = W / total
ax.text(-0.25, y + 0.12, name, ha="right", va="center", fontsize=10)
ax.text(-0.25, y - 0.32, what, ha="right", va="center", fontsize=8.5, color=C[1])
for k, (a, b) in enumerate(frames):
ax.add_patch(Rectangle((a * sc, y - 0.25), (b - a) * sc, 0.5, facecolor=C[0], alpha=0.10 + 0.08 * (k % 2),
edgecolor=INK, lw=0.7))
inside = [(m - a) / (b - a) for m, _ in broken if a <= m < b] # 깨진 칸을 피해 이름을 둔다
f = 0.5 if not inside else (0.36 if inside[0] > 0.5 else 0.66)
ax.text((a + f * (b - a)) * sc, y, f"프레임 {k + 1}", ha="center", va="center", fontsize=8.5)
for a, w in broken:
ax.add_patch(Rectangle((a * sc, y - 0.25), max(w * sc, 0.09), 0.5, facecolor=C[1], alpha=0.9, edgecolor=C[1]))
ax.text((a + w / 2) * sc, y + 0.33, "깨짐", ha="center", va="bottom", fontsize=8, color=C[1])
for a, b, st in chunks:
x0, x1 = a * sc + 0.03, b * sc - 0.03
col = {"ok": C[2], "bad": C[1], "skip": INK}[st]
yy = y - 0.42
ax.plot([x0, x0, x1, x1], [yy + 0.1, yy, yy, yy + 0.1], color=col, lw=1.0, ls="-" if st != "skip" else ":")
lab = {"ok": "정상", "bad": "버림", "skip": "건너뜀"}[st]
if st == "skip" and (x1 - x0) < 0.9:
lab = "건너뜀"
ax.text((x0 + x1) / 2, yy - 0.08, lab, ha="center", va="top", fontsize=8, color=col)
n_lost = len(lost(frames, chunks))
ax.text(W + 0.2, y, f"잃은 프레임\n{n_lost}개", ha="left", va="center", fontsize=9,
color=C[1] if n_lost > 1 else INK)
ax.text(0, 0.85, "위: 보낸 프레임 네 개(주황 = 깨진 칸) 아래 괄호: 받는 쪽이 자른 덩어리", ha="left", va="bottom",
fontsize=8.5)
ax.set_xlim(-2.6, W + 1.4)
ax.set_ylim(-3 * 2.05 - 0.95, 1.25)
ax.axis("off")
save(fig, 1)
if __name__ == "__main__":
# 보낸 줄에 우연한 보초가 없다: BISYNC의 CRC·헤더 바이트에 ETX·DLE가 없고, DDCMP 줄의 본문·CRC에 SYN이 없다
for body in BODIES:
assert not set(crc_bytes(body)) & {ETX, DLE, SYN}
assert DS.count(SYN) == 2 * len(BODIES)
# 첫 프레임 CRC는 44 문서와 같은 50 2B
assert crc_bytes(BODIES[0]) == [0x50, 0x2B]
# 깨지지 않으면 네 방식 모두 네 프레임을 정상으로 받는다
assert [st for _, _, st in bisync_receive(BS)] == ["ok"] * 4
assert [st for _, _, st in ddcmp_receive(DS)] == ["ok"] * 4
assert [st for _, _, st in hdlc_receive(HS)] == ["ok"] * 4
# BISYNC: ETX가 깨지면 프레임 2의 ETX까지 이어 읽어 1·2를 잃고, 3·4는 정상
assert [st for _, _, st in B_CH] == ["bad", "ok", "ok"] and B_CH[0][1] == BF[1][1]
assert lost(BF, B_CH) == [1, 2]
# DDCMP: Count 3이면 1만 잃는다(진짜 CRC 두 바이트를 건너뛰고 프레임 2의 SYN에서 다시 시작)
assert [st for _, _, st in DS_CH] == ["bad", "skip", "ok", "ok", "ok"] and DS_CH[1][1] - DS_CH[1][0] == 2
assert lost(DF, DS_CH) == [1]
# DDCMP: Count 21이면 프레임 2를 통째로, 프레임 3의 앞부분까지 삼켜 1·2·3을 잃는다
assert [st for _, _, st in DB_CH] == ["bad", "skip", "ok"] and lost(DF, DB_CH) == [1, 2, 3]
assert DB_CH[0][1] > DF[2][0] # 버린 덩어리가 프레임 3 안에서 끝난다
# HDLC: 끝 깃발이 깨지면 프레임 2의 앞 깃발에서 끝을 찾고, 그 깃발에서 프레임 2가 시작한다. 1만 잃는다
assert [st for _, _, st in H_CH] == ["bad", "ok", "ok", "ok"] and lost(HF, H_CH) == [1]
assert H_BAD[h_end:h_end + 8] == "01101110"
# Count 21 = 프레임 1 본문 5 + CRC 2 + 프레임 2 전체 10 + 프레임 3 앞 4바이트
assert DF[1][1] - DF[1][0] == 10 and 5 + 2 + 10 + 4 == 21 and D_BIG[DF[2][0]:DF[2][0] + 4] == [SYN, SYN, CLASS, 4]
assert 5 ^ (1 << 13) == 8197 and 2 ** 14 - 1 == 16383
# 깃발의 마지막 0이 1로 뒤집히면 1이 일곱 개 → 받는 쪽은 곧바로 오류
last = HM[1][0] + 7
try:
hdlc_receive(HS[:last] + "1" + HS[last + 1:]); raise RuntimeError
except ValueError:
pass
# 본문 41 03 42: 비트 채우기에서는 끼울 0이 없다(1이 많아야 두 개 이어짐)
assert bits_of([0x41, 0x03, 0x42]) == "010000010000001101000010"
assert stuff(bits_of([0x41, 0x03, 0x42])) == bits_of([0x41, 0x03, 0x42])
# 13비트 본문은 채우면 15비트, 깃발까지 31비트
assert len(stuff("0111111111110")) == 15
# 오버헤드의 최악 입력이 서로 다르다: FF 10바이트는 BISYNC에 무해, HDLC에 +16비트(20%)
# DLE 10바이트는 BISYNC에 +10바이트, HDLC에는 0비트
ff, dl = [0xFF] * 10, [DLE] * 10
assert len(stuff(bits_of(ff))) - 80 == 16 and len(stuff(bits_of(dl))) == 80
bis = lambda d: sum(2 if b in (ETX, DLE) else 1 for b in d)
assert bis(ff) == 10 and bis(dl) == 20
print("ALL CHECKS PASSED")