Work with Keychron's source-available CAD hardware design files (STEP/DXF/PDF) for keyboards and mice, including scripting, inventory management, and 3D printing workflows.
Skill by ara.so — Daily 2026 Skills collection.
Keychron's Keychron-Keyboards-Hardware-Design repository provides production-grade industrial design files for Keychron keyboards and mice. These are real CAD files — not approximations — covering cases, plates, stabilizers, encoders, keycaps, and full assembly models.
Formats included:
.stp / .step — STEP (ISO 10303) 3D solid models, importable in FreeCAD, Fusion 360, SolidWorks, CATIA, etc..dxf — 2D Drawing Exchange Format, used for plates and cutouts in laser cutting or CNC workflows.dwg — AutoCAD native drawing format.pdf — Dimensioned engineering drawings for referenceSeries covered:
| Series | Notable Models |
|---|---|
| Q Series | Q1–Q12, Q0 Plus, Q60, Q65 |
| Q Pro Series | Q1 Pro–Q14 Pro |
| Q HE Series | Q1 HE, Q3 HE, Q5 HE, Q6 HE |
| K Pro Series | K1 Pro–K17 Pro |
| K Max Series | K1 Max–K17 Max |
| K HE Series | K2 HE–K10 HE |
| L Series | L1, L3 |
| V Max Series | V1 Max–V10 Max |
| P HE Series | P1 HE |
| Mice | M1–M7, G1, G2 |
License: Source-available. Personal, educational, and non-commercial use only. Commercial use is strictly prohibited.
git clone https://github.com/Keychron/Keychron-Keyboards-Hardware-Design.git
cd Keychron-Keyboards-Hardware-Design
git clone --filter=blob:none --sparse https://github.com/Keychron/Keychron-Keyboards-Hardware-Design.git
cd Keychron-Keyboards-Hardware-Design
git sparse-checkout set "Q-Series/Q1"
git sparse-checkout set "K-Pro-Series"
Q-Series/
Q1/
Q1-Case.stp
Q1-Plate.stp
Q1-Encoder.stp
Q1-Stabilizer.stp
Q1-Full-Model.stp
Q1-OSA-Keycap.stp
Q-Pro-Series/
Q1 Pro/
Q1-Pro-Case.stp
Q1-Pro-Plate.dxf
...
K-Pro-Series/
K6 Pro/
K8 Pro/
K8-Pro-Keycap.stp
V-Max-Series/
V1 Max/
K-Max-Series/
K8 Max/
K8-Max-Keycap.stp
K-HE-Series/
K2 HE/
K2-HE-Cherry-Keycap.stp
K2-HE-OSA-Keycap.stp
Mice/
M1/
M1-Shell.stp
M1-Full-Model.stp
Keycap Profiles/
OSA Profile/
KSA Profile/
docs/
file-format-guide.md
getting-started.md
3d-printing-guide.md
repo-inventory.md
license-faq.md
"""
inventory.py — Scan the repo and produce a structured inventory of all design files.
"""
import os
import json
from pathlib import Path
from collections import defaultdict
REPO_ROOT = Path(__file__).parent # adjust if running from elsewhere
SUPPORTED_EXTENSIONS = {".stp", ".step", ".dxf", ".dwg", ".pdf"}
def build_inventory(root: Path) -> dict:
inventory = defaultdict(lambda: defaultdict(list))
for path in sorted(root.rglob("*")):
if path.suffix.lower() in SUPPORTED_EXTENSIONS:
# Series = top-level folder; model = second-level folder
parts = path.relative_to(root).parts
series = parts[0] if len(parts) > 0 else "Unknown"
model = parts[1] if len(parts) > 1 else "Root"
inventory[series][model].append({
"file": path.name,
"format": path.suffix.lower().lstrip(".").upper(),
"size_kb": round(path.stat().st_size / 1024, 1),
"path": str(path.relative_to(root)),
})
return inventory
def print_summary(inventory: dict):
total_files = 0
for series, models in inventory.items():
series_count = sum(len(files) for files in models.values())
total_files += series_count
print(f"\n{series} ({len(models)} models, {series_count} files)")
for model, files in models.items():
formats = sorted({f["format"] for f in files})
print(f" {model}: {len(files)} files [{', '.join(formats)}]")
print(f"\nTotal: {total_files} design files across {len(inventory)} series")
if __name__ == "__main__":
inv = build_inventory(REPO_ROOT)
print_summary(inv)
# Optional: dump to JSON
with open("inventory.json", "w") as f:
json.dump(inv, f, indent=2)
print("\nInventory saved to inventory.json")
Run it:
python inventory.py
"""
find_plates.py — List all DXF plate files suitable for laser cutting or CNC.
"""
from pathlib import Path
REPO_ROOT = Path(".")
def find_plates(root: Path):
results = []
for path in sorted(root.rglob("*.dxf")):
name_lower = path.name.lower()
if "plate" in name_lower:
results.append(path)
return results
if __name__ == "__main__":
plates = find_plates(REPO_ROOT)
print(f"Found {len(plates)} plate DXF files:\n")
for p in plates:
print(f" {p}")
"""
find_model.py — Find all files for a given keyboard model.
Usage:
python find_model.py "Q8"
python find_model.py "K8 Pro"
python find_model.py "M3"
"""
import sys
from pathlib import Path
REPO_ROOT = Path(".")
def find_model_files(root: Path, query: str):
query_lower = query.lower().replace(" ", "")
matches = []
for path in sorted(root.rglob("*")):
if path.is_file():
# Check folder name or filename
normalized = str(path).lower().replace(" ", "").replace("-", "")
if query_lower.replace("-", "") in normalized:
matches.append(path)
return matches
if __name__ == "__main__":
query = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Q8"
results = find_model_files(REPO_ROOT, query)
if not results:
print(f"No files found matching '{query}'")
else:
print(f"Files matching '{query}':\n")
for r in results:
print(f" {r}")
"""
export_csv.py — Export a CSV of all design files with metadata.
"""
import csv
from pathlib import Path
REPO_ROOT = Path(".")
OUTPUT = Path("keychron_inventory.csv")
SUPPORTED_EXTENSIONS = {".stp", ".step", ".dxf", ".dwg", ".pdf"}
def export_csv(root: Path, output: Path):
rows = []
for path in sorted(root.rglob("*")):
if path.suffix.lower() in SUPPORTED_EXTENSIONS:
parts = path.relative_to(root).parts
series = parts[0] if len(parts) > 0 else ""
model = parts[1] if len(parts) > 1 else ""
component = _infer_component(path.name)
rows.append({
"series": series,
"model": model,
"component": component,
"filename": path.name,
"format": path.suffix.lower().lstrip(".").upper(),
"size_kb": round(path.stat().st_size / 1024, 1),
"relative_path": str(path.relative_to(root)),
})
with open(output, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
print(f"Exported {len(rows)} rows to {output}")
def _infer_component(filename: str) -> str:
name = filename.lower()
for keyword in ["case", "plate", "encoder", "stabilizer", "keycap",
"full-model", "full_model", "shell", "knob"]:
if keyword.replace("-", "") in name.replace("-", "").replace("_", ""):
return keyword.replace("-", " ").title()
return "Other"
if __name__ == "__main__":
export_csv(REPO_ROOT, OUTPUT)
"""
validate_naming.py — Check that files follow Keychron naming conventions.
Expected pattern: <ModelName>-<Component>.<ext>
Example: Q8-Plate.stp, K8-Pro-Case.dxf
"""
import re
from pathlib import Path
REPO_ROOT = Path(".")
SUPPORTED_EXTENSIONS = {".stp", ".step", ".dxf", ".dwg", ".pdf"}
NAMING_PATTERN = re.compile(
r"^[A-Z0-9][A-Za-z0-9\s\-]+-"
r"(Case|Plate|Encoder|Stabilizer|Keycap|Full.Model|Shell|Knob|Knob.*)"
r"\.(stp|step|dxf|dwg|pdf)$",
re.IGNORECASE,
)
issues = []
for path in sorted(REPO_ROOT.rglob("*")):
if path.suffix.lower() in SUPPORTED_EXTENSIONS:
if not NAMING_PATTERN.match(path.name):
issues.append(str(path.relative_to(REPO_ROOT)))
if issues:
print(f"Files with non-standard names ({len(issues)}):\n")
for i in issues:
print(f" {i}")