TCP socket Python: gửi kết quả và tọa độ cho robot
Viết server, client TCP; đóng khung tin bằng ký tự kết thúc, xử lý tin dính/vỡ, timeout, kết nối lại và gửi lại không gắp hai lần.
Học xong bài này, bạn sẽ
- Viết server, client TCP bằng Python
- Thiết kế giao thức tin nhắn có kết thúc rõ ràng
- Xử lý mất kết nối, timeout
Nên học trước: OPC UA cho kỹ sư Vision
Nội dung bài viết
Modbus và OPC UA hợp với dữ liệu kiểu "ô nhớ": vài bit, vài thanh ghi. Nhưng khi vision cần gửi cho robot tọa độ gắp (x, y, góc) cho từng vật, hoặc nhận lệnh "chụp đi" từ robot, cách phổ biến nhất trong thực tế lại là TCP socket với tin nhắn văn bản tự định nghĩa: robot của gần như mọi hãng đều có lệnh mở socket, gửi và đọc chuỗi. Đơn giản đến mức rất nhiều trạm chạy "gần đúng" trong nhiều tháng rồi mới hỏng vì một lỗi kinh điển: coi mỗi lần recv là một tin. Bài này nối tiếp "Modbus TCP bằng Python với pymodbus" và "OPC UA cho kỹ sư Vision", dạy bạn viết client/server TCP đúng cách, thiết kế giao thức có kết thúc rõ ràng và chịu được mất kết nối. Bài sau, "Vision dẫn hướng robot gắp vật", sẽ dùng chính kênh này để gửi tọa độ đã đổi sang hệ robot.
socket; thiết kế tin nhắn có ký tự kết thúc, số thập phân cố định, số thứ tự và xác nhận ACK; xử lý tin dính/tin vỡ bằng bộ đệm; xử lý timeout, mất kết nối, kết nối lại và gửi lại mà robot không gắp hai lần.1. TCP là dòng byte, không phải gói tin
TCP đảm bảo các byte đến đủ và đúng thứ tự, nhưng không giữ ranh giới giữa các lần gửi. Hai lần sendall có thể đến bên kia trong một lần recv (tin dính), một lần sendall có thể bị chia ra nhiều lần recv (tin vỡ). Trên mạng thật điều này phụ thuộc kích thước gói, thuật toán Nagle, tải của máy; trên bàn thử nghiệm thì hầu như không xảy ra, nên lỗi chỉ lộ ra ở hiện trường. Ví dụ dưới dùng socket.socketpair() (hai đầu của một kết nối dòng byte, không cần mạng) để tái hiện cả hai trường hợp một cách chắc chắn:
import socket
vision, robot = socket.socketpair() # hai đầu của một kết nối dòng byte, không cần mạng
# Trường hợp 1: vision gửi hai tin liên tiếp, robot recv một lần
vision.sendall(b"OK,123.45,67.89,30.00\r\n")
vision.sendall(b"NG,0.00,0.00,0.00\r\n")
lan1 = robot.recv(1024)
print("recv 1:", lan1)
# Trường hợp 2: một tin đến thành hai mảnh
vision.sendall(b"OK,101.2")
print("recv 2:", robot.recv(1024))
vision.sendall(b"5,55.10,90.00\r\n")
print("recv 3:", robot.recv(1024))
# Code "ngây thơ": coi mỗi lần recv là một tin
truong = lan1.decode("ascii").strip().split(",")
print("Robot hiểu:", len(truong), "trường →", truong[:5], "...")
vision.close()
robot.close()
recv 1: b'OK,123.45,67.89,30.00\r\nNG,0.00,0.00,0.00\r\n'
recv 2: b'OK,101.2'
recv 3: b'5,55.10,90.00\r\n'
Robot hiểu: 7 trường → ['OK', '123.45', '67.89', '30.00\r\nNG', '0.00'] ...
Robot đọc theo kiểu "mỗi recv là một tin" sẽ thấy 7 trường, trường thứ tư là '30.00\r\nNG': tùy cách robot xử lý lỗi, nó dừng chương trình, hoặc tệ hơn, gắp ở góc sai. Ở trường hợp 2, nó nhận OK,101.2 và có thể hiểu x = 101.2 thay vì 101.25.
2. Đóng khung tin nhắn (framing)
Vì TCP không có ranh giới, chính giao thức của bạn phải nói tin nhắn kết thúc ở đâu. Hai cách chuẩn:
| Ký tự kết thúc (delimiter) | Tiền tố độ dài (length prefix) | |
|---|---|---|
| Ví dụ | OK,123.45,67.89,30.00\r\n | 00 1F + 31 byte nội dung |
| Ưu điểm | Đọc được bằng mắt, gõ thử bằng công cụ terminal, robot nào cũng có lệnh "đọc đến ký tự" | Chứa được dữ liệu bất kỳ (nhị phân, JSON nhiều dòng, ảnh), biết trước cần đọc bao nhiêu |
| Nhược điểm | Nội dung không được chứa ký tự kết thúc | Lệch 1 byte là lệch toàn bộ phần sau; khó gõ thử bằng tay |
| Dùng khi | Giao tiếp với robot, PLC, thiết bị đọc mã (phổ biến nhất) | PC ↔ PC, gửi ảnh, gửi bản ghi lớn |
Ký tự kết thúc hay dùng là \r\n (CR LF) hoặc chỉ \r, tùy thiết bị. Ghi rõ trong tài liệu giao thức, kèm mã hex (0x0D 0x0A), vì "xuống dòng" là cách nói không đủ chính xác.
3. Bộ đệm nhận: gom byte thành tin
Cách đúng: mọi byte nhận được đổ vào một bộ đệm; mỗi lần đổ, cắt ra tất cả các tin đã có đủ ký tự kết thúc, phần dở dang giữ lại chờ lần sau. Thêm một giới hạn độ dài để rác trên đường truyền (hoặc bên kia quên ký tự kết thúc) không làm bộ đệm phình mãi.
class BoDemDong:
"""Gom dòng byte thành từng tin hoàn chỉnh kết thúc bằng b'\\r\\n'."""
def __init__(self, ket_thuc=b"\r\n", dai_toi_da=256):
self.buf = bytearray()
self.ket_thuc = ket_thuc
self.dai_toi_da = dai_toi_da
def nap(self, du_lieu):
"""Nạp một khối byte vừa recv được, trả về danh sách các tin ĐỦ (chưa có ký tự kết thúc)."""
self.buf += du_lieu
cac_tin = []
while True:
vi_tri = self.buf.find(self.ket_thuc)
if vi_tri < 0:
break
cac_tin.append(bytes(self.buf[:vi_tri]).decode("ascii"))
del self.buf[:vi_tri + len(self.ket_thuc)]
if len(self.buf) > self.dai_toi_da: # rác hoặc thiếu ký tự kết thúc: đừng phình bộ nhớ
raise ValueError(f"Tin quá dài ({len(self.buf)} byte) mà chưa thấy ký tự kết thúc")
return cac_tin
dong = b"OK,123.45,67.89,30.00\r\nNG,0.00,0.00,0.00\r\nOK,101.25,55.10,90.00\r\n"
# Cắt dòng byte theo ba cách khác nhau: kết quả phải giống hệt nhau
for kich_thuoc in (len(dong), 7, 1):
bd = BoDemDong()
tin = []
for i in range(0, len(dong), kich_thuoc):
tin += bd.nap(dong[i:i + kich_thuoc])
print(f"mảnh {kich_thuoc:>2} byte →", tin)
mảnh 65 byte → ['OK,123.45,67.89,30.00', 'NG,0.00,0.00,0.00', 'OK,101.25,55.10,90.00']
mảnh 7 byte → ['OK,123.45,67.89,30.00', 'NG,0.00,0.00,0.00', 'OK,101.25,55.10,90.00']
mảnh 1 byte → ['OK,123.45,67.89,30.00', 'NG,0.00,0.00,0.00', 'OK,101.25,55.10,90.00']
Dù dòng byte đến nguyên khối, từng 7 byte hay từng byte một, kết quả vẫn là đúng ba tin. Đây là bài kiểm tra bạn nên viết cho mọi bộ phân tích giao thức.
4. Thiết kế tin nhắn cho robot
Dạng tối thiểu là OK,123.45,67.89,30.00\r\n. Trong dự án thật, nên thêm số thứ tự ở đầu và yêu cầu robot xác nhận:
Vision → Robot : 0007,OK,123.46,67.89,30.00\r\n
│ │ │ │ └ góc (độ), 2 chữ số thập phân
│ │ │ └ y (mm, hệ tọa độ robot)
│ │ └ x (mm, hệ tọa độ robot)
│ └ OK = gắp, NG = bỏ qua
└ số thứ tự 0000–9999, quay vòng
Robot → Vision : ACK,0007\r\n
- Số thập phân cố định (
{x:.2f}): độ dài tin ổn định, robot dễ cắt chuỗi, và không bao giờ xuất hiện dạng1e-05hay123.45600000000002. - Luôn dùng dấu chấm thập phân, ASCII. Python f-string luôn dùng dấu chấm, nhưng nếu phần mềm vision viết bằng C#/.NET chạy trên Windows cài định dạng Việt Nam,
ToString()sẽ ra123,45và phá vỡ dấu phẩy phân cách trường. Bên .NET phải dùngCultureInfo.InvariantCulture. - Đơn vị và hệ tọa độ ghi trong tài liệu: mm hay 0,01 mm, hệ tọa độ user frame nào của robot, góc theo chiều nào, khoảng giá trị hợp lệ.
- Số thứ tự + ACK: vision biết robot đã nhận đúng tin nào; khi phải gửi lại, robot nhận ra tin trùng và không gắp hai lần (mục 7).
- Bên nhận kiểm tra, không đoán: sai số trường, sai chữ, số ngoài khoảng → từ chối và báo lỗi.
def tao_tin(seq, ok, x, y, goc):
"""Tin vision → robot: seq,OK|NG,x,y,góc (mm, mm, độ; luôn 2 chữ số thập phân, dấu chấm)."""
if not (0 <= seq <= 9999):
raise ValueError("seq phải trong 0..9999")
return f"{seq:04d},{'OK' if ok else 'NG'},{x:.2f},{y:.2f},{goc:.2f}\r\n".encode("ascii")
def doc_tin(dong):
"""Phía robot: tách và KIỂM TRA một tin. Sai định dạng thì báo lỗi, không đoán."""
truong = dong.split(",")
if len(truong) != 5:
raise ValueError(f"cần 5 trường, nhận {len(truong)}: {dong!r}")
seq, kq, x, y, goc = truong
if kq not in ("OK", "NG"):
raise ValueError(f"kết quả lạ: {kq!r}")
return {"seq": int(seq), "ok": kq == "OK", "x": float(x), "y": float(y), "goc": float(goc)}
print(tao_tin(7, True, 123.4567, 67.891, 30))
print(tao_tin(8, False, 0, 0, 0))
print(doc_tin("0007,OK,123.46,67.89,30.00"))
for xau in ["0007,OK,123,46,67,89,30,00", "0007,ok,1.00,2.00,3.00"]:
try:
doc_tin(xau)
except ValueError as e:
print("Từ chối:", e)
b'0007,OK,123.46,67.89,30.00\r\n'
b'0008,NG,0.00,0.00,0.00\r\n'
{'seq': 7, 'ok': True, 'x': 123.46, 'y': 67.89, 'goc': 30.0}
Từ chối: cần 5 trường, nhận 8: '0007,OK,123,46,67,89,30,00'
Từ chối: kết quả lạ: 'ok'
Tin thứ hai bị từ chối chính là trường hợp dấu phẩy thập phân kiểu Việt Nam. Nhờ kiểm tra số trường, lỗi bị chặn ngay thay vì thành tọa độ x = 123, y = 46.
5. Cách thứ hai: tiền tố độ dài
Khi gửi dữ liệu lớn hoặc nhị phân giữa hai chương trình PC (ví dụ gửi bản ghi JSON nhiều điểm, hay ảnh), dùng 2 hoặc 4 byte đầu để ghi độ dài. Chìa khóa là hàm nhan_du: recv(n) có thể trả về ít hơn n byte, nên phải lặp tới khi đủ.
import json
import socket
import struct
def gui_khung(sock, payload: bytes):
sock.sendall(struct.pack(">H", len(payload)) + payload) # 2 byte độ dài (big-endian) + nội dung
def nhan_du(sock, n):
"""recv tới khi đủ đúng n byte (recv có thể trả về ít hơn n)."""
buf = bytearray()
while len(buf) < n:
manh = sock.recv(n - len(buf))
if not manh:
raise ConnectionError("đầu kia đóng kết nối giữa chừng")
buf += manh
return bytes(buf)
def nhan_khung(sock):
(do_dai,) = struct.unpack(">H", nhan_du(sock, 2))
return nhan_du(sock, do_dai)
a, b = socket.socketpair()
tin = json.dumps({"seq": 9, "ok": True, "diem": [[10.5, 20.25], [30.0, 40.75]]}).encode()
gui_khung(a, tin)
gui_khung(a, b"") # khung rỗng vẫn hợp lệ
print(len(tin), "byte →", json.loads(nhan_khung(b)))
print("khung 2:", nhan_khung(b))
a.close()
b.close()
62 byte → {'seq': 9, 'ok': True, 'diem': [[10.5, 20.25], [30.0, 40.75]]}
khung 2: b''
6. Ai là server, ai là client?
Server mở cổng và chờ (listen/accept); client chủ động kết nối (connect). Về lâu dài, quy tắc quan trọng nhất là: client chịu trách nhiệm kết nối lại, server chỉ cần luôn sẵn sàng nhận kết nối mới.
| Cách bố trí | Khi nào hợp | Lưu ý |
|---|---|---|
| Robot là server, vision là client | Robot đứng chờ lệnh; vision chủ động gửi khi có kết quả | Vision phải tự kết nối lại khi robot khởi động lại chương trình |
| Vision là server, robot là client | Robot điều khiển nhịp: gửi "TRIG", chờ tọa độ (rất phổ biến trong pick & place) | Vision phải chấp nhận kết nối mới khi kết nối cũ chết; chỉ giữ một kết nối hoạt động |
| Hai kết nối, mỗi bên một server | Ít khi cần | Phức tạp gấp đôi, tránh nếu được |
Hỏi kỹ sư robot xem bộ điều khiển hỗ trợ vai nào, ký tự kết thúc nào, độ dài chuỗi tối đa bao nhiêu, và robot làm gì khi socket đứt giữa chương trình (dừng có báo lỗi, hay tự mở lại).
7. Timeout, mất kết nối, kết nối lại và gửi lại
Các tình huống phía client phải xử lý:
| Tình huống | Python báo thế nào | Xử lý |
|---|---|---|
| Robot chưa bật / sai IP | ConnectionRefusedError hoặc TimeoutError khi connect | Chờ rồi thử lại, báo lỗi cho PLC nếu quá số lần |
| Robot đóng kết nối "lịch sự" | recv trả về b"" (không phải exception!) | Coi như mất kết nối: đóng, kết nối lại |
| Robot reset kết nối | ConnectionResetError, BrokenPipeError | Như trên |
| Robot im lặng (bận, treo, rút dây) | Không có gì cả, nếu không đặt timeout thì recv chờ mãi | Luôn settimeout; quá hạn chờ ACK → đóng, kết nối lại, gửi lại |
Một chi tiết hay bị bỏ qua: khi rút dây mạng, không có gói nào báo cho bên kia biết. Cơ chế keepalive của TCP mặc định tắt, và kể cả khi bật (SO_KEEPALIVE), Linux mặc định chỉ bắt đầu thăm dò sau 2 giờ im lặng. Vì vậy phát hiện mất kết nối phải dựa vào timeout của giao thức (chờ ACK, hoặc tin heartbeat định kỳ), không trông chờ hệ điều hành.
Gửi lại sinh ra vấn đề mới: nếu robot đã gắp nhưng ACK đến trễ, vision gửi lại và robot gắp lần hai vào chỗ trống. Số thứ tự giải quyết việc này: robot nhớ các seq đã thực hiện, gặp lại thì chỉ ACK lại. Demo dưới chạy một robot giả làm server trên 127.0.0.1:6020 với hai sự cố: rớt kết nối khi nhận tin #3 (chưa kịp gắp), và ACK trễ 1,3 s ở tin #5 trong khi vision chỉ chờ 1 s. Code dùng lại BoDemDong, tao_tin, doc_tin ở trên.
import socket
import threading
import time
class RobotGia:
"""Robot giả làm TCP server. Kịch bản lỗi: rớt kết nối khi nhận tin #3, ACK chậm ở tin #5."""
def __init__(self, port, rot_o=3, cham_o=5, tre_ack=1.3):
self.port, self.rot_o, self.cham_o, self.tre_ack = port, rot_o, cham_o, tre_ack
self.dung = threading.Event()
self.da_rot = self.da_cham = False
self.da_thuc_hien = set()
self.nhat_ky = []
self.srv = socket.create_server(("127.0.0.1", port)) # mở cổng ngay, trước khi client kết nối
self.srv.settimeout(0.2)
self.thread = threading.Thread(target=self._chay, daemon=True)
self.thread.start()
def _chay(self):
with self.srv:
while not self.dung.is_set():
try:
conn, _ = self.srv.accept()
except TimeoutError:
continue
self.nhat_ky.append("robot: có client kết nối")
with conn:
self._phuc_vu(conn)
def _phuc_vu(self, conn):
conn.settimeout(0.2)
bd = BoDemDong()
while not self.dung.is_set():
try:
du_lieu = conn.recv(1024)
except TimeoutError:
continue
except OSError:
return
if not du_lieu: # client đã đóng
return
for dong in bd.nap(du_lieu):
tin = doc_tin(dong)
seq = tin["seq"]
if seq == self.rot_o and not self.da_rot:
self.da_rot = True
self.nhat_ky.append(f"robot: nhận #{seq} rồi rớt kết nối (chưa thực hiện)")
return
if seq in self.da_thuc_hien:
self.nhat_ky.append(f"robot: #{seq} bị gửi lại, KHÔNG gắp lần hai, chỉ ACK lại")
else:
self.da_thuc_hien.add(seq)
hanh_dong = f"gắp tại ({tin['x']}, {tin['y']}, {tin['goc']}°)" if tin["ok"] else "bỏ qua (NG)"
self.nhat_ky.append(f"robot: #{seq} {hanh_dong}")
if seq == self.cham_o and not self.da_cham:
self.da_cham = True
time.sleep(self.tre_ack) # robot bận, ACK trễ
try:
conn.sendall(f"ACK,{seq:04d}\r\n".encode("ascii"))
except OSError:
return
def tat(self):
self.dung.set()
self.thread.join(timeout=2)
class GuiRobot:
"""Phía vision là TCP client: gửi tin, chờ ACK đúng seq, có timeout và kết nối lại."""
def __init__(self, host, port, timeout=1.0, so_lan_thu=3):
self.host, self.port, self.timeout, self.so_lan_thu = host, port, timeout, so_lan_thu
self.sock = None
def _ket_noi(self):
self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # tin nhỏ: gửi ngay
self.bd = BoDemDong() # bỏ mọi dữ liệu dở dang cũ
def _dong(self):
if self.sock:
self.sock.close()
self.sock = None
def _cho_ack(self, seq):
han = time.monotonic() + self.timeout
while True:
con_lai = han - time.monotonic()
if con_lai <= 0:
raise TimeoutError("quá hạn chờ ACK")
self.sock.settimeout(con_lai)
du_lieu = self.sock.recv(1024)
if not du_lieu:
raise ConnectionError("robot đóng kết nối")
for dong in self.bd.nap(du_lieu):
if dong == f"ACK,{seq:04d}":
return
# ACK của tin cũ hoặc tin lạ: bỏ qua, tiếp tục chờ
def gui(self, seq, ok, x, y, goc):
tin = tao_tin(seq, ok, x, y, goc)
for lan in range(1, self.so_lan_thu + 1):
try:
if self.sock is None:
self._ket_noi()
self.sock.sendall(tin)
self._cho_ack(seq)
return True
except OSError as e: # TimeoutError, ConnectionError... đều là OSError
print(f" #{seq} lần {lan}: {type(e).__name__} ({e}) → đóng socket, thử lại")
self._dong()
time.sleep(0.1)
return False
def dong(self):
self._dong()
robot = RobotGia(port=6020)
vision = GuiRobot("127.0.0.1", 6020, timeout=1.0)
cac_vat = [(True, 120.5, 60.25, 15.0), (False, 0, 0, 0), (True, 98.123, 70.0, -30.5),
(True, 101.0, 65.5, 0.0), (True, 130.75, 58.1, 45.0)]
for seq, (ok, x, y, goc) in enumerate(cac_vat, start=1):
thanh_cong = vision.gui(seq, ok, x, y, goc)
print(f"#{seq}: {'đã ACK' if thanh_cong else 'THẤT BẠI → báo lỗi cho PLC'}")
vision.dong()
time.sleep(0.3) # cho robot ghi nốt nhật ký
robot.tat()
print("\n".join(robot.nhat_ky))
#1: đã ACK
#2: đã ACK
#3 lần 1: ConnectionError (robot đóng kết nối) → đóng socket, thử lại
#3: đã ACK
#4: đã ACK
#5 lần 1: TimeoutError (timed out) → đóng socket, thử lại
#5: đã ACK
robot: có client kết nối
robot: #1 gắp tại (120.5, 60.25, 15.0°)
robot: #2 bỏ qua (NG)
robot: nhận #3 rồi rớt kết nối (chưa thực hiện)
robot: có client kết nối
robot: #3 gắp tại (98.12, 70.0, -30.5°)
robot: #4 gắp tại (101.0, 65.5, 0.0°)
robot: #5 gắp tại (130.75, 58.1, 45.0°)
robot: có client kết nối
robot: #5 bị gửi lại, KHÔNG gắp lần hai, chỉ ACK lại
Đọc kết quả: tin #3 bị rớt trước khi robot thực hiện, vision kết nối lại và gửi lại, robot gắp đúng một lần. Tin #5 thì ngược lại: robot đã gắp nhưng ACK trễ, vision hết giờ, kết nối lại và gửi lại; nhờ số thứ tự, robot nhận ra tin trùng và không gắp lần hai. Khi thử hết so_lan_thu mà vẫn thất bại, hàm trả về False: lúc đó vision phải báo lỗi cho PLC (ERROR) để dừng hoặc giữ sản phẩm, không được im lặng bỏ qua.
8. Lỗi thường gặp
| Hiện tượng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Thỉnh thoảng robot báo "định dạng sai" hoặc gắp lệch | Coi mỗi recv là một tin | Bộ đệm + ký tự kết thúc |
| Chương trình treo, không báo lỗi | recv không có timeout | settimeout mọi socket, kể cả accept |
| CPU 100%, log đầy dòng trống | Không xử lý recv trả về b"", vòng lặp chạy liên tục | b"" = đầu kia đã đóng: thoát vòng, kết nối lại |
| Chỉ gửi được một phần tin | Dùng send thay vì sendall | Luôn sendall |
| Tọa độ sai sau khi đổi máy tính | Định dạng số theo vùng (dấu phẩy thập phân) | Định dạng cố định, dấu chấm, bên nhận kiểm tra số trường |
| Robot gắp hai lần một vật | Gửi lại sau timeout mà không có số thứ tự | seq + robot bỏ qua tin trùng |
OSError: Address already in use khi chạy lại server | Cổng cũ còn trong trạng thái chờ đóng | socket.create_server đã bật SO_REUSEADDR trên Linux; đóng server đúng cách khi thoát |
9. Bài tập
Bài 1. Bộ điều khiển robot chỉ đọc được chuỗi tối đa 32 ký tự mỗi tin (kể cả ký tự kết thúc). Biết x, y nằm trong khoảng −9999,99…9999,99 mm và góc trong −180…180 độ, định dạng ở mục 4 có vừa không? Nếu không, bạn sửa thế nào?
Xem đáp án
tin_dai_nhat = tao_tin(9999, True, -9999.99, -9999.99, -180)
print(tin_dai_nhat, len(tin_dai_nhat), "byte")
b'9999,OK,-9999.99,-9999.99,-180.00\r\n' 35 byte
35 byte, không vừa. Các cách sửa: thu hẹp khoảng giá trị theo vùng làm việc thật của robot (ví dụ x, y trong −999,99…999,99 thì tiết kiệm 2 byte), bỏ số 0 đệm của seq, rút gọn OK/NG thành 1/0 (tiết kiệm 1 byte), hoặc gửi đơn vị 0,1 mm với 1 chữ số thập phân. Dù chọn gì, ghi rõ trong tài liệu giao thức và để tao_tin báo lỗi khi vượt độ dài thay vì cắt bớt âm thầm.
Bài 2. Đổi vai: robot là client, gửi TRIG,<seq>\r\n; vision là server, mỗi lần nhận trigger thì trả về một tin kết quả theo định dạng mục 4. Viết cả hai phía, chạy trên 127.0.0.1.
Xem đáp án
import socket
import threading
def ket_qua_gia(seq):
return (seq % 3 != 0), 100 + seq * 1.5, 50 - seq * 0.25, 10.0 * seq # vật thứ 3, 6… là NG
def vision_server(srv, so_lan):
"""Vision là SERVER: chờ robot kết nối, mỗi 'TRIG,<seq>' trả về một tin kết quả."""
conn, _ = srv.accept()
with conn:
conn.settimeout(2.0)
bd = BoDemDong()
da_tra = 0
while da_tra < so_lan:
du_lieu = conn.recv(1024)
if not du_lieu:
break
for dong in bd.nap(du_lieu):
lenh, seq = dong.split(",")
if lenh == "TRIG":
conn.sendall(tao_tin(int(seq), *ket_qua_gia(int(seq))))
da_tra += 1
srv = socket.create_server(("127.0.0.1", 6021))
srv.settimeout(3.0)
t = threading.Thread(target=vision_server, args=(srv, 3), daemon=True)
t.start()
# Robot là CLIENT
with socket.create_connection(("127.0.0.1", 6021), timeout=2.0) as robot:
bd = BoDemDong()
for seq in (1, 2, 3):
robot.sendall(f"TRIG,{seq:04d}\r\n".encode("ascii"))
tin = []
while not tin:
du_lieu = robot.recv(1024)
if not du_lieu:
raise ConnectionError("vision đóng kết nối")
tin = bd.nap(du_lieu)
print("robot nhận:", doc_tin(tin[0]))
t.join(timeout=3)
srv.close()
robot nhận: {'seq': 1, 'ok': True, 'x': 101.5, 'y': 49.75, 'goc': 10.0}
robot nhận: {'seq': 2, 'ok': True, 'x': 103.0, 'y': 49.5, 'goc': 20.0}
robot nhận: {'seq': 3, 'ok': False, 'x': 104.5, 'y': 49.25, 'goc': 30.0}
Server đặt timeout cho cả accept và recv để không treo mãi; phía robot cũng dùng BoDemDong vì tin kết quả có thể đến thành nhiều mảnh. Trong trạm thật, vòng accept phải nằm trong vòng lặp ngoài để chấp nhận kết nối mới mỗi khi robot khởi động lại.
Bài 3. Một trạm chạy tốt nhiều tuần, rồi một hôm ai đó rút nhầm dây mạng của robot 10 giây rồi cắm lại. Từ đó vision báo "đã gửi" nhưng robot không nhận gì, đến khi khởi động lại phần mềm vision. Giải thích và đề xuất sửa.
Xem đáp án
Khi rút dây, không bên nào nhận được gói đóng kết nối. Robot thường phát hiện socket hỏng (hoặc chương trình robot bị reset) và mở lại cổng chờ kết nối mới, trong khi vision vẫn giữ socket cũ "nửa mở": sendall vẫn thành công vì dữ liệu chỉ được chép vào bộ đệm gửi của hệ điều hành. Nếu vision không chờ ACK, nó không bao giờ biết. Sửa: (1) luôn chờ ACK có timeout, timeout → đóng socket và kết nối lại như demo; (2) gửi heartbeat định kỳ khi không có sản phẩm, để phát hiện sự cố trước khi có vật cần gắp; (3) ghi log mọi lần kết nối lại để bảo trì biết chuyện gì đã xảy ra.
Tóm tắt
- TCP là dòng byte: tin có thể dính hoặc vỡ. Không bao giờ coi một lần
recvlà một tin. - Đóng khung bằng ký tự kết thúc (
\r\n, hợp với robot/PLC) hoặc tiền tố độ dài (hợp PC ↔ PC, dữ liệu lớn); dùng bộ đệm hoặcnhan_du. - Tin cho robot: số thứ tự, OK/NG, tọa độ thập phân cố định, dấu chấm, ASCII; robot trả ACK kèm seq; bên nhận kiểm tra định dạng.
- Client chịu trách nhiệm kết nối lại; server luôn sẵn sàng nhận kết nối mới.
- Luôn đặt timeout;
b""nghĩa là đầu kia đã đóng; sau timeout thì đóng socket, kết nối lại, gửi lại; seq giúp robot không thực hiện hai lần.
Bài tiếp theo: Vision dẫn hướng robot gắp vật, nơi tọa độ ảnh được đổi sang tọa độ robot trước khi gửi qua kênh socket bạn vừa xây.


