เรียนรู้ครบวงจรตั้งแต่การออกแบบชิ้นส่วน 3 มิติด้วย Onshape การจัดหาอุปกรณ์จาก Shopee การพิมพ์ 3 มิติ การประกอบ การเขียนโปรแกรมดึงไลบรารีจาก GitHub จนถึงการใส่สมองกลด้วย AI ท้องถิ่น (Local LLM)
นักเรียนสามารถออกแบบชิ้นส่วนของตัวเอง รู้จักแหล่งหาอะไหล่ และเตรียมชิ้นงานพิมพ์ 3 มิติ ได้
Sketch, Dimension, Extrude, Fillet, Holeรูป: ตัวอย่างสเก็ตช์ Gripper 2 นิ้วใน Onshape (มีรูน็อต Ø สำหรับยึดกับเซอร์โว)
📖 ดูเพิ่มเติม: Onshape — วิธีเริ่มต้นใช้งานและพื้นฐานการวาด
ESP32, PCA9685, สเต็ปเปอร์มอเตอร์ NEMA 17, เซอร์โวมอเตอร์รูป: ชิ้นส่วนหลักที่ต้องจัดซื้อ — ESP32, สเต็ปเปอร์, เซอร์โว + งบประมาณ (BOM)
ส่งออกชิ้นส่วน Gripper ที่ออกแบบเป็นไฟล์ STL หรือ STEP
ดาวน์โหลดชิ้นส่วนแขนกล 3 แกนสำเร็จรูปจาก MakerWorld / Printables / Thingiverse เพื่อใช้เป็นโครงสร้างหลัก แล้วนำ Gripper ของตัวเองไปติดที่ปลายแขน
ตั้งค่า Infill (แนะนำ 20–40% สำหรับชิ้นส่วนรับแรง) และ Support (เปิดเฉพาะจุดที่ยื่นเกิน 45° ที่จำเป็น)
เริ่มพิมพ์ชิ้นส่วนพร้อมตรวจคุณภาพ (Support หลุดง่ายไหม? ผิวเรียบไหม? รูน็อตพอดีไหม?) — การประกอบ + เดินสายไฟจะทำต่อในช่วงเช้าของวันที่ 2 เมื่อชิ้นส่วนพิมพ์ครบ
รูป: เครื่องพิมพ์ 3 มิติกำลังพิมพ์ Gripper — ตั้งค่า Infill และ Support ให้ถูกจุดก่อนสั่งพิมพ์
Onshape คือโปรแกรม CAD แบบ Cloud-Native — ไม่ต้องติดตั้งซอฟต์แวร์ใด ๆ เปิดเว็บเบราว์เซอร์แล้วใช้งานได้ทันที ไฟล์ทั้งหมดถูกบันทึกลงคลาวด์อัตโนมัติ และรองรับการทำงานร่วมกันแบบ Real-time (เหมือน Google Docs แต่เป็น 3D CAD) มีบัญชีฟรี และบัญชีนักเรียน (Student Account) ที่ได้ฟีเจอร์เต็มรูปแบบ
cad.onshape.com ผ่านเบราว์เซอร์ Chrome / Edge / Firefox (อินเทอร์เน็ตควรเสถียร ประมาณ 10 Mbps ขึ้นไป)Parts Studio (ออกแบบชิ้นงาน), Assembly (ประกอบ), Drawing (แบบ 2 มิติ)รูป: หน้าจอจำลอง Onshape — Feature Toolbar ด้านบน, Feature Tree ด้านซ้าย, View Cube มุมขวา
กดปุ่ม + Create → ตั้งชื่อ เช่น My_Gripper → เปิด Tab Parts Studio (หน่วยเริ่มต้นเป็น มม. อยู่แล้ว)
เลือกระนาบ (Plane) เช่น Top Plane → กด Sketch (หรือกด S) → วาดด้วยเครื่องมือ Line / Rectangle / Circle / Arc ตามรูปร่างที่ต้องการ
กดเครื่องมือ Dimension (หรือกด D) → คลิกที่เส้น/จุด → ลากออกมาแล้วพิมพ์ตัวเลข (เช่น 20 มม.) → Enter — กรณีทำ Gripper ให้กำหนดรูน็อต Ø3.2 มม. (เผื่อ M3) ให้พอดีกับเซอร์โวมอเตอร์
กดเครื่องหมาย ✓ (Check) เพื่อจบ Sketch → กด Extrude (หรือกด E) → เลือกหน้าตัด → ป้อนความหนา (เช่น 8 มม.) → กด ✓
Fillet (กด F) = ปัดขอบมุมให้เรียบ · Hole = เจาะรูน็อตพร้อมรูปแบบ
จากนั้นคลิกขวาที่ชิ้นงาน → Export เลือก STL (สำหรับพิมพ์ 3 มิติ) หรือ STEP (สำหรับแชร์ไฟล์ CAD)
Onshape มี AI ให้ใช้จริง 3 ระดับ — เหมาะกับการสอดแทรกหัวข้อ AI ในค่ายนี้ด้วย:
ประกอบร่างให้สมบูรณ์ ดึงไลบรารีมาใช้ และเข้าใจกลศาสตร์แขนกล 3 แกน พร้อมปรับโค้ดจาก GitHub ให้แขนกลขยับได้เอง
README.mdhttp://[IP-ESP32]/รูป: ตัวอย่างรีโปโปรเจกต์บน GitHub — ควรอ่าน README.md ก่อนเสมอ
FK: มุมข้อต่อ → พิกัดปลายแขน · IK: พิกัดปลายแขน → มุมข้อต่อ (ดูสูตรและตัวอย่างในหัวข้อ Kinematics)
ลองเลื่อนมุม θ1–θ3 ดูตำแหน่งปลายแขน และกดคำนวณ IK ใน Kinematics Simulator
นำโค้ดจาก GitHub มาปรับแก้ (Modify) เพื่อสั่งแขนกลไปยังพิกัด X, Y, Z ที่ต้องการ และทดสอบเปิด–ปิด Gripper ที่ออกแบบเอง
รูป: แขนกล 3 ข้อต่อบนแกน X-Y-Z — เปลี่ยนมุม θ แล้วพิกัดปลายแขนเปลี่ยนตาม
ยกระดับแขนกลด้วย AI และแข่งขันทักษะการทำภารกิจ
รูป: สมองกล AI — ตีความภาษามนุษย์แล้วสั่งงานแขนกลผ่าน Wi-Fi
| เกณฑ์ | รายละเอียด | คะแนน |
|---|---|---|
| การออกแบบ (Onshape) | Gripper ใช้งานได้จริง มีรูน็อตตรงสเปก ออกแบบมีเหตุผล | 20 |
| งบประมาณ (BOM) | ตารางต้นทุนครบถ้วน เลือกอุปกรณ์ตรงสเปก คุ้มค่า | 15 |
| การปรับโค้ด (GitHub) | เข้าใจโค้ดที่นำมาใช้ อธิบายส่วนที่ปรับแก้ได้ | 20 |
| การทำงานของแขนกล | คีบ–วางชิ้นงานได้ถูกต้อง แม่นยำ ทันเวลา | 25 |
| AI Integration | รับคำสั่งภาษาไทยผ่าน Local LLM แล้วแปลงเป็นพิกัดได้ | 10 |
| การนำเสนอ (Pitching) | เล่าเรื่องครบวงจร สื่อสารชัดเจน ตอบคำถามได้ | 10 |
จำลองการทำงานของแขนกล 3 แกนจากโปรเจกต์ terk_arm_esp32_v1
(ใช้ค่าจริงจากโค้ด: L1 = 135 mm, L2 = 157 mm, Base = 156.5 mm, End-Effector = 125 mm)
เลื่อนมุมข้อต่อเพื่อดูตำแหน่งปลายแขน หรือกรอกพิกัด X, Y, Z แล้วกด คำนวณ IK เพื่อหามุมข้อต่อ
d1 = sin(θ1) × L1 d2 = cos(θ1) × L1
d3 = cos(θ2) × L2 d4 = sin(θ2) × L2
x = cos(θ3) × (d1 + d3 + END)
y = sin(θ3) × (d1 + d3 + END) + WORK_SPACE
z = |(d4 − d2) − BASE|
L1 = 135, L2 = 157, END = 125, BASE = 156.5, WORK_SPACE = 420 (มม.)
r = |√(x² + (y−WORK_SPACE)²) − END|
theta = atan2(r, BASE − z)
g = √(r² + (BASE − z)²)
alpha = acos((L1² + g² − L2²) / (2·L1·g))
beta = alpha + theta
theta1 = 180 − beta
theta2 = (180 − free − alpha) + (90 − theta)
theta3 = atan2(y − WORK_SPACE, x)
โดย free = acos((L1² + L2² − g²) / (2·L1·L2)) — กฎของโคไซน์ในสามเหลี่ยมข้อต่อ
โจทย์: กำหนดมุมข้อต่อ θ1 = 30°, θ2 = 30°, θ3 = 20° จงหาตำแหน่งปลายแขน (x, y, z)
d1 = sin(30°) × 135 = 0.5000 × 135 = 67.5
d2 = cos(30°) × 135 = 0.8660 × 135 = 116.9
d3 = cos(30°) × 157 = 0.8660 × 157 = 136.0
d4 = sin(30°) × 157 = 0.5000 × 157 = 78.5
d1 + d3 + END = 67.5 + 136.0 + 125 = 328.5
x = cos(20°) × 328.5 = 0.9397 × 328.5 = 308.7 mm
y = sin(20°) × 328.5 + 420 = 112.3 + 420 = 532.3 mm
z = |(78.5 − 116.9) − 156.5| = |−194.9| = 194.9 mm
โจทย์: ต้องการให้ปลายแขนไปที่พิกัด (150, 300, 100) มม. จงหามุมข้อต่อ θ1, θ2, θ3
base_stant = 156.5 − 100 = 56.5
ระยะรวม = √(150² + 120²) = √36900 = 192.1
r = |192.1 − 125| = 67.1
θ = atan2(67.1, 56.5) = 49.9°
g = √(67.1² + 56.5²) = √7694 = 87.7
α = acos((135² + 87.7² − 157²) / (2×135×87.7))
= acos(0.0536) = 86.9°
β = α + θ = 86.9 + 49.9 = 136.8°
free = acos((135² + 157² − 87.7²) / (2×135×157))
= acos(0.8299) = 33.9°
θ1 = 180 − 136.8 = 43.2°
θ2 = (180 − 33.9 − 86.9) + (90 − 49.9) = 99.3°
θ3 = atan2(300 − 420, 150) = atan2(−120, 150) = −38.7°
หลักการ: Forward Kinematics (FK) คือการหาตำแหน่งปลายแขนเมื่อรู้มุมข้อต่อ — Inverse Kinematics (IK) คือการหามุมข้อต่อเมื่อรู้ตำแหน่งปลายแขน
ตัวอย่าง: ก้านเดียว l = 10 cm, มุม θ = 45°
cos θ = x / l → x = cos 45° × 10 = 7.07 cm
sin θ = y / l → y = sin 45° × 10 = 7.07 cm
ตำแหน่งปลายแขน = p(7.07, 7.07) cm — สังเกตว่ามุม 45° ทำให้ x = y
โค้ด Python (จากบทความต้นฉบับ):
import math
def solve_1dof_fk(link, degree):
x = math.cos(math.radians(degree)) * link
y = math.sin(math.radians(degree)) * link
return x, y
x, y = solve_1dof_fk(10, 45)
print("x =", x, " y =", y)
# ผลลัพธ์: x = 7.07 y = 7.07
โจทย์ต้นฉบับกำหนดค่าให้: l1 = l2 = 10 cm, d2 = 7 cm,
θ1 = 60°, θ2 = 30°, θ3 = 140°
รูป: แขนกล 3 DOF — ซ้าย: TOP VIEW (θ1), ขวา: SIDE VIEW (θ2, θ3, z) พร้อมค่าที่กำหนดให้
จากภาพ Side View เราจะหาความสูง z ก่อน แล้วค่อยหาจาก Top View เอาระยะเอื้อมแนวนอน d1 ไปหาพิกัด x, y
รูป: SIDE VIEW — BASE, ก้าน L1/L2, มุม θ2/θ3, ความสูง z
รูป: SIDE VIEW แบบเต็มป้าย — d1…d6, θ4, θ5, 90°, z (ใช้ในขั้นตอนด้านล่าง)
sin θ2 = d3 / l1
d3 = sin 30° × 10 = 5.0 cm
θ4 = 180° − (θ2 + 90°) = 180° − (30° + 90°) = 60°
θ5 = θ3 − θ4 = 140° − 60° = 80°
cos θ5 = d6 / l2 → d6 = cos 80° × 10 = 1.74 cm
z = d2 + d3 − d6 = 7 + 5.0 − 1.74 = 10.26 cm
d4 = cos θ2 × l1 = cos 30° × 10 = 8.66 cm
d5 = sin θ5 × l2 = sin 80° × 10 = 9.85 cm
d1 = d4 + d5 = 8.66 + 9.85 = 18.51 cm
x = cos θ1 × d1 = cos 60° × 18.51 = 9.26 cm
y = sin θ1 × d1 = sin 60° × 18.51 = 16.03 cm
รูป: โครงสร้างแขน — Base, Shoulder, Elbow, Wrist, End-Effector พร้อมแกน x/y/z
แขนตัวอย่างมีอิสระ 3 แกน (3 DOF): Swing (หมุนทั้งแขนรอบฐาน), Main (เลื่อนข้อศอกเข้า–ออก), Actuator (เลื่อนปลายแขนขึ้น–ลง) — ข้อดีของแขน 3 DOF คือ ทุกตำแหน่งของปลายแขนมีท่าคำตอบเดียว (unique solution) ทำให้แก้ IK แบบวิเคราะห์ (เรขาคณิต) ได้โดยไม่ต้องใช้การคำนวณเชิงตัวเลขที่ซับซ้อน
รูป: “Easy” = จุดหมุนอยู่กึ่งกลาง (ใช้ตรีโกณมิติตรง ๆ) · “Less easy” = จุดหมุนเยื้องศูนย์ (ต้องใช้การค้นหา)
กรณีกึ่งกลาง: หามุมหมุนง่าย ๆ ด้วย atan2 แต่แขนจริงมีระนาบการทำงานเยื้องจากแกนหมุน
(ดูภาพขวา) ทำให้ถ้าใช้ตรีโกณมิติตรง ๆ แขนจะชี้เฉไปด้านข้าง
วิธีที่ผู้เขียนใช้: Binary Search (วิธีแบ่งครึ่ง / Bisection Method) — กำหนดขอบบน–ล่างของมุม ตรวจจุดกึ่งกลางว่ามาก/น้อยกว่าเป้าหมาย แล้วลดช่วงลงครึ่งหนึ่งทุกครั้ง 3–5 รอบได้ความแม่นยำต่ำกว่า 0.3° (เพียงพอสำหรับเซอร์โว)
import numpy as np
def normalize(v):
return v / np.linalg.norm(v)
def signed_angle(A, B):
"""มุมมีเครื่องหมาย: บวก = A หมุนตามเข็มนาฬิกาจาก B (ใช้ dot product)"""
A, B = normalize(A), normalize(B)
perp = np.array([-B[1], B[0]]) # เวกเตอร์ตั้งฉากกับ B ไปทางตามเข็มนาฬิกา
return np.rad2deg(np.arctan2(np.dot(A, perp), np.dot(A, B)))
# เริ่มต้นจากมุมคร่าว ๆ (naive) แล้วค้นหาแบบแบ่งครึ่ง
start_theta = np.rad2deg(np.arctan2(goal[1], goal[0])) # atan2 แก้ quadrant ให้ถูกต้อง
lo, hi = start_theta - 60, start_theta + 60 # ขอบเขตเริ่มต้น
for _ in range(8): # 8 รอบ = แม่นยำมาก
mid = (lo + hi) / 2
err = signed_angle(arm_direction(mid), goal - shoulder(mid))
if err > 0: lo = mid # มากเกินไป -> ใช้ mid เป็นขอบบน
else: hi = mid # น้อยเกินไป -> ใช้ mid เป็นขอบล่าง
swing = (lo + hi) / 2
ผลลัพธ์ที่ได้จากขั้นนี้ด้วย: ระยะรัศมีจากข้อต่อ Shoulder ถึงปลายแขน (goal_radial) — ใช้ต่อในขั้นตอนที่ 2
goal_radial = np.linalg.norm(goal - shoulder(swing)) # ระยะ 2D บนระนาบการทำงาน
รูป: วงกลม 2 วง (ศูนย์กลางที่ Shoulder รัศมี 148mm = Main arm · ศูนย์กลางที่ Wrist รัศมี = Forearm) — ข้อศอกอยู่ที่จุดตัดบน
เปลี่ยนมาใช้พิกัด 2D ในระนาบการทำงาน (แกน x = ระยะ radial, แกน y = ความสูง) — main arm กับ forearm สร้างสามเหลี่ยมโดยมีข้อศอกอยู่ที่ยอด เนื่องจากข้อศอกอยู่ห่างจาก Shoulder = ความยาว main arm และห่างจาก Wrist = ความยาว forearm เราจึงวาดวงกลม 2 วง แล้วหาจุดตัด — ข้อศอกคือจุดตัดบน (ค่า y มากสุด)
def circle_intersection(c1, r1, c2, r2):
"""จุดตัดของวงกลม 2 วง — คืนจุดบนสุด (y มากสุด) หรือ None ถ้าหาคำตอบไม่ได้"""
d = np.linalg.norm(c2 - c1)
if d > r1 + r2: return None # วงแยกกัน -> ระยะเอื้อมไม่ถึง
if d < abs(r1 - r2): return None # วงหนึ่งกลืนอีกวง -> โครงสร้างเป็นไปไม่ได้
a = (r1*r1 - r2*r2 + d*d) / (2*d)
h = np.sqrt(r1*r1 - a*a)
p = c1 + a * (c2 - c1) / d
u = np.array([-(c2[1]-c1[1]), c2[0]-c1[0]]) / d # ทิศตั้งฉาก
p1, p2 = p + h*u, p - h*u
return max(p1, p2, key=lambda v: v[1]) # เลือกข้อศอกตำแหน่งบน
elbow = circle_intersection(shoulder, MAIN_LEN, wrist_goal, FOREARM_LEN)
terk_arm_esp32_v1) ใช้สูตรตรีโกณมิติ IK ตรง ๆ ในหัวข้อ Kinematics ด้านบน
ส่วนบทความนี้เสนออีกแนวคิดหนึ่ง — การค้นหาแบบแบ่งครึ่ง + เรขาคณิตวงกลม
ซึ่งเหมาะกับแขนที่โครงสร้างมีจุดหมุนเยื้องศูนย์ และใช้โค้ด Python + NumPy
(pip install numpy) — สอนให้เห็นว่าการแก้ปัญหาเดียวกันมีได้หลายวิธีDenavit–Hartenberg (DH) เป็นวิธีมาตรฐาน (ตีพิมพ์ปี 1955) ในการอธิบายโครงสร้างแขนหุ่นยนต์ ด้วยพารามิเตอร์เพียง 4 ค่าต่อข้อต่อ — ต่างจากตรีโกณมิติที่ต้องคิดเฉพาะโครงสร้าง DH ใช้ได้กับแขนทุกแบบและหลายข้อต่อ (รวมถึงหุ่นยนต์อุตสาหกรรม 6 แกน)
พารามิเตอร์ 4 ตัว สำหรับข้อต่อที่ i:
ระยะตามแกน x (ความยาวก้าน)
มุมบิดรอบแกน x (Link Twist)
ระยะเลื่อนตามแกน z (Offset)
มุมหมุนรอบแกน z — ตัวแปรของข้อต่อหมุน
| ข้อต่อ i | aᵢ (mm) | αᵢ (deg) | dᵢ (mm) | θᵢ (deg) | หมายเหตุ |
|---|---|---|---|---|---|
| 1 (ฐานหมุน) | 0 | −90° | 156.5 | θ₁ (ตัวแปร) | หมุนรอบแกน Z |
| 2 | 135 | 0° | 0 | θ₂ (ตัวแปร) | ความยาว L1 |
| 3 | 157 | 0° | 0 | θ₃ (ตัวแปร) | ความยาว L2 |
ค่า a, d, α เป็นค่าคงที่ของโครงสร้าง ส่วน θ เป็นตัวแปรที่เปลี่ยนตามข้อต่อหมุน (Revolute)
Tᵢ = Rot(z, θᵢ) · Trans(z, dᵢ) · Trans(x, aᵢ) · Rot(x, αᵢ)
┌ cosθ −sinθ·cosα sinθ·sinα a·cosθ ┐
T = │ sinθ cosθ·cosα −cosθ·sinα a·sinθ │
│ 0 sinα cosα d │
└ 0 0 0 1 ┘
FK ทั้งแขน = T₁ × T₂ × T₃ (คูณเมทริกซ์ต่อกันทุกข้อต่อ)
— ตำแหน่งปลายแขน (x, y, z) อยู่ในคอลัมน์สุดท้ายของผลลัพธ์
import numpy as np
def dh_transform(a, alpha, d, theta):
"""เมทริกซ์แปลง DH สำหรับหนึ่งข้อต่อ (มุมเป็นเรเดียน)"""
ct, st = np.cos(theta), np.sin(theta)
ca, sa = np.cos(alpha), np.sin(alpha)
return np.array([
[ct, -st*ca, st*sa, a*ct],
[st, ct*ca, -ct*sa, a*st],
[0, sa, ca, d ],
[0, 0, 0, 1 ],
])
# ตาราง DH ของแขน 3 DOF (ใช้ค่าจริงจากโปรเจกต์)
dh = [
{"a": 0, "alpha": np.deg2rad(-90), "d": 156.5, "theta": np.deg2rad(30)},
{"a": 135, "alpha": 0, "d": 0, "theta": np.deg2rad(45)},
{"a": 157, "alpha": 0, "d": 0, "theta": np.deg2rad(-20)},
]
T = np.eye(4)
for joint in dh:
T = T @ dh_transform(**joint)
print("ตำแหน่งปลายแขน (x, y, z):", T[:3, 3])
รายการอุปกรณ์หลักของแขนกลในโปรเจกต์ terk_arm_esp32_v1 พร้อมขา GPIO ที่ใช้จริงในโค้ด
รูป: แผนภาพการเดินสายไฟ — ESP32 → A4988 ×3 → NEMA 17 ×3 + Servo Gripper + แหล่งจ่ายไฟ (GND ต้องต่อร่วมกันทุกบอร์ด)
สมองหลัก ควบคุมมอเตอร์ 3 ตัว + Gripper + Web Server (Wi-Fi) มีโหมด AP (SSID: RobotArmAP_04)
NEMA 17 ขับด้วย AccelStepper + MultiStepper สำหรับการเคลื่อนที่แบบประสานงาน
เซอร์โวควบคุมการเปิด–ปิดหัวจับ (0° = คีบ, 180° = ปล่อย) ขับด้วยไลบรารี ESP32Servo
โมดูลขับ PWM 16 ช่อง ใช้ควบคุมเซอร์โวหลายตัว พร้อมไลบรารีจาก GitHub
12V สำหรับสเต็ปเปอร์ + 5V สำหรับ ESP32/เซอร์โว ระวังเรื่องกระแสรวม (ดูหัวข้อ BOM)
อัปเดตเฟิร์มแวร์ผ่าน Wi-Fi ได้ ไม่ต้องเสียบสาย — เหมาะสำหรับการแข่งในค่าย
| อุปกรณ์ | ฟังก์ชัน | GPIO (โค้ด) | หมายเหตุ |
|---|---|---|---|
| Stepper 1 (ข้อต่อฐาน Z) | DIR / STEP | 17 / 16 | AccelStepper stepper1(1, 17, 16) |
| Stepper 2 (ข้อต่อกลาง) | DIR / STEP | 18 / 5 | AccelStepper stepper2(1, 18, 5) |
| Stepper 3 (ข้อต่อปลาย) | DIR / STEP | 21 / 19 | AccelStepper stepper3(1, 21, 19) |
| Gripper Servo | PWM | กำหนดในโค้ด | ไลบรารี ESP32Servo |
| Wi-Fi | AP/STA | — | AP: RobotArmAP_04 / STA: SSID ในโค้ด |
VMOT ของ Driver ให้ตรงกับมอเตอร์ และต่อขา GND ร่วมกันทุกบอร์ด
terk_arm_esp32_v1terk_arm_esp32_v1/
├── terk_arm_esp32_v1.ino # เฟิร์มแวร์หลัก
├── Constants.h # ค่าคงที่ (π, AP name...)
├── RobotArmIK.h / .cpp # FK + IK
├── WebPages.h / .cpp # หน้าเว็บควบคุม (HTML/CSS/JS)
├── API_Examples.md # เอกสารตัวอย่าง API
├── test_compile.ino # ไฟล์ทดสอบคอมไพล์
└── build/ # โฟลเดอร์ build
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.jsonAccelStepper, ESP32Servo, Adafruit PWM Servo Driver (PCA9685)terk_arm_esp32_v1.ino แก้ค่า ssid/password ให้เป็น Wi-Fi ของตัวเอง/ota ได้โดยไม่ต้องใช้สาย
แขนกลมี Web Server ในตัว (พอร์ต 80) เข้าใช้งานผ่านเบราว์เซอร์ที่
http://[IP-ESP32]/ ประกอบด้วยโหมด:
การเคลื่อนที่ใช้ Trajectory Planning — สร้างจุดขั้นกลาง (interpolation) แล้วป้อนเข้าคิวคำสั่ง
commandQueue เพื่อให้ข้อต่อทั้ง 3 ขยับพร้อมกันอย่างนุ่มนวล
แขนกลเปิด API แบบ HTTP (ไม่ต้องมี Authentication) — เหมาะสำหรับต่อกับ AI, Python, หรือเว็บแอป
Base URL: http://[ESP32_IP]/api/ · รูปแบบตอบกลับ: JSON
| Method | Endpoint | พารามิเตอร์ | คำอธิบาย |
|---|---|---|---|
POST | /api/move | x, y, z, speed, acceleration | เคลื่อนปลายแขนไปพิกัด X, Y, Z (มม.) |
POST | /api/joint | theta1, theta2, theta3, speed, acceleration | หมุนข้อต่อไปยังมุม (องศา) |
POST | /api/gripper | position | ตั้ง Gripper 0–180° |
GET | /api/status | — | ดูตำแหน่ง มุมข้อต่อ สถานะคิว Wi-Fi IP |
POST | /api/gcode | gcode | ส่งชุดคำสั่ง G-code (หลายบรรทัดได้) |
POST | /api/home | — | กลับตำแหน่ง Home |
POST | /api/zero | — | ตั้งศูนย์ / Calibrate |
GET | /api/docs | — | หน้าเอกสาร API ในตัว |
application/x-www-form-urlencoded สำหรับ POST — รองรับ CORS สำหรับเว็บแอป
import requests, time
robot = "http://192.168.1.100" # เปลี่ยนเป็น IP ของ ESP32
def api(ep, data=None):
r = requests.post(f"{robot}/api/{ep}", data=data)
print(ep, "→", r.json()["message"])
# 1. ไปยังตำแหน่งหยิบชิ้นงาน
api("move", {"x": 100, "y": 100, "z": 50, "speed": 1000})
time.sleep(2)
# 2. ลดแขนลง
api("move", {"x": 100, "y": 100, "z": 10})
time.sleep(1)
# 3. คีบชิ้นงาน (Gripper ปิด = 0°)
api("gripper", {"position": 0})
time.sleep(1)
# 4. ยกขึ้น แล้วไปทิ้งที่ตำแหน่งใหม่
api("move", {"x": 100, "y": 100, "z": 50})
time.sleep(1)
api("move", {"x": 200, "y": 200, "z": 50})
time.sleep(2)
api("move", {"x": 200, "y": 200, "z": 10})
time.sleep(1)
# 5. ปล่อยชิ้นงาน (Gripper เปิด = 180°)
api("gripper", {"position": 180})
print("✅ Pick & Place เสร็จสิ้น")
# เคลื่อนที่ไปพิกัด (XYZ x y z delay)
XYZ 100 100 50 1000
# คีบชิ้นงาน
GPON
# หน่วงเวลา 500 ms
DELAY 500
# ย้ายไปตำแหน่งทิ้ง
XYZ 200 200 50 1000
# ปล่อยชิ้นงาน
GPOFF
ส่งผ่าน POST /api/gcode ด้วยพารามิเตอร์ gcode
— ระบบจะแยกทีละบรรทัดแล้วเข้าคิวสั่งงานตามลำดับ
ollama pull typhoon-instruct (หรือ qwen2.5 / gemma2)ollama run typhoon-instruct "สวัสดี"รูป: AI ตีความคำสั่งภาษาไทย → แปลงเป็น JSON → ส่งผ่าน Wi-Fi → ESP32 ขยับแขนกลทำงาน
import requests, subprocess, json, re
ESP32 = "http://192.168.1.100" # IP ของแขนกล
def ask_ai(prompt):
r = subprocess.run(["ollama", "run", "typhoon-instruct", prompt],
capture_output=True, text=True)
return r.stdout.strip()
# 1) ให้ AI แปลงคำสั่งภาษาคน → JSON พิกัด
text = "หยิบกระป๋องสีแดงไปทิ้ง"
prompt = f"""แขนกล 3DOF ทำงานบนพื้นที่ x=0..300, y=0..400, z=0..250 มม.
กระป๋องสีแดงอยู่ที่ x=100,y=100,z=50 และถังขยะอยู่ที่ x=250,y=250,z=50
ตอบเป็น JSON เดียวเท่านั้น: {{"x":..,"y":..,"z":..,"gripper":0}} (gripper 0=คีบ 180=ปล่อย)
คำสั่ง: {text}"""
out = ask_ai(prompt)
# 2) แยก JSON ออกจากคำตอบ แล้วส่งไปแขนกล
m = re.search(r'\{.*\}', out, re.S)
data = json.loads(m.group(0))
print("🤖 AI ตีความได้:", data)
# 3) สั่งแขนกลทำงาน
requests.post(f"{ESP32}/api/move", data={"x": data["x"], "y": data["y"], "z": data["z"]})
requests.post(f"{ESP32}/api/gripper", data={"position": data["gripper"]})
print("✅ แขนกลทำงานตามคำสั่ง AI แล้ว!")
นอกจาก AI ที่ “ฟังคำสั่ง” แล้ว เรายังเพิ่ม AI ที่ “มองเห็น” ได้ — ใช้กล้องเว็บแคม (Webcam) + โมเดล Vision Language Model (VLM) ให้แขนกลหาตำแหน่งชิ้นงานจากภาพได้เอง โดยไม่ต้องบอกพิกัดล่วงหน้า
qwen3-vl, minicpm-v4.5, llava (รันผ่าน Ollama ได้เลย)
— ถามได้ว่า “กระป๋องสีแดงอยู่ตรงไหนในภาพ?” แล้วได้พิกัดพิกเซลกลับมาสถาปัตยกรรมที่แนะนำสำหรับค่าย:
กล้อง → VLM หาตำแหน่งชิ้นงาน → แปลงพิกเซลเป็นพิกัด X, Y, Z → ส่ง /api/move
(ใช้ IK ที่เรียนมาแล้ว — เหมาะกับงานจริงและปลอดภัยกว่า VLA แบบเต็มรูปแบบ)
รูป: แขนกลมองเห็นด้วย VLM — กล้องจับภาพ → โมเดลหาตำแหน่ง → แปลงพิกัด → สั่งแขนกล
import requests, base64, json, re, cv2
# 1) ถ่ายภาพจากกล้องเว็บแคม (OpenCV)
cap = cv2.VideoCapture(0)
ok, frame = cap.read()
cap.release()
_, buf = cv2.imencode(".jpg", frame)
b64 = base64.b64encode(buf).decode()
# 2) ถาม VLM ผ่าน Ollama API ให้หาตำแหน่งกระป๋องในภาพ
r = requests.post("http://localhost:11434/api/generate", json={
"model": "qwen3-vl", # หรือ minicpm-v4.5 / llava
"prompt": 'ในภาพนี้มีกระป๋องสีแดง 1 ใบ '
'ตอบเป็น JSON: {"x_px":..,"y_px":..} (กึ่งกลางวัตถุเป็นพิกเซล)',
"images": [b64],
"stream": False,
})
out = r.json()["response"]
m = re.search(r'\{.*\}', out, re.S)
px = json.loads(m.group(0))
print("📍 VLM เห็นชิ้นงานที่พิกเซล:", px)
# 3) แปลงพิกเซล -> พิกัดแขนกล (ต้องคาลิเบรตกล้องก่อน) แล้วสั่งงาน
x = int(px["x_px"] * 0.5) # ตัวอย่างการแมปพิกัด
y, z = 300, 80
requests.post("http://192.168.1.100/api/move", data={"x": x, "y": y, "z": z})
print("✅ แขนกลเคลื่อนไปหยิบชิ้นงานที่มองเห็น")
ต่อยอดจากแขนกลของเรา — นอกจากการควบคุมด้วยสูตร Kinematics แล้ว ยังมีอีกแนวทางคือ ให้ AI เรียนรู้การควบคุมเองด้วย Reinforcement Learning (RL) โดยเทรนในโลกจำลอง (Simulator) ก่อน แล้วค่อยนำไปใช้กับแขนจริง (Sim-to-Real)
Agent เล่นซ้ำหลายพันรอบ (Episode) เพื่อปรับ Policy ให้ได้คะแนนรวมสูงสุด — เหมือนการฝึกสุนัขด้วยขนม!
ซิมรันเร็วหลายร้อยเท่าของเวลาจริง — เทรนเป็นล้านรอบได้ในไม่กี่ชั่วโมง
ไม่ต้องกลัวแขนพัง/ชนของจริง ตอนที่ยัง Trial & Error ช่วงแรก
ไม่ต้องใช้อุปกรณ์หลายชุด — รันขนานได้หลายพัน Environment บน GPU
pip install mujoco# pip install mujoco gymnasium stable-baselines3
import gymnasium as gym
from stable_baselines3 import PPO
# แขนกล 2 ข้อต่อใน MuJoCo (Reacher: เอื้อมถึงเป้า)
env = gym.make("Reacher-v5")
# เทรนด้วย PPO (Proximal Policy Optimization)
model = PPO("MlpPolicy", env, verbose=1)
model.learn(total_timesteps=200_000)
model.save("reacher_policy")
# ทดสอบ Policy ที่เทรนแล้ว
env = gym.make("Reacher-v5", render_mode="human")
obs, _ = env.reset()
for _ in range(500):
action, _ = model.predict(obs, deterministic=True)
obs, reward, terminated, truncated, _ = env.step(action)
if terminated or truncated:
obs, _ = env.reset()
# Isaac Lab: เทรนแขน Franka ให้เอื้อมถึงเป้า (ตัวอย่างคำสั่ง)
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py \
--task Isaac-Reach-Franka-v0 --num_envs 4096
| หัวข้อ | MuJoCo | NVIDIA Isaac Sim + Isaac Lab |
|---|---|---|
| ลิขสิทธิ์ | ฟรี + Open Source (DeepMind) | ฟรี (Isaac Lab: Apache 2.0) |
| ความสมจริงภาพ | พื้นฐาน (OpenGL) | Photorealistic (RTX / Omniverse) |
| ความเร็ว / ขนาด | เบา เร็ว รันบน CPU ได้ | GPU-accelerated รันหลายพัน Env พร้อมกัน |
| ฟอร์แมตโมเดล | MJCF, URDF | USD, URDF, MJCF, Onshape, CAD |
| เซนเซอร์ | พื้นฐาน | RTX Camera, LiDAR, Depth, IMU ฯลฯ |
| RL Framework | Gymnasium + Stable-Baselines3 | RSL-RL, SKRL, RL Games, SB3 |
| ฮาร์ดแวร์ขั้นต่ำ | CPU/GPU ทั่วไป | ต้องมี GPU NVIDIA RTX |
| เหมาะกับ | มือใหม่, เรียน RL, ทดสอบไอเดียเร็ว | งานใหญ่, Sim-to-Real, Perception |
/api/move ที่มีอยู่แล้วเจาะลึกการตั้งค่า PPO ที่สำคัญ — Actor–Critic, Runner, Algorithm, Observation/Action Space และ Distillation — อธิบายทีละพารามิเตอร์ พร้อมค่าที่แนะนำสำหรับแขนกลของเรา:
ค่าที่แนะนำสำหรับแขนกล 3DOF (terk_arm):
obs = [θ1, θ2, θ3, gripper, target_x, target_y, target_z] (7)
action = [Δθ1, Δθ2, Δθ3, gripper] (4)
actor_hidden_dims = [128, 64] critic_hidden_dims = [128, 64]
clip_param = 0.2 entropy_coef = 0.005 init_noise_std = 1.0
policy.onnx
→ แปลงเป็น C → รันบน ESP32 → สั่งงานผสานกับ /api/move ที่มีอยู่แล้วตารางตัวอย่างสำหรับ 1 ชุด ให้ปรับราคาตามจริงที่หาได้จาก Shopee ในวันที่เรียน (ราคาประมาณการเท่านั้น — ใช้เป็น แบบฟอร์ม ให้ผู้เรียนกรอกเอง)
| # | รายการ | จำนวน | สเปกโดยย่อ | ราคา/ชิ้น (บาท) | รวม (บาท) |
|---|---|---|---|---|---|
| 1 | ESP32 DevKit / SuperMini | 1 | Wi-Fi + Bluetooth, 3.3V | — | — |
| 2 | สเต็ปเปอร์มอเตอร์ NEMA 17 | 3 | 1.8°/step, 12V, ~0.4A | — | — |
| 3 | Driver A4988 / DRV8825 | 3 | สำหรับขับสเต็ปเปอร์ | — | — |
| 4 | เซอร์โว Gripper (MG996R/SG90) | 1 | สำหรับหัวจับ | — | — |
| 5 | PCA9685 (โมดูล PWM) | 1 | 16 ช่อง ควบคุมเซอร์โว | — | — |
| 6 | แหล่งจ่ายไฟ 12V | 1 | กระแสเพียงพอสำหรับมอเตอร์ทั้งหมด | — | — |
| 7 | Buck Converter 12V→5V | 1 | จ่ายไฟให้ ESP32/เซอร์โว | — | — |
| 8 | ชิ้นส่วนพิมพ์ 3 มิติ | 1 ชุด | โครงสร้าง + Gripper (MakerWorld + ออกแบบเอง) | — | — |
| 9 | สกรู/น็อต M2, M3 | 1 ชุด | สำหรับยึดข้อต่อและ Gripper | — | — |
| 10 | สายไฟ + Jumper + แผงต่อวงจร | 1 ชุด | สำหรับเดินสายไฟ | — | — |
| รวมทั้งชุด | ___________ บาท | ||||
API_Examples.md ในรีโป