upload previous file
This commit is contained in:
@@ -1,4 +1,74 @@
|
||||
# fori_ZLTECHDRIVER_ID_SET
|
||||
# ZLAC8015D V4 RS485 슬레이브 ID 운용 가이드
|
||||
|
||||
fori_ZLTECHDRIVER_ID_SET
|
||||
ZLTECH 드라이버의 RS485 ID 부여 코드입니다.
|
||||
ZLTECH ZLAC8015D V4 듀얼 드라이버의 RS485(Modbus RTU) 슬레이브 ID를 확인/변경하기 위한 파이썬 스크립트 모음.
|
||||
프로토콜 세부 사항은 동봉된 `ZLAC8015D V4 Series RS485 Communication Version 1.06` 매뉴얼 기준.
|
||||
|
||||
## 사전 준비
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
./.venv/bin/pip install -r requirements.txt # pymodbus + pyserial
|
||||
```
|
||||
|
||||
- 통신 라이브러리로 `pymodbus`(ModbusSerialClient, RTU) 사용
|
||||
- RS485-USB 어댑터로 드라이버와 연결 (A/B 라인, GND 공통)
|
||||
- 기본 통신 설정: Baud rate 115200, 8 data bit, No parity, Stop bit 1
|
||||
- 드라이버 출고 시 기본 슬레이브 ID는 **1**
|
||||
- 실제 하드웨어 운용 테스트는 우분투 환경에서 진행 예정 (개발/문법 검증은 macOS에서 완료)
|
||||
|
||||
## 파일 구성
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `zltech_modbus.py` | pymodbus 기반 통신 공용 함수 (read/write register) |
|
||||
| `zltech_scan_ids.py` | 버스에 연결된 슬레이브 ID 스캔 |
|
||||
| `zltech_set_id.py` | 슬레이브 ID 변경 (EEPROM 저장 포함) |
|
||||
| `requirements.txt` | 의존 패키지 (pymodbus, pyserial) |
|
||||
|
||||
## 1. 슬레이브 ID 확인 (스캔)
|
||||
|
||||
```bash
|
||||
./.venv/bin/python zltech_scan_ids.py --port /dev/tty.usbserial-XXXX --range 1 16
|
||||
```
|
||||
|
||||
- `--port`: 시리얼 포트 (Ubuntu: `/dev/ttyUSB0` 등, macOS: `/dev/tty.usbserial-XXXX`, Windows: `COM5` 등)
|
||||
- `--baud`: 기본값 115200
|
||||
- `--range START END`: 스캔할 ID 범위 (기본 1~16)
|
||||
|
||||
응답한 ID 목록이 출력됨.
|
||||
|
||||
## 2. 슬레이브 ID 변경 (1과 2로 설정)
|
||||
|
||||
⚠️ **두 드라이버 모두 기본 ID가 1**이라, 동시에 버스에 연결하면 응답이 충돌합니다.
|
||||
**반드시 한 번에 하나씩만 연결**한 상태로 진행하세요.
|
||||
|
||||
### 순서
|
||||
|
||||
1. 드라이버 A만 연결 → 기본값 그대로 ID 1 유지 (변경 불필요)
|
||||
2. 드라이버 A 전원 off, 분리
|
||||
3. 드라이버 B만 연결 → ID를 2로 변경
|
||||
|
||||
```bash
|
||||
./.venv/bin/python zltech_set_id.py --port /dev/tty.usbserial-XXXX --old-id 1 --new-id 2
|
||||
```
|
||||
|
||||
4. 드라이버 B 전원을 껐다 켜서(파워사이클) 변경 사항 완전 반영
|
||||
5. 이제 드라이버 A(ID 1), B(ID 2)를 같은 버스에 함께 연결
|
||||
|
||||
6. 확인:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python zltech_scan_ids.py --port /dev/tty.usbserial-XXXX --range 1 2
|
||||
```
|
||||
|
||||
ID 1, 2 모두 응답하면 성공.
|
||||
|
||||
## 참고: 레지스터 주소
|
||||
|
||||
| 주소 | 이름 | 범위 | 비고 |
|
||||
|---|---|---|---|
|
||||
| `0x2001` | RS485 Custom Drive Node Number | 1~127 | 기본값 1 |
|
||||
| `0x2002` | RS485 custom communication baud rate | 1~6 (128000~9600bps) | 기본값 2 (115200bps) |
|
||||
| `0x2010` | RW 속성 파라미터 EEPROM 저장 | 0/1 | 1 = 저장 |
|
||||
|
||||
자세한 레지스터 목록은 동봉 매뉴얼의 "Address Directionary" 섹션 참고.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
pymodbus>=3.7
|
||||
pyserial>=3.5
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,55 @@
|
||||
"""pymodbus-based helpers for the ZLAC8015D V4 RS485 (Modbus RTU) protocol.
|
||||
|
||||
Register reference (from "ZLAC8015D V4 Series RS485 Communication" manual):
|
||||
0x2001 RS485 Custom Drive Node Number (RW/S, range 1-127, default 1)
|
||||
0x2002 RS485 custom communication baud rate (RW/S)
|
||||
0x2010 Save all RW attribute parameters to EEPROM (write 1 to save)
|
||||
|
||||
Requires: pip install pymodbus pyserial
|
||||
"""
|
||||
from pymodbus.client import ModbusSerialClient
|
||||
|
||||
REG_NODE_ID = 0x2001
|
||||
REG_SAVE_EEPROM = 0x2010
|
||||
|
||||
# pymodbus renamed the "slave id" keyword across major versions
|
||||
# (unit -> slave -> device_id). Try each so this works regardless of
|
||||
# which pymodbus version is installed (e.g. on the Ubuntu test box).
|
||||
_ID_KWARGS = ("device_id", "slave", "unit")
|
||||
|
||||
|
||||
def _call(fn, *args, node_id, **kwargs):
|
||||
last_err = None
|
||||
for kw in _ID_KWARGS:
|
||||
try:
|
||||
return fn(*args, **{kw: node_id}, **kwargs)
|
||||
except TypeError as exc:
|
||||
last_err = exc
|
||||
continue
|
||||
raise TypeError(f"pymodbus client does not accept any of {_ID_KWARGS}: {last_err}")
|
||||
|
||||
|
||||
def make_client(port: str, baud: int = 115200, timeout: float = 0.3) -> ModbusSerialClient:
|
||||
return ModbusSerialClient(
|
||||
port,
|
||||
baudrate=baud,
|
||||
bytesize=8,
|
||||
parity="N",
|
||||
stopbits=1,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
def read_register(client: ModbusSerialClient, node_id: int, address: int, count: int = 1):
|
||||
"""Return list of register values, or None on error/no response."""
|
||||
result = _call(client.read_holding_registers, address, node_id=node_id, count=count)
|
||||
if result is None or result.isError():
|
||||
return None
|
||||
return result.registers
|
||||
|
||||
|
||||
def write_register(client: ModbusSerialClient, node_id: int, address: int, value: int) -> bool:
|
||||
result = _call(client.write_register, address, value, node_id=node_id)
|
||||
if result is None or result.isError():
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scan an RS485 bus for ZLAC8015D drivers and report which node IDs answer.
|
||||
|
||||
Requires: pip install pymodbus pyserial
|
||||
|
||||
Example:
|
||||
python3 zltech_scan_ids.py --port /dev/tty.usbserial-XXXX
|
||||
python3 zltech_scan_ids.py --port COM5 --baud 115200 --range 1 10
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from zltech_modbus import REG_NODE_ID, make_client, read_register
|
||||
|
||||
|
||||
def scan(port: str, baud: int, id_range, timeout: float):
|
||||
found = []
|
||||
client = make_client(port, baud, timeout)
|
||||
if not client.connect():
|
||||
print(f"ERROR: could not open serial port {port}")
|
||||
sys.exit(1)
|
||||
try:
|
||||
for node_id in id_range:
|
||||
values = read_register(client, node_id, REG_NODE_ID, 1)
|
||||
if values is not None:
|
||||
found.append((node_id, values[0]))
|
||||
print(f" id {node_id:3d}: responded (node-id register reads {values[0]})")
|
||||
finally:
|
||||
client.close()
|
||||
return found
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--port", required=True, help="Serial port, e.g. /dev/tty.usbserial-XXXX or COM5")
|
||||
parser.add_argument("--baud", type=int, default=115200, help="Baud rate (default 115200, the driver default)")
|
||||
parser.add_argument("--range", nargs=2, type=int, metavar=("START", "END"), default=[1, 16],
|
||||
help="Inclusive node-ID range to scan (default 1 16)")
|
||||
parser.add_argument("--timeout", type=float, default=0.15, help="Per-ID response timeout in seconds")
|
||||
args = parser.parse_args()
|
||||
|
||||
start, end = args.range
|
||||
id_range = range(start, end + 1)
|
||||
|
||||
print(f"Scanning {args.port} @ {args.baud}bps for node IDs {start}-{end} ...")
|
||||
found = scan(args.port, args.baud, id_range, args.timeout)
|
||||
|
||||
print()
|
||||
if found:
|
||||
print(f"Found {len(found)} driver(s):")
|
||||
for node_id, _ in found:
|
||||
print(f" -> ID {node_id}")
|
||||
else:
|
||||
print("No drivers responded. Check wiring, baud rate, and that only powered drivers are on the bus.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Set the RS485 node ID of a ZLAC8015D driver.
|
||||
|
||||
Only ONE driver should be connected to the RS485 bus while running this
|
||||
script (both drivers ship with default ID 1, so with two on the bus their
|
||||
replies would collide). Recommended workflow to end up with IDs 1 and 2:
|
||||
|
||||
1. Connect ONLY the first driver. Leave it at the default ID:
|
||||
python3 zltech_set_id.py --port <PORT> --old-id 1 --new-id 1
|
||||
(or just skip it, it is already 1)
|
||||
2. Power off, disconnect it, connect ONLY the second driver.
|
||||
3. Set its ID to 2:
|
||||
python3 zltech_set_id.py --port <PORT> --old-id 1 --new-id 2
|
||||
4. Power-cycle that driver.
|
||||
5. Now connect both drivers to the same bus; they will answer as
|
||||
ID 1 and ID 2 respectively.
|
||||
|
||||
Requires: pip install pymodbus pyserial
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from zltech_modbus import REG_NODE_ID, REG_SAVE_EEPROM, make_client, read_register, write_register
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--port", required=True, help="Serial port, e.g. /dev/tty.usbserial-XXXX or COM5")
|
||||
parser.add_argument("--baud", type=int, default=115200, help="Baud rate (default 115200, the driver default)")
|
||||
parser.add_argument("--old-id", type=int, required=True, help="Current node ID of the driver (1-127)")
|
||||
parser.add_argument("--new-id", type=int, required=True, help="New node ID to assign (1-127)")
|
||||
parser.add_argument("--timeout", type=float, default=0.3, help="Response timeout in seconds")
|
||||
args = parser.parse_args()
|
||||
|
||||
for name, value in (("--old-id", args.old_id), ("--new-id", args.new_id)):
|
||||
if not (1 <= value <= 127):
|
||||
parser.error(f"{name} must be in range 1-127")
|
||||
|
||||
client = make_client(args.port, args.baud, args.timeout)
|
||||
if not client.connect():
|
||||
print(f"ERROR: could not open serial port {args.port}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
print(f"Checking driver at ID {args.old_id} ...")
|
||||
values = read_register(client, args.old_id, REG_NODE_ID, 1)
|
||||
if values is None:
|
||||
print(f"ERROR: no response from ID {args.old_id}. Check wiring/baud rate, "
|
||||
f"and that exactly one driver is on the bus.")
|
||||
sys.exit(1)
|
||||
print(f" driver responded, current node-id register = {values[0]}")
|
||||
|
||||
print(f"Writing new node ID {args.new_id} ...")
|
||||
if not write_register(client, args.old_id, REG_NODE_ID, args.new_id):
|
||||
print("ERROR: write did not get a valid response.")
|
||||
sys.exit(1)
|
||||
|
||||
# The ID change can take effect immediately, so try to save under
|
||||
# the new ID first, and fall back to the old ID if that fails.
|
||||
time.sleep(0.05)
|
||||
saved = write_register(client, args.new_id, REG_SAVE_EEPROM, 1)
|
||||
if not saved:
|
||||
saved = write_register(client, args.old_id, REG_SAVE_EEPROM, 1)
|
||||
if saved:
|
||||
print("Saved to EEPROM.")
|
||||
else:
|
||||
print("WARNING: could not confirm the save-to-EEPROM write; "
|
||||
"the ID change may be lost on power-cycle.")
|
||||
|
||||
print(f"Done. Power-cycle the driver, then verify with:\n"
|
||||
f" python3 zltech_scan_ids.py --port {args.port} --range {args.new_id} {args.new_id}")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user