Punch cards are almost forgotten today, the last machines to use them were retired back in the 80s. There are some exceptions though, some industrial machines have survived to this day and still use them in small scale.
The biggest drawback with punched cards are that they don't store very much information. Back in the day they were often 72 bytes, and you needed a lot of them for a Fortran or Algol program. They do how ever present some interesting advantages, they har hard to hack, and they can be made from material that's nearly indestructible like stainless steel, or materials that are very easy to destroy like dissolving paper.
Designing a new punch cardMaking a new punch card from scratch lets you decide lots of these things your self. With optical recognition holes are good for 1/0 since they are easy to find with OpenCV. For size 128 bytes is a good value since you can store a good random password that's really safe with that information level. A card size of approximately 120x70mm is small enough to fit in a back pocket or in a nice necklace around your neck for safe keeping.
Some kind of error correction or detection is also a useful when dealing with OpenCV, I there fore added one parity bit that works as a check sum for every byte on the punch card. As for hole size I worked out that 3mm works good with both recognition and 3D-printing.
Reading the punch card is done using a Logitech 920 HD web camera 200mm above a dark table, and the card is made from light grey plastic. A raster is put over the card, and the colour of each point in the matrix is measured. If its dark, there is a hole, and a "1" is put into the matrix. If the value is grey, the value is "0".
The program can move the raster around the image with WASD for x/y moves, and rotate with IO. Its also possible to scale it with GH. Using shift increases the speed of the moves. The resulting scan result looks like this for the card used.
Well, there are a lots of uses for punch cards!
- Safe keeping of large passwords
- Use as key card in locking mechanisms
- Bitcoin wallet
- Educational use to teach about computer history
- Let kids experiment with binary values and learn mathematics
- Encryption keys
There is nothing preventing using bigger cards, perhaps 64 bytes would be possible but that's something like an A4 paper in size. Several cards can also be used to carry a message together.
Even though the card stores 127 bits, if you only use the 94 printable ASCII characters(letters, numbers and special characters) the practical entropy is only 105 bits. And the card is actually 143 bits, but my standard uses 16 of them for parity control. Even 80-bits is considered strong today.
Shared secrets?Not a problem! A card can be split into a number of parts, and only together will the owners have the code. This is unless one person gets only one bit, then that could be calculated from the parity bit.
Data recovery and error correctionwith Reed-Solomon
Using a parity bit enables the loss of one bit on every byte, but the loss can be bigger than that. One possibility is to use Reed-Solomon error correction, that's used in a lot of applications including the Voyager space probes.
Doing some experiments with reedsolo from RSCodec Python library, you can test different layers of security. Using 12 bytes of data on the card allowes for 4 bytes of ECC for a total of 16 bytes. That should provide the possibility to recover two completely corrupted bytes or 4 known missing bytes.
Encoding the 10 byte message "helloworld" with 6 ECC bytes gives a total of 16 bytes. The message coded with Python RSCode is [104, 101, 108, 108, 111, 119, 111, 114, 108, 100, 192, 96, 126, 3, 95, 130]. A few tests shows that it does actually work and can recover missing bits.
Creating the cards
Using the provided Python script you can create keys fast in the SCAD format, and export them to STL for manufacturing.
Improved card, MkII
An improvement was done to the card and read, its now 16 bytes with one checksum byte, the last on the card.
import cv2
import numpy as np
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
print("r=read | wasd=move | WASD=fast | g/h=zoom | G/H=fast | i/o=rotate | q=quit")
base_points = [
(668, 387), (668, 433), (668, 477), (668, 522),
(668, 567), (668, 610), (668, 655), (668, 700), (668, 745)
]
columns = []
# 8 columns (45px)
for i in range(8):
columns.append([(x + i*45, y) for (x, y) in base_points])
# 3 columns (42px)
offset = 8 * 45
for i in range(3):
columns.append([(x + offset + i*42, y) for (x, y) in base_points])
# 5 columns (45px)
offset += 3 * 42
for i in range(5):
columns.append([(x + offset + i*45, y) for (x, y) in base_points])
# Fine adjustments
columns[8] = [(x - 1, y) for (x, y) in columns[8]]
for i in range(10, min(16, len(columns))):
shift = 2
if i == 11:
shift = 4
columns[i] = [(x + shift, y) for (x, y) in columns[i]]
THRESHOLD = 100
poly_pts = np.array([
[520, 317], [1408, 317], [1408, 811],
[593, 811], [516, 741]
], np.float32)
# Movement / scale / rotation
offset_x = 0
offset_y = 0
scale = 1.0
rotation = 0.0 # radians
# Persistent overlay
last_matrix = None
last_ascii = ""
last_status = ""
last_color = (255,255,255)
while True:
ret, frame = cap.read()
if not ret:
break
h, w = frame.shape[:2]
cx, cy = w // 2, h // 2
display = frame.copy()
# ---------- Transform with rotation ----------
def transform(x, y):
# scale first
x0 = (x - cx) * scale
y0 = (y - cy) * scale
# rotate
xr = x0 * np.cos(rotation) - y0 * np.sin(rotation)
yr = x0 * np.sin(rotation) + y0 * np.cos(rotation)
# translate back
x2 = cx + xr + offset_x
y2 = cy + yr + offset_y
return int(x2), int(y2)
# Draw polygon
poly_scaled = np.array([transform(x, y) for (x, y) in poly_pts])
cv2.polylines(display, [poly_scaled.reshape((-1,1,2))], True, (0,255,0), 2)
# Draw columns
shifted_columns = []
for col in columns:
new_col = []
for (x, y) in col:
tx, ty = transform(x, y)
new_col.append((tx, ty))
cv2.circle(display, (tx, ty), 5, (0,0,255), -1)
shifted_columns.append(new_col)
# ---------- Input ----------
key = cv2.waitKey(1) & 0xFF
# Move
if key == ord('w'):
offset_y -= 1
elif key == ord('s'):
offset_y += 1
elif key == ord('a'):
offset_x -= 1
elif key == ord('d'):
offset_x += 1
# Fast move
elif key == ord('W'):
offset_y -= 5
elif key == ord('S'):
offset_y += 5
elif key == ord('A'):
offset_x -= 5
elif key == ord('D'):
offset_x += 5
# Zoom
elif key == ord('g'):
scale *= 0.995
elif key == ord('h'):
scale /= 0.995
# Fast zoom
elif key == ord('G'):
scale *= 0.97
elif key == ord('H'):
scale /= 0.97
# Rotation
elif key == ord('i'):
rotation -= 0.002
elif key == ord('o'):
rotation += 0.002
# Fast rotation
elif key == ord('I'):
rotation -= 0.01
elif key == ord('O'):
rotation += 0.01
# ---------- Read ----------
elif key == ord('r'):
matrix = []
ascii_chars = []
decimal_values = []
all_ok = True
for col in shifted_columns:
bits = []
for (x, y) in col:
if 0 <= x < w and 0 <= y < h:
b, g, r_pix = frame[y, x]
gray = int(0.299*r_pix + 0.587*g + 0.114*b)
bits.append(1 if gray < THRESHOLD else 0)
else:
bits.append(0)
matrix.append(bits)
parity = bits[0]
data_bits = bits[1:]
value = sum(b << i for i, b in enumerate(data_bits))
decimal_values.append(value)
ones = sum(data_bits)
expected = 0 if ones % 2 == 0 else 1
if parity != expected:
all_ok = False
ascii_chars.append(chr(33 + (value % 94)))
last_matrix = matrix
last_ascii = ''.join(ascii_chars)
print("Decimal lista:")
print(decimal_values)
print("lösenord:", last_ascii)
if all_ok:
last_status = "PARITY OK"
last_color = (0,255,0)
else:
last_status = "PARITY ERROR"
last_color = (0,0,255)
elif key == ord('q'):
break
# ---------- Overlay ----------
if last_matrix is not None:
cell = 18
rows = 9
cols = len(last_matrix)
tx = w - cols * cell - 20
ty = 20
cv2.rectangle(
display,
(tx-10, ty-10),
(tx + cols*cell + 10, ty + rows*cell + 60),
(0,0,0),
-1
)
for c in range(cols):
for r in range(rows):
val = last_matrix[c][r]
color = (0,255,0) if val else (100,100,100)
cv2.putText(
display,
str(val),
(tx + c*cell, ty + r*cell + 14),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
color,
1
)
cv2.putText(
display,
last_ascii,
(tx, ty + rows*cell + 20),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(255,255,255),
2
)
cv2.putText(
display,
last_status,
(tx, ty + rows*cell + 45),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
last_color,
2
)
cv2.imshow("Camera", display)
cap.release()
cv2.destroyAllWindows()
import cv2
import numpy as np
from reedsolo import RSCodec
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
print("r=ASCII read | p=RS decode | wasd=move | WASD=fast | g/h=zoom | G/H=fast | i/o=rotate | q=quit")
# ==========================================================
# BASE POINTS
# ==========================================================
base_points = [
(668, 387),
(668, 433),
(668, 477),
(668, 522),
(668, 567),
(668, 610),
(668, 655),
(668, 700),
(668, 745)
]
# ==========================================================
# BUILD COLUMNS
# ==========================================================
columns = []
# 8 columns (45 px spacing)
for i in range(8):
columns.append([
(x + i * 45, y)
for (x, y) in base_points
])
# 3 columns (42 px spacing)
offset = 8 * 45
for i in range(3):
columns.append([
(x + offset + i * 42, y)
for (x, y) in base_points
])
# 5 columns (45 px spacing)
offset += 3 * 42
for i in range(5):
columns.append([
(x + offset + i * 45, y)
for (x, y) in base_points
])
# ==========================================================
# FINE CALIBRATION
# ==========================================================
# Column 9 slightly left
columns[8] = [
(x - 1, y)
for (x, y) in columns[8]
]
# Columns 11-16 slightly right
for i in range(10, min(16, len(columns))):
shift = 2
# Column 12 extra shift
if i == 11:
shift = 4
columns[i] = [
(x + shift, y)
for (x, y) in columns[i]
]
# ==========================================================
# SETTINGS
# ==========================================================
THRESHOLD = 100
poly_pts = np.array([
[520, 317],
[1408, 317],
[1408, 811],
[593, 811],
[516, 741]
], np.float32)
# ==========================================================
# TRANSFORM VARIABLES
# ==========================================================
offset_x = 0
offset_y = 0
scale = 1.0
rotation = 0.0
# ==========================================================
# OVERLAY DATA
# ==========================================================
last_matrix = None
last_ascii = ""
last_status = ""
last_color = (255, 255, 255)
# ==========================================================
# MAIN LOOP
# ==========================================================
while True:
ret, frame = cap.read()
if not ret:
break
h, w = frame.shape[:2]
cx = w // 2
cy = h // 2
display = frame.copy()
# ======================================================
# TRANSFORM FUNCTION
# ======================================================
def transform(x, y):
# Scale around center
x0 = (x - cx) * scale
y0 = (y - cy) * scale
# Rotate
xr = (
x0 * np.cos(rotation)
- y0 * np.sin(rotation)
)
yr = (
x0 * np.sin(rotation)
+ y0 * np.cos(rotation)
)
# Translate
x2 = cx + xr + offset_x
y2 = cy + yr + offset_y
return int(x2), int(y2)
# ======================================================
# DRAW POLYGON
# ======================================================
poly_scaled = np.array([
transform(x, y)
for (x, y) in poly_pts
])
cv2.polylines(
display,
[poly_scaled.reshape((-1, 1, 2))],
True,
(0, 255, 0),
2
)
# ======================================================
# DRAW SAMPLE POINTS
# ======================================================
shifted_columns = []
for col in columns:
new_col = []
for (x, y) in col:
tx, ty = transform(x, y)
new_col.append((tx, ty))
cv2.circle(
display,
(tx, ty),
5,
(0, 0, 255),
-1
)
shifted_columns.append(new_col)
# ======================================================
# INPUT
# ======================================================
key = cv2.waitKey(1) & 0xFF
# ------------------------------------------------------
# MOVE
# ------------------------------------------------------
if key == ord('w'):
offset_y -= 1
elif key == ord('s'):
offset_y += 1
elif key == ord('a'):
offset_x -= 1
elif key == ord('d'):
offset_x += 1
# ------------------------------------------------------
# FAST MOVE
# ------------------------------------------------------
elif key == ord('W'):
offset_y -= 5
elif key == ord('S'):
offset_y += 5
elif key == ord('A'):
offset_x -= 5
elif key == ord('D'):
offset_x += 5
# ------------------------------------------------------
# ZOOM
# ------------------------------------------------------
elif key == ord('g'):
scale *= 0.995
elif key == ord('h'):
scale /= 0.995
# ------------------------------------------------------
# FAST ZOOM
# ------------------------------------------------------
elif key == ord('G'):
scale *= 0.97
elif key == ord('H'):
scale /= 0.97
# ------------------------------------------------------
# ROTATION
# ------------------------------------------------------
elif key == ord('i'):
rotation -= 0.002
elif key == ord('o'):
rotation += 0.002
# ------------------------------------------------------
# FAST ROTATION
# ------------------------------------------------------
elif key == ord('I'):
rotation -= 0.01
elif key == ord('O'):
rotation += 0.01
# ======================================================
# ASCII CARD MODE
# ======================================================
elif key == ord('r'):
matrix = []
ascii_chars = []
all_ok = True
for col in shifted_columns:
bits = []
for (x, y) in col:
if 0 <= x < w and 0 <= y < h:
b, g, r_pix = frame[y, x]
gray = int(
0.299 * r_pix +
0.587 * g +
0.114 * b
)
bit = 1 if gray < THRESHOLD else 0
bits.append(bit)
else:
bits.append(0)
matrix.append(bits)
# row 0 = parity
# row 1 = LSB
parity = bits[0]
data_bits = bits[1:]
value = 0
for i, b in enumerate(data_bits):
value |= (b << i)
# even parity
ones = sum(data_bits)
expected = 1 if ones % 2 == 0 else 0
if parity != expected:
all_ok = False
ascii_chars.append(
chr(33 + (value % 94))
)
last_matrix = matrix
last_ascii = ''.join(ascii_chars)
if all_ok:
last_status = "PARITY OK"
last_color = (0, 255, 0)
else:
last_status = "PARITY ERROR"
last_color = (0, 0, 255)
print("\nASCII:", last_ascii)
# ======================================================
# REED SOLOMON MODE
# ======================================================
elif key == ord('p'):
rs_values = []
rs_matrix = []
print("\n--- REED SOLOMON READ ---")
for col_idx, col in enumerate(shifted_columns):
bits = []
for (x, y) in col:
if 0 <= x < w and 0 <= y < h:
b, g, r_pix = frame[y, x]
gray = int(
0.299 * r_pix +
0.587 * g +
0.114 * b
)
bit = 1 if gray < THRESHOLD else 0
bits.append(bit)
else:
bits.append(0)
rs_matrix.append(bits)
# row 0 = parity
# rows 1-8 = data
data_bits = bits[1:]
# LSB at bottom
reversed_bits = list(reversed(data_bits))
value = 0
for i, b in enumerate(reversed_bits):
value |= (b << i)
rs_values.append(value)
print(f"Column {col_idx}: {value}")
print("RS Values:", rs_values)
# --------------------------------------------------
# REED SOLOMON DECODE
# --------------------------------------------------
try:
rsc = RSCodec(6)
decoded = rsc.decode(
bytearray(rs_values)
)
decoded_bytes = decoded[0]
decoded_text = decoded_bytes.decode(
'utf-8',
errors='replace'
)
print("Decoded text:", decoded_text)
last_matrix = rs_matrix
last_ascii = decoded_text
last_status = "RS DECODE OK"
last_color = (0, 255, 255)
except Exception as e:
print("RS decode failed:", e)
last_matrix = rs_matrix
last_ascii = "RS DECODE FAILED"
last_status = "RS ERROR"
last_color = (0, 0, 255)
# ======================================================
# QUIT
# ======================================================
elif key == ord('q'):
break
# ======================================================
# OVERLAY
# ======================================================
if last_matrix is not None:
cell = 18
rows = 9
cols = len(last_matrix)
tx = w - cols * cell - 20
ty = 20
# Background
cv2.rectangle(
display,
(tx - 10, ty - 10),
(tx + cols * cell + 10,
ty + rows * cell + 60),
(0, 0, 0),
-1
)
# Matrix
for c in range(cols):
for r in range(rows):
val = last_matrix[c][r]
color = (
(0, 255, 0)
if val else
(100, 100, 100)
)
cv2.putText(
display,
str(val),
(tx + c * cell,
ty + r * cell + 14),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
color,
1
)
# Result text
cv2.putText(
display,
last_ascii,
(tx, ty + rows * cell + 20),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(255, 255, 255),
2
)
# Status
cv2.putText(
display,
last_status,
(tx, ty + rows * cell + 45),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
last_color,
2
)
cv2.imshow("Camera", display)
cap.release()
cv2.destroyAllWindows()
Card creation code
Pythonimport random
# =========================
# PARAMETERS
# =========================
CARD_THICKNESS = 3
FRAME_HEIGHT = 1
FRAME_WIDTH = 3
HOLE_DIAMETER = 3
HOLE_RADIUS = HOLE_DIAMETER / 2
PITCH = 6
COLS = 16
ROWS = 9 # parity + 8 bits
MARGIN = 6
SIDE_TAB_WIDTH = 10
SIDE_TAB_HEIGHT = 3
# hole in left side tab
SIDE_TAB_HOLE_DIAMETER = 5
# move hole upward
SIDE_TAB_HOLE_Y_OFFSET = 3
# =========================
# CARD DIMENSIONS
# =========================
card_w = (
MARGIN * 2
+ (COLS - 1) * PITCH
+ HOLE_DIAMETER
)
card_h = (
MARGIN * 2
+ (ROWS - 1) * PITCH
+ HOLE_DIAMETER
)
# =========================
# READ INPUT DATA
# =========================
values = []
print("")
print("1 = Enter custom bytes")
print("2 = Generate random bytes")
print("")
mode = input("Selection: ").strip()
# ------------------------------------------
# RANDOM BYTES
# ------------------------------------------
if mode == "2":
values = [
random.randint(0, 255)
for _ in range(16)
]
print("")
print("Random bytes generated:")
print(values)
# ------------------------------------------
# MANUAL INPUT
# ------------------------------------------
else:
print("")
print("Enter 16 values between 0 and 255")
for i in range(16):
while True:
try:
v = int(input(f"Byte {i + 1}: "))
if 0 <= v <= 255:
values.append(v)
break
print("Value must be between 0 and 255")
except:
print("Invalid value")
# =========================
# GENERATE SCAD
# =========================
scad = []
scad.append("$fn=48;")
scad.append("")
scad.append("difference() {")
# ==========================================
# UNION
# ==========================================
scad.append(" union() {")
# ------------------------------------------
# BASE CARD
# ------------------------------------------
scad.append(
f" cube([{card_w}, {card_h}, {CARD_THICKNESS}]);"
)
# ------------------------------------------
# FRAME
# ------------------------------------------
# top
scad.append(
f"""
translate([0,{card_h - FRAME_WIDTH},{CARD_THICKNESS}])
cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
"""
)
# bottom
scad.append(
f"""
translate([0,0,{CARD_THICKNESS}])
cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
"""
)
# left
scad.append(
f"""
translate([0,0,{CARD_THICKNESS}])
cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
"""
)
# right
scad.append(
f"""
translate([{card_w - FRAME_WIDTH},0,{CARD_THICKNESS}])
cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
"""
)
# ------------------------------------------
# LEFT SIDE TAB
# ------------------------------------------
scad.append(
f"""
translate([-{SIDE_TAB_WIDTH},0,0])
cube([{SIDE_TAB_WIDTH},{card_h},{SIDE_TAB_HEIGHT}]);
"""
)
# end union
scad.append(" }")
# ==========================================
# CARD HOLES
# ==========================================
for c in range(COLS):
value = values[c]
# LSB first
bits = [
(value >> i) & 1
for i in range(8)
]
# parity = 1 if odd number of 1 bits
parity = 1 if sum(bits) % 2 == 1 else 0
full = [parity] + bits
for r in range(ROWS):
if full[r] == 1:
x = (
MARGIN
+ HOLE_RADIUS
+ c * PITCH
)
# parity row at top
y = (
MARGIN
+ HOLE_RADIUS
+ (ROWS - 1 - r) * PITCH
)
scad.append(
f"""
translate([{x},{y},-1])
cylinder(
h={CARD_THICKNESS + FRAME_HEIGHT + 10},
r={HOLE_RADIUS}
);
"""
)
# ==========================================
# HOLE IN LEFT TAB
# ==========================================
hole_x = -SIDE_TAB_WIDTH / 2
hole_y = (
card_h
- MARGIN
- SIDE_TAB_HOLE_DIAMETER
+ SIDE_TAB_HOLE_Y_OFFSET
)
scad.append(
f"""
translate([{hole_x},{hole_y},-1])
cylinder(
h={CARD_THICKNESS + FRAME_HEIGHT + 10},
r={SIDE_TAB_HOLE_DIAMETER / 2}
);
"""
)
# ==========================================
# END DIFFERENCE
# ==========================================
scad.append("}")
# =========================
# SAVE FILE
# =========================
filename = "punchcard.scad"
with open(filename, "w", encoding="utf-8") as f:
f.write("\n".join(scad))
# =========================
# DISPLAY BYTE INFO
# =========================
print("")
print("Byte information:")
print("")
for i, value in enumerate(values):
bits = [
(value >> b) & 1
for b in range(8)
]
parity = 1 if sum(bits) % 2 == 1 else 0
print(
f"Byte {i+1:02d}: "
f"decimal={value:3d} "
f"parity={parity} "
f"bits(LSB->MSB)="
f"{''.join(map(str, bits))}"
)
print("")
print(f"SCAD file created: {filename}")
print("")
print("Open the file in OpenSCAD")
print("Press F6")
print("Then export as STL")
import random
# =========================
# PARAMETERS
# =========================
CARD_THICKNESS = 2
FRAME_HEIGHT = 1
FRAME_WIDTH = 3
HOLE_DIAMETER = 3
HOLE_RADIUS = HOLE_DIAMETER / 2
PITCH = 4
# 16 data bytes + 1 checksum byte
COLS = 17
# 8 bits only
ROWS = 8
MARGIN = 4
# ------------------------------------------
# LEFT HANDLE / TAB
# ------------------------------------------
SIDE_TAB_WIDTH = 16
SIDE_TAB_HEIGHT = 3
# Hole in left handle
SIDE_TAB_HOLE_DIAMETER = 6
SIDE_TAB_HOLE_Y_OFFSET = 3
# ------------------------------------------
# STICKER RECESS
# ------------------------------------------
STICKER_RECESS_WIDTH = 12
STICKER_RECESS_HEIGHT = 25
STICKER_RECESS_DEPTH = 0.5
# Positive values move recess downward
STICKER_RECESS_Y_OFFSET = 5
# =========================
# CARD DIMENSIONS
# =========================
card_w = (
MARGIN * 2
+ (COLS - 1) * PITCH
+ HOLE_DIAMETER
)
card_h = (
MARGIN * 2
+ (ROWS - 1) * PITCH
+ HOLE_DIAMETER
)
# =========================
# READ INPUT DATA
# =========================
data_values = []
print("")
print("1 = Enter custom bytes")
print("2 = Generate random bytes")
print("")
mode = input("Selection: ").strip()
# ------------------------------------------
# RANDOM BYTES
# ------------------------------------------
if mode == "2":
data_values = [
random.randint(0, 255)
for _ in range(16)
]
print("")
print("Random bytes generated:")
print(data_values)
# ------------------------------------------
# MANUAL INPUT
# ------------------------------------------
else:
print("")
print("Enter 16 values between 0 and 255")
for i in range(16):
while True:
try:
v = int(input(f"Byte {i + 1}: "))
if 0 <= v <= 255:
data_values.append(v)
break
print("Value must be between 0 and 255")
except:
print("Invalid value")
# =========================
# CHECKSUM BYTE
# =========================
checksum = sum(data_values) % 256
values = data_values + [checksum]
print("")
print(f"Checksum byte: {checksum}")
# =========================
# GENERATE SCAD
# =========================
scad = []
scad.append("$fn=48;")
scad.append("")
scad.append("difference() {")
# ==========================================
# MAIN UNION
# ==========================================
scad.append(" union() {")
# ------------------------------------------
# BASE CARD
# ------------------------------------------
scad.append(
f"""
cube([{card_w}, {card_h}, {CARD_THICKNESS}]);
"""
)
# ------------------------------------------
# FRAME
# ------------------------------------------
# top
scad.append(
f"""
translate([0,{card_h - FRAME_WIDTH},{CARD_THICKNESS}])
cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
"""
)
# bottom
scad.append(
f"""
translate([0,0,{CARD_THICKNESS}])
cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
"""
)
# left
scad.append(
f"""
translate([0,0,{CARD_THICKNESS}])
cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
"""
)
# right
scad.append(
f"""
translate([{card_w - FRAME_WIDTH},0,{CARD_THICKNESS}])
cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
"""
)
# ------------------------------------------
# LEFT HANDLE
# ------------------------------------------
scad.append(
f"""
translate([-{SIDE_TAB_WIDTH},0,0])
cube([{SIDE_TAB_WIDTH},{card_h},{SIDE_TAB_HEIGHT}]);
"""
)
# end union
scad.append(" }")
# ==========================================
# CARD DATA HOLES
# ==========================================
for c in range(COLS):
value = values[c]
# LSB first
bits = [
(value >> i) & 1
for i in range(8)
]
for r in range(ROWS):
if bits[r] == 1:
x = (
MARGIN
+ HOLE_RADIUS
+ c * PITCH
)
y = (
MARGIN
+ HOLE_RADIUS
+ (ROWS - 1 - r) * PITCH
)
scad.append(
f"""
translate([{x},{y},-1])
cylinder(
h={CARD_THICKNESS + FRAME_HEIGHT + 10},
r={HOLE_RADIUS}
);
"""
)
# ==========================================
# HOLE IN LEFT HANDLE
# ==========================================
hole_x = -SIDE_TAB_WIDTH / 2
hole_y = (
card_h
- MARGIN
- SIDE_TAB_HOLE_DIAMETER
+ SIDE_TAB_HOLE_Y_OFFSET
)
scad.append(
f"""
translate([{hole_x},{hole_y},-1])
cylinder(
h={CARD_THICKNESS + FRAME_HEIGHT + 10},
r={SIDE_TAB_HOLE_DIAMETER / 2}
);
"""
)
# ==========================================
# STICKER RECESS
# ==========================================
recess_x = -SIDE_TAB_WIDTH + 2
recess_y = (
(card_h / 2)
- (STICKER_RECESS_HEIGHT / 2)
- STICKER_RECESS_Y_OFFSET
)
scad.append(
f"""
translate([
{recess_x},
{recess_y},
{CARD_THICKNESS - STICKER_RECESS_DEPTH}
])
cube([
{STICKER_RECESS_WIDTH},
{STICKER_RECESS_HEIGHT},
{STICKER_RECESS_DEPTH + 1}
]);
"""
)
# ==========================================
# END DIFFERENCE
# ==========================================
scad.append("}")
# =========================
# SAVE FILE
# =========================
filename = "punchcard.scad"
with open(filename, "w", encoding="utf-8") as f:
f.write("\n".join(scad))
# =========================
# DISPLAY BYTE INFO
# =========================
print("")
print("Byte information:")
print("")
for i, value in enumerate(values):
bits = [
(value >> b) & 1
for b in range(8)
]
label = "CHECKSUM" if i == 16 else f"BYTE {i+1:02d}"
print(
f"{label}: "
f"decimal={value:3d} "
f"bits(LSB->MSB)="
f"{''.join(map(str, bits))}"
)
print("")
print(f"SCAD file created: {filename}")
print("")
print("Open the file in OpenSCAD")
print("Press F6")
print("Then export as STL")
import cv2
import numpy as np
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
print("r=read | wasd=move | WASD=fast | g/h=zoom | G/H=fast | i/o=rotate | q=quit")
# ---------- 8 bits per column ----------
base_points = [
(668, 387), (668, 433), (668, 477), (668, 522),
(668, 567), (668, 610), (668, 655), (668, 700)
]
columns = []
# ---------- 17 columns ----------
for i in range(8):
columns.append([(x + i * 45, y) for (x, y) in base_points])
offset = 8 * 45
for i in range(3):
columns.append([(x + offset + i * 42, y) for (x, y) in base_points])
offset += 3 * 42
for i in range(6):
columns.append([(x + offset + i * 45, y) for (x, y) in base_points])
# Fine adjustments
columns[8] = [(x - 1, y) for (x, y) in columns[8]]
for i in range(10, min(17, len(columns))):
shift = 2
if i == 11:
shift = 4
columns[i] = [(x + shift, y) for (x, y) in columns[i]]
THRESHOLD = 100
# ---------- Rectangle card ----------
poly_pts = np.array([
[430, 317],
[1435, 317],
[1435, 771],
[430, 771]
], np.float32)
offset_x = 0
offset_y = 0
scale = 1.0
rotation = 0.0
last_matrix = None
last_status = ""
last_color = (255, 255, 255)
decimal_values = []
checksum_value = 0
while True:
ret, frame = cap.read()
if not ret:
break
h, w = frame.shape[:2]
cx, cy = w // 2, h // 2
display = frame.copy()
# ---------- Left-side decimal overlay ----------
if last_matrix is not None:
overlay_x = 20
overlay_y = 60
cv2.rectangle(
display,
(10, 20),
(320, 500),
(0, 0, 0),
-1
)
cv2.putText(
display,
"Decimal values:",
(overlay_x, overlay_y),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(0, 255, 0),
2
)
# ---------- Bytes 1-16 ----------
for i, val in enumerate(decimal_values):
cv2.putText(
display,
f"{i+1}: {val}",
(overlay_x, overlay_y + 35 + i * 22),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(255, 255, 255),
1
)
# ---------- Byte 17 = checksum ----------
cv2.putText(
display,
f"17 (checksum): {checksum_value}",
(overlay_x, overlay_y + 35 + len(decimal_values) * 22 + 15),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 255, 255),
2
)
# ---------- Transform ----------
def transform(x, y):
x0 = (x - cx) * scale
y0 = (y - cy) * scale
xr = x0 * np.cos(rotation) - y0 * np.sin(rotation)
yr = x0 * np.sin(rotation) + y0 * np.cos(rotation)
x2 = cx + xr + offset_x
y2 = cy + yr + offset_y
return int(x2), int(y2)
# ---------- Draw polygon ----------
poly_scaled = np.array([transform(x, y) for (x, y) in poly_pts])
cv2.polylines(
display,
[poly_scaled.reshape((-1, 1, 2))],
True,
(0, 255, 0),
2
)
# ---------- Orientation circle ----------
circle_center = transform(505, 402)
cv2.circle(
display,
circle_center,
20,
(0, 255, 0),
2
)
# ---------- Draw columns ----------
shifted_columns = []
for col in columns:
new_col = []
for (x, y) in col:
tx, ty = transform(x, y)
new_col.append((tx, ty))
cv2.circle(display, (tx, ty), 5, (0, 0, 255), -1)
shifted_columns.append(new_col)
# ---------- Input ----------
key = cv2.waitKey(1) & 0xFF
# Move
if key == ord('w'):
offset_y -= 1
elif key == ord('s'):
offset_y += 1
elif key == ord('a'):
offset_x -= 1
elif key == ord('d'):
offset_x += 1
# Fast move
elif key == ord('W'):
offset_y -= 5
elif key == ord('S'):
offset_y += 5
elif key == ord('A'):
offset_x -= 5
elif key == ord('D'):
offset_x += 5
# Zoom
elif key == ord('g'):
scale *= 0.995
elif key == ord('h'):
scale /= 0.995
# Fast zoom
elif key == ord('G'):
scale *= 0.97
elif key == ord('H'):
scale /= 0.97
# Rotation
elif key == ord('i'):
rotation -= 0.002
elif key == ord('o'):
rotation += 0.002
# Fast rotation
elif key == ord('I'):
rotation -= 0.01
elif key == ord('O'):
rotation += 0.01
# ---------- READ ----------
elif key == ord('r'):
matrix = []
decimal_values = []
# ---------- First 16 columns ----------
for col in shifted_columns[:16]:
bits = []
for (x, y) in col:
if 0 <= x < w and 0 <= y < h:
b, g, r_pix = frame[y, x]
gray = int(0.299 * r_pix + 0.587 * g + 0.114 * b)
bits.append(1 if gray < THRESHOLD else 0)
else:
bits.append(0)
matrix.append(bits)
value = sum(b << i for i, b in enumerate(bits))
decimal_values.append(value)
# ---------- Checksum column ----------
checksum_bits = []
for (x, y) in shifted_columns[16]:
if 0 <= x < w and 0 <= y < h:
b, g, r_pix = frame[y, x]
gray = int(0.299 * r_pix + 0.587 * g + 0.114 * b)
checksum_bits.append(1 if gray < THRESHOLD else 0)
else:
checksum_bits.append(0)
matrix.append(checksum_bits)
checksum_value = sum(b << i for i, b in enumerate(checksum_bits))
expected_checksum = sum(decimal_values) % 256
if checksum_value == expected_checksum:
last_status = "CHECKSUM OK"
last_color = (0, 255, 0)
else:
last_status = f"CHECKSUM ERROR ({checksum_value} != {expected_checksum})"
last_color = (0, 0, 255)
last_matrix = matrix
print("Decimal lista:")
print(decimal_values)
print("Checksum:")
print(checksum_value)
print("Expected checksum:")
print(expected_checksum)
elif key == ord('q'):
break
# ---------- Right overlay ----------
if last_matrix is not None:
cell = 18
rows = 8
cols = len(last_matrix)
tx = w - cols * cell - 20
ty = 20
cv2.rectangle(
display,
(tx - 10, ty - 10),
(tx + cols * cell + 10, ty + rows * cell + 35),
(0, 0, 0),
-1
)
for c in range(cols):
for r in range(rows):
val = last_matrix[c][r]
color = (0, 255, 0) if val else (100, 100, 100)
cv2.putText(
display,
str(val),
(tx + c * cell, ty + r * cell + 14),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
color,
1
)
cv2.putText(
display,
last_status,
(tx, ty + rows * cell + 20),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
last_color,
2
)
cv2.imshow("Camera", display)
cap.release()
cv2.destroyAllWindows()




Comments