Skip to content

3.14 ModbusClient 마스터 기능

개요

Arm::modbusClient 는 로봇이 Modbus 마스터로 동작할 때의 시리얼 파라미터 관리, 슬레이브 세션 생성, 레지스터 읽기/쓰기, 시리얼 passthrough 기능에 대응합니다.

현재 C++17 공개 기능은 두 계층으로 나뉩니다:

  1. ModbusClient 인터페이스 모듈: GetSlave / GetParam / SetParam 담당
  2. modbus_client::Slave 세션: 실제 읽기/쓰기, 시리얼 송수신, 세션별 파라미터 조회 담당

권장 호출 경로

cpp
Arm arm;  // 로봇 세션 객체 생성
STATUS_CODE connectRet = arm.Connect("10.27.1.254", "");  // 컨트롤러 또는 라우터 주소에 연결
if (connectRet != STATUS_CODE::OK) {  // 연결 실패 여부 확인
    return;  // 연결 실패 시 현재 흐름 종료
}  // 연결 판단 종료
auto [slave, slaveRet] = arm.modbusClient.GetSlave(  // Modbus 슬레이브 세션 생성
    modbus_client::MODBUS_CHANNEL::CONTROLLER_485,  // 컨트롤러 485 채널 지정
    1,  // 슬레이브 ID 를 1 로 지정
    0  // masterId 를 0 으로 지정
);  // 슬레이브 세션 생성 호출 종료
if (slaveRet != STATUS_CODE::OK || !slave.IsValid()) {  // 세션 생성 성공 및 유효 여부 확인
    return;  // 세션을 사용할 수 없으면 현재 흐름 종료
}  // 세션 판단 종료
auto [values, readRet] = slave.ReadCoils(0, 2);  // 주소 0 부터 코일 값 2개 읽기
if (readRet != STATUS_CODE::OK) {  // 읽기 실패 여부 확인
    return;  // 읽기 실패 시 현재 흐름 종료
}  // 읽기 판단 종료

인터페이스 개요

ModbusClient 인터페이스 모듈

cpp
GetSlave(channel, slaveId, masterId) -> std::pair<modbus_client::Slave, STATUS_CODE>
GetParam(channel, masterId = 1) -> std::pair<modbus_client::SerialParams, STATUS_CODE>
SetParam(const modbus_client::SerialParams& params) -> std::pair<int32_t, STATUS_CODE>
메서드입력출력주요 동작
GetSlave채널, 슬레이브 ID, 마스터 IDstd::pair<modbus_client::Slave, STATUS_CODE>로컬 세션만 생성하며 즉시 요청을 보내지 않음
GetParam채널, 마스터 IDstd::pair<modbus_client::SerialParams, STATUS_CODE>TCP_TO_485 채널은 masterId 기준으로 조회
SetParam시리얼 파라미터std::pair<int32_t, STATUS_CODE>컨트롤러가 확인한 파라미터 레코드 id 반환

modbus_client::Slave 세션

cpp
IsValid() const -> bool
ReadCoils(addr, num) -> std::pair<std::vector<int32_t>, STATUS_CODE>
WriteCoils(addr, values) -> STATUS_CODE
ReadHoldingRegs(addr, num) -> std::pair<std::vector<int32_t>, STATUS_CODE>
WriteHoldingRegs(addr, values) -> STATUS_CODE
ReadDiscreteInputs(addr, num) -> std::pair<std::vector<int32_t>, STATUS_CODE>
ReadInputRegs(addr, num) -> std::pair<std::vector<int32_t>, STATUS_CODE>
WriteInputRegs(addr, values) -> STATUS_CODE
GetParam() -> std::pair<modbus_client::SerialParams, STATUS_CODE>
SetParam(params) -> std::pair<int32_t, STATUS_CODE>
SerialSend(msg) -> STATUS_CODE
SerialReceive() -> std::pair<std::string, STATUS_CODE>
메서드입력출력주요 동작
IsValid없음bool세션이 여전히 유효한 Arm 에 바인딩되어 있는지만 확인
ReadCoils시작 주소, 개수std::pair<std::vector<int32_t>, STATUS_CODE>1..120 및 주소 범위를 로컬에서 검증
WriteCoils시작 주소, 값 목록STATUS_CODE쓰기 개수 1..1024 를 로컬에서 검증
ReadHoldingRegs시작 주소, 개수std::pair<std::vector<int32_t>, STATUS_CODE>ReadCoils 와 동일한 동작
WriteHoldingRegs시작 주소, 값 목록STATUS_CODEWriteCoils 와 동일한 동작
ReadDiscreteInputs시작 주소, 개수std::pair<std::vector<int32_t>, STATUS_CODE>ReadCoils 와 동일한 동작
ReadInputRegs시작 주소, 개수std::pair<std::vector<int32_t>, STATUS_CODE>ReadCoils 와 동일한 동작
WriteInputRegs시작 주소, 값 목록STATUS_CODEWriteCoils 와 동일한 동작
GetParam없음std::pair<modbus_client::SerialParams, STATUS_CODE>현재 Slave 가 저장한 채널 / 마스터 컨텍스트 사용
SetParam시리얼 파라미터std::pair<int32_t, STATUS_CODE>현재 세션을 통해 설정 인터페이스 호출
SerialSend텍스트 시리얼 메시지STATUS_CODE채널별 시리얼 passthrough 프로토콜 사용
SerialReceive없음std::pair<std::string, STATUS_CODE>passthrough 로 수신한 원본 문자열 반환

전제 조건과 수명 주기

항목규칙
연결 전제먼저 Arm::Connect() 에 성공해야 하며, 그렇지 않으면 ModbusClient 인터페이스 모듈은 NOT_CONNECTED 를 반환
GetSlave로컬 Slave 를 생성합니다. 원격 슬레이브 상태는 이후 읽기/쓰기 결과로 확인합니다.
세션 무효화Arm::Disconnect() 후에는 기존 modbus_client::Slave 가 모두 무효화됨
IsValid현재 세션이 유효한 Arm 과 계속 바인딩되어 있는지 확인합니다. 원격 온라인 상태는 읽기/쓰기 결과로 확인합니다.
버스 설정 충돌호출자가 컨트롤러의 버스 설정 충돌 여부를 직접 확인해야 합니다.

파라미터 검증 및 실패 기준

시나리오반환
modbus_client::Slave 세션 무효INVALID_SESSION
addr < 0INVALID_PARAMETER
num < 1 또는 num > 120INVALID_PARAMETER
addr + num - 1 > 65535INVALID_PARAMETER
values.size() < 1 또는 values.size() > 1024INVALID_PARAMETER
addr + values.size() - 1 > 65535INVALID_PARAMETER
컨트롤러가 예상과 다른 JSON 구조 반환OTHER_ERR

추가 설명:

  1. 읽기 인터페이스가 반환하는 std::vector<int32_t> 는 컨트롤러가 반환한 원본 정수 배열이며, 코일 값도 컨트롤러 결과대로 표시합니다.
  2. 쓰기 인터페이스는 주소 범위와 개수를 검증합니다. values 내용의 업무 의미는 컨트롤러가 처리합니다.
  3. SetParam() / GetParam() 은 baud rate, port 같은 업무 파라미터를 컨트롤러로 전달해 유효성을 판단합니다.

상세 의미

GetSlave

시그니처

cpp
std::pair<modbus_client::Slave, STATUS_CODE> GetSlave(
    modbus_client::MODBUS_CHANNEL channel,
    int32_t slaveId,
    int32_t masterId
);
입력타입설명
channelmodbus_client::MODBUS_CHANNEL채널 enum
slaveIdint32_t슬레이브 ID, 생성 시 세션에 바로 기록
masterIdint32_t마스터 ID, 이후 읽기/쓰기 요청에 그대로 포함
출력설명
modbus_client::Slave성공 시 사용 가능한 세션 반환, 실패 시 기본 생성된 빈 세션 반환
STATUS_CODEOK / NOT_CONNECTED

제약 및 동작

  • GetSlave() 는 로컬 세션을 생성합니다. 원격 슬레이브 상태는 이후 읽기/쓰기 결과로 확인합니다.
  • slaveId / masterId 는 컨트롤러 의미대로 전달합니다.
  • 성공적으로 반환된 Slave 는 현재 Arm 수명 주기에 의존하므로 Disconnect() 이후 재사용할 수 없습니다.

호출 예제

cpp
auto [slave, ret] = arm.modbusClient.GetSlave(  // Modbus 슬레이브 세션 생성
    modbus_client::MODBUS_CHANNEL::CONTROLLER_485,  // 컨트롤러 485 채널 지정
    7,  // 슬레이브 ID 를 7 로 지정
    0  // masterId 를 0 으로 지정
);  // 슬레이브 세션 생성 호출 종료
if (ret != STATUS_CODE::OK || !slave.IsValid()) {  // 세션 생성 성공 및 계속 유효한지 확인
    return;  // 세션을 사용할 수 없으면 현재 흐름 종료
}  // 세션 판단 종료

GetParam

시그니처

cpp
std::pair<modbus_client::SerialParams, STATUS_CODE> GetParam(
    modbus_client::MODBUS_CHANNEL channel,
    int32_t masterId = 1
);
입력타입설명
channelmodbus_client::MODBUS_CHANNEL조회할 채널
masterIdint32_tCONTROLLER_TCP_TO_485 채널에서만 실제 의미가 있으며 기본값은 1
출력설명
modbus_client::SerialParams성공 시 파라미터 반환, 실패 시 기본 생성값 반환
STATUS_CODEOK / NOT_CONNECTED / 기타 컨트롤러 오류 코드

제약 및 동작

  • CONTROLLER_TCP_TO_485 채널은 컨트롤러에 masterId 를 보냅니다.
  • 다른 시리얼 채널은 컨트롤러에 채널 enum 값을 보내며 masterId 는 무시됩니다.
  • 이는 상대 프로토콜의 기존 기준이며 SDK 가 새로 정의한 규칙이 아닙니다.

호출 예제

cpp
auto [params, ret] = arm.modbusClient.GetParam(  // Modbus 시리얼 파라미터 조회
    modbus_client::MODBUS_CHANNEL::CONTROLLER_TCP_TO_485,  // TCP-to-485 채널 지정
    12  // masterId 를 12 로 지정
);  // 시리얼 파라미터 조회 호출 종료
if (ret == STATUS_CODE::OK) {  // 파라미터 조회 성공 여부 확인
    std::cout << "ip=" << params.ip << "\n";  // 조회된 대상 IP 출력
}  // 파라미터 조회 판단 종료

SetParam

시그니처

cpp
std::pair<int32_t, STATUS_CODE> SetParam(const modbus_client::SerialParams& params);
입력타입설명
paramsmodbus_client::SerialParams시리얼 파라미터 객체
출력설명
int32_t성공 시 컨트롤러가 확인한 파라미터 레코드 id 반환, 실패 시 0 반환
STATUS_CODEOK / NOT_CONNECTED / 기타 컨트롤러 오류 코드

제약 및 동작

  • channel , ip , port , baud , dataBit , stopBit , parity , timeout 은 모두 직렬화되어 컨트롤러로 직접 전송됩니다.

호출 예제

cpp
modbus_client::SerialParams params;  // 시리얼 파라미터 객체 생성
params.channel = modbus_client::MODBUS_CHANNEL::CONTROLLER_485;  // 컨트롤러 485 채널 지정
params.ip = "10.27.1.99";  // 대상 IP 설정
params.port = 1502;  // 대상 포트 설정
auto [id, ret] = arm.modbusClient.SetParam(params);  // 시리얼 파라미터 저장, 컨트롤러 설정 변경
if (ret == STATUS_CODE::OK) {  // 파라미터 저장 성공 여부 확인
    std::cout << "saved_param_id=" << id << "\n";  // 컨트롤러가 확인한 파라미터 레코드 ID 출력
}  // 파라미터 저장 판단 종료

ReadCoils / ReadHoldingRegs / ReadDiscreteInputs / ReadInputRegs

이 네 인터페이스는 동일한 로컬 검증 규칙과 반환 기준을 공유합니다.

시그니처 패턴

cpp
std::pair<std::vector<int32_t>, STATUS_CODE> ReadXxx(
    int32_t addr,
    int32_t num
);
입력설명
addr시작 주소, addr >= 0 필요
num읽기 개수, 1 <= num <= 120 필요
출력설명
std::vector<int32_t>성공 시 정수 배열 반환, 실패 시 빈 배열 반환
STATUS_CODEOK / INVALID_SESSION / INVALID_PARAMETER / 기타 컨트롤러 오류 코드

제약 및 동작

  • addr + num - 165535 를 초과할 수 없습니다.
  • Slave 가 무효이면 바로 INVALID_SESSION 을 반환합니다.
  • 컨트롤러가 정수 배열이 아닌 값을 반환하면 SDK 는 OTHER_ERR 를 반환합니다.

호출 예제

cpp
auto [coils, ret] = slave.ReadCoils(10, 3);  // 주소 10 부터 코일 값 3개 읽기
if (ret == STATUS_CODE::OK) {  // 코일 읽기 성공 여부 확인
    std::cout << "coils=" << coils.size() << "\n";  // 읽은 코일 개수 출력
}  // 코일 읽기 판단 종료

WriteCoils / WriteHoldingRegs / WriteInputRegs

이 쓰기 인터페이스들은 대상 Modbus 영역만 다르고 나머지 규칙은 동일합니다.

시그니처 패턴

cpp
STATUS_CODE WriteXxx(int32_t addr, const std::vector<int32_t>& values);
입력설명
addr시작 주소, addr >= 0 필요
values쓸 값 목록, 개수는 1..1024 필요
출력설명
STATUS_CODEOK / INVALID_SESSION / INVALID_PARAMETER / 기타 컨트롤러 오류 코드

제약 및 동작

  • addr + values.size() - 165535 를 초과할 수 없습니다.
  • SDK 는 주소와 개수를 검증합니다. 각 요소의 업무 의미는 컨트롤러가 처리합니다.

호출 예제

cpp
STATUS_CODE ret = slave.WriteHoldingRegs(20, {11, 12});  // 주소 20 부터 holding register 값 2개 쓰기
if (ret != STATUS_CODE::OK) {  // holding register 쓰기 실패 여부 확인
    return;  // 쓰기 실패 시 현재 흐름 종료
}  // holding register 쓰기 판단 종료
// STATUS_CODE inputRet = slave.WriteInputRegs(20, {11, 12});  // input register 쓰기는 슬레이브 상태를 변경하므로 확인 후 실행

Slave::GetParam / Slave::SetParam

이 두 인터페이스는 ModbusClient::GetParam / SetParam 의미를 재사용하며, 차이는 세션 컨텍스트가 현재 Slave 에서 온다는 점입니다:

  • Slave::GetParam() 은 세션 생성 시 기록된 channel / masterId 를 사용합니다.
  • Slave::SetParam(params) 는 현재 유효한 세션을 통해 요청을 보냅니다.

Slave 가 이미 무효이면 두 인터페이스 모두 INVALID_SESSION 을 반환합니다.

SerialSend / SerialReceive

시그니처

cpp
STATUS_CODE SerialSend(const std::string& msg);
std::pair<std::string, STATUS_CODE> SerialReceive();
인터페이스입력출력주요 동작
SerialSend원본 문자열STATUS_CODE현재 channel 기준으로 전송 요청, slaveId / masterId 미포함
SerialReceive없음std::pair<std::string, STATUS_CODE>성공 시 원본 문자열 반환, 실패 시 빈 문자열 반환

제약 및 동작

  • Passthrough 프로토콜은 채널 기준으로 동작하므로 Slave 에 저장된 slaveId 는 시리얼 passthrough 에 영향을 주지 않습니다.
  • SerialReceive() 성공 시 컨트롤러가 반환한 data 는 문자열이어야 하며, 그렇지 않으면 OTHER_ERR 를 반환합니다.

일반 타입

modbus_client::MODBUS_CHANNEL

enum 값설명
CONTROLLER_TCP_TO_4852컨트롤러 TCP-to-485 채널
WRIST_485_03손목 485_0 채널
WRIST_485_14손목 485_1 채널
CONTROLLER_4855컨트롤러 485 채널

modbus_client::MODBUS_PARITY

enum 값설명
NONE78ASCII N
ODD79ASCII O
EVEN69ASCII E

modbus_client::SerialParams

필드타입기본값설명
idint32_t1파라미터 레코드 ID
channelmodbus_client::MODBUS_CHANNELCONTROLLER_TCP_TO_485채널
ipstd::string빈 문자열대상 IP
portint32_t502포트
baudint32_t9600baud rate
dataBitint32_t8data bit
stopBitint32_t1stop bit
paritymodbus_client::MODBUS_PARITYNONEparity
timeoutint32_t1000timeout, 밀리초

예제

C++17 최소 예제

cpp
#include <iostream>  // Modbus 조회 결과를 출력하기 위한 표준 출력 스트림 포함
#include "arm_api.h"  // Arm 메인 진입점을 포함하며, 연결 후 arm.modbusClient 로 Modbus 인터페이스에 접근
int main()  // 예제 프로그램 진입점
{  // 예제 main 함수 시작
    Arm arm;  // 로봇 세션 객체 생성
    STATUS_CODE connectRet = arm.Connect("10.27.1.254", "");  // 컨트롤러 또는 라우터 주소에 연결, 티치 펜던트 주소를 비워 기본 규칙 사용
    if (connectRet != STATUS_CODE::OK) {  // 연결 실패 여부 확인
        return 1;  // 연결 실패 시 바로 종료
    }  // 연결 결과 판단 종료
    auto [params, getRet] = arm.modbusClient.GetParam(  // 지정한 Modbus 채널의 시리얼 파라미터 조회
        modbus_client::MODBUS_CHANNEL::CONTROLLER_485,  // 컨트롤러 485 채널 지정
        0  // masterId 지정, TCP_TO_485 가 아닌 채널에서는 일반적으로 컨트롤러가 무시
    );  // 시리얼 파라미터 조회 호출 종료
    if (getRet == STATUS_CODE::OK) {  // 조회 성공 시에만 시리얼 파라미터 출력
        std::cout << "modbus_ip=" << params.ip << "\n";  // 시리얼 파라미터의 대상 IP 출력
    }  // 시리얼 파라미터 조회 판단 종료
    auto [slave, slaveRet] = arm.modbusClient.GetSlave(  // Modbus 슬레이브 세션 하나 생성
        modbus_client::MODBUS_CHANNEL::CONTROLLER_485,  // 컨트롤러 485 채널 지정
        1,  // 슬레이브 ID 를 1 로 지정
        0  // masterId 를 0 으로 지정
    );  // 슬레이브 세션 생성 호출 종료
    if (slaveRet != STATUS_CODE::OK || !slave.IsValid()) {  // 슬레이브 세션 생성 성공 및 유효 여부 확인
        return 1;  // 세션이 무효이면 오류 코드 반환
    }  // 슬레이브 세션 판단 종료
    auto [coils, readRet] = slave.ReadCoils(0, 2);  // 주소 0 부터 코일 값 2개 읽기
    if (readRet != STATUS_CODE::OK) {  // 코일 읽기 실패 여부 확인
        return 1;  // 읽기 실패 시 오류 코드 반환
    }  // 코일 읽기 판단 종료
    std::cout << "coil_count=" << coils.size() << "\n";  // 읽은 코일 개수 출력
    return 0;  // 예제 정상 종료
}  // 예제 main 함수 종료

C++17 시나리오 예제

아래 여러 코드 조각은 "Modbus 파라미터 인터페이스, 슬레이브 세션, 레지스터 읽기/쓰기, 시리얼 passthrough" 관점에서 이 페이지의 API 를 교차로 다룹니다. 코드 조각은 최소 예제에서 이미 연결에 성공한 arm 객체를 이어받는다고 가정합니다. 레지스터 쓰기, 시리얼 파라미터 변경, 시리얼 passthrough 는 현장 장치에 접근하므로 확인 후 실행하십시오.

시리얼 파라미터 조회 및 설정

cpp
auto [facadeParams, facadeGetRet] = arm.modbusClient.GetParam(  // ModbusClient 인터페이스 모듈로 시리얼 파라미터 조회
    modbus_client::MODBUS_CHANNEL::CONTROLLER_485,  // 컨트롤러 485 채널 지정
    0  // masterId 를 0 으로 지정
);  // 파라미터 조회 호출 종료
modbus_client::SerialParams params = facadeParams;  // SetParam 호출 형태를 보여주기 위해 현재 파라미터 복사
params.channel = modbus_client::MODBUS_CHANNEL::CONTROLLER_485;  // 설정할 채널 지정
// auto [savedId, facadeSetRet] = arm.modbusClient.SetParam(params);  // 시리얼 파라미터 설정은 컨트롤러 설정을 변경하므로 확인 후 실행

슬레이브 세션 생성 및 세션별 파라미터 조회

cpp
auto [slave, slaveRet] = arm.modbusClient.GetSlave(  // ModbusClient 인터페이스 모듈로 슬레이브 세션 생성
    modbus_client::MODBUS_CHANNEL::CONTROLLER_485,  // 컨트롤러 485 채널 지정
    1,  // 슬레이브 ID 를 1 로 지정
    0  // masterId 를 0 으로 지정
);  // 슬레이브 세션 생성 호출 종료
if (slaveRet != STATUS_CODE::OK || !slave.IsValid()) {  // Slave::IsValid 로 로컬 세션 유효 여부 확인
    return;  // 세션이 무효이면 현재 흐름 종료
}  // 슬레이브 세션 판단 종료
auto [slaveParams, slaveGetRet] = slave.GetParam();  // 현재 Slave 세션으로 시리얼 파라미터 조회
// auto [slaveSavedId, slaveSetRet] = slave.SetParam(slaveParams);  // Slave 를 통한 시리얼 파라미터 설정은 컨트롤러 설정을 변경하므로 확인 후 실행

Modbus 데이터 영역 읽기 및 쓰기

cpp
auto [coils, coilsRet] = slave.ReadCoils(0, 2);  // 코일 영역 데이터 읽기
auto [holdingRegs, holdingRet] = slave.ReadHoldingRegs(0, 2);  // holding register 데이터 읽기
auto [discreteInputs, discreteRet] = slave.ReadDiscreteInputs(0, 2);  // discrete input 데이터 읽기
auto [inputRegs, inputRet] = slave.ReadInputRegs(0, 2);  // input register 데이터 읽기
if (coilsRet == STATUS_CODE::OK || holdingRet == STATUS_CODE::OK || discreteRet == STATUS_CODE::OK || inputRet == STATUS_CODE::OK) {  // 읽기 중 하나 이상 성공했는지 확인
    std::cout << "coil_count=" << coils.size() << " holding_count=" << holdingRegs.size() << "\n";  // 읽기 결과 요약 출력
}  // Modbus 읽기 판단 종료
// STATUS_CODE writeCoilsRet = slave.WriteCoils(0, {1, 0});  // 코일 쓰기는 슬레이브 상태를 변경하므로 확인 후 실행
// STATUS_CODE writeHoldingRet = slave.WriteHoldingRegs(0, {100, 200});  // holding register 쓰기는 슬레이브 상태를 변경하므로 확인 후 실행
// STATUS_CODE writeInputRet = slave.WriteInputRegs(0, {100, 200});  // input register 쓰기는 슬레이브 상태를 변경하므로 확인 후 실행

시리얼 passthrough

cpp
// STATUS_CODE serialSendRet = slave.SerialSend("010300000002");  // 시리얼 passthrough 전송은 현장 버스에 접근하므로 확인 후 실행
// auto [serialText, serialReceiveRet] = slave.SerialReceive();  // 시리얼 passthrough 수신은 현장 데이터를 기다리므로 확인 후 실행

예제 코드

cpp17/modbus_basic/src/main.cpp
cpp
#include "query_master_params/run.h"
#include "operate_slave/run.h"
#include "set_master_params/run.h"

int main(void)
{
    // [ZH] 默认只调用一个门面方法;如需体验其他接口,请把下一行替换成下面任意一行。
    // [EN] The main function calls only one facade by default. Replace the next line with any line below to try other APIs.
    return RunModbusBasicQueryMasterParams();
    // return RunModbusBasicOperateSlave();
    // return RunModbusBasicSetMasterParams();
}
c99/modbus_basic/src/main.cpp
cpp
#include <stdio.h>
#include <string.h>

extern "C" {
#include "c_arm_api.h"
}

int main(void)
{
    // [ZH] 本示例直接在源码中写死连接地址,不解析命令行参数。
    // [EN] This example hard-codes the connection addresses in the source code and does not parse command-line arguments.
    // [ZH] 创建并连接 SDK 句柄。
    // [EN] Create the SDK handle and connect to the robot.
    ArmHandle* handle = Arm_Create();
    if (handle == NULL) {
        printf("[c99_modbus] 创建句柄失败 / Failed to create the handle\n");
        return 1;
    }
    int ret = Arm_Connect(handle, "10.27.1.2", "10.27.1.102");
    if (ret != 0) {
        printf("[c99_modbus] 连接失败 / Connect failed, 状态码 / Status code: %d\n", ret);
        Arm_Destroy(handle);
        return 1;
    }
    printf("[c99_modbus] 机器人连接成功 / Robot connected successfully\n");

    // [ZH] 准备主站串口参数并读取当前值。
    // [EN] Prepare master serial params and read the current values.
    ArmSerialParams masterParams = {0};
    int32_t paramId = 0;
    snprintf(masterParams.ip, sizeof(masterParams.ip), "127.0.0.1");
    masterParams.channel = ARM_MODBUS_CHANNEL_CONTROLLER_485;
    masterParams.id = 1;
    masterParams.port = 502;
    masterParams.baud = 9600;
    masterParams.dataBit = 8;
    masterParams.stopBit = 1;
    masterParams.parity = ARM_MODBUS_PARITY_NONE;
    masterParams.timeout = 1000;
    ret = Arm_Modbus_GetParam(handle, ARM_MODBUS_CHANNEL_CONTROLLER_485, 0, &masterParams);
    printf("[c99_modbus] GetParam 状态码 / GetParam status code: %d, channel=%d, baud=%d\n", ret, masterParams.channel, masterParams.baud);
    ret = Arm_Modbus_SetParam(handle, &masterParams, &paramId);
    printf("[c99_modbus] SetParam 状态码 / SetParam status code: %d, id=%d\n", ret, paramId);

    // [ZH] 获取从站句柄并顺序执行全部从站接口。
    // [EN] Acquire the slave handle and execute all slave APIs in sequence.
    ArmModbusSlaveHandle* slave = NULL;
    ret = Arm_Modbus_GetSlave(handle, ARM_MODBUS_CHANNEL_CONTROLLER_485, 1, 0, &slave);
    printf("[c99_modbus] GetSlave 状态码 / GetSlave status code: %d\n", ret);
    if (slave != NULL) {
        int valid = Arm_ModbusSlave_IsValid(slave);
        printf("[c99_modbus] Slave_IsValid / Slave_IsValid: %d\n", valid);
        int32_t values[8] = {0};
        size_t valueCount = 0U;
        ret = Arm_ModbusSlave_ReadCoils(slave, 0, 2, values, 8U, &valueCount);
        printf("[c99_modbus] ReadCoils 状态码 / ReadCoils status code: %d, count=%zu\n", ret, valueCount);
        int32_t writeCoils[2] = {1, 0};
        ret = Arm_ModbusSlave_WriteCoils(slave, 0, writeCoils, 2U);
        printf("[c99_modbus] WriteCoils 状态码 / WriteCoils status code: %d\n", ret);
        ret = Arm_ModbusSlave_ReadHoldingRegs(slave, 0, 2, values, 8U, &valueCount);
        printf("[c99_modbus] ReadHoldingRegs 状态码 / ReadHoldingRegs status code: %d, count=%zu\n", ret, valueCount);
        int32_t holdingRegs[2] = {1, 2};
        ret = Arm_ModbusSlave_WriteHoldingRegs(slave, 0, holdingRegs, 2U);
        printf("[c99_modbus] WriteHoldingRegs 状态码 / WriteHoldingRegs status code: %d\n", ret);
        ret = Arm_ModbusSlave_ReadDiscreteInputs(slave, 0, 2, values, 8U, &valueCount);
        printf("[c99_modbus] ReadDiscreteInputs 状态码 / ReadDiscreteInputs status code: %d, count=%zu\n", ret, valueCount);
        ret = Arm_ModbusSlave_ReadInputRegs(slave, 0, 2, values, 8U, &valueCount);
        printf("[c99_modbus] ReadInputRegs 状态码 / ReadInputRegs status code: %d, count=%zu\n", ret, valueCount);
        int32_t inputRegs[2] = {3, 4};
        ret = Arm_ModbusSlave_WriteInputRegs(slave, 0, inputRegs, 2U);
        printf("[c99_modbus] WriteInputRegs 状态码 / WriteInputRegs status code: %d\n", ret);
        ret = Arm_ModbusSlave_SetParam(slave, &masterParams, &paramId);
        printf("[c99_modbus] Slave_SetParam 状态码 / Slave_SetParam status code: %d, id=%d\n", ret, paramId);
        ret = Arm_ModbusSlave_GetParam(slave, &masterParams);
        printf("[c99_modbus] Slave_GetParam 状态码 / Slave_GetParam status code: %d, baud=%d\n", ret, masterParams.baud);
        ret = Arm_ModbusSlave_SerialSend(slave, "hello from c99 example");
        printf("[c99_modbus] SerialSend 状态码 / SerialSend status code: %d\n", ret);
        char recvBuf[256] = {0};
        ret = Arm_ModbusSlave_SerialReceive(slave, recvBuf, sizeof(recvBuf));
        printf("[c99_modbus] SerialReceive 状态码 / SerialReceive status code: %d, 内容 / Text: %s\n", ret, recvBuf);
        Arm_ModbusSlave_Destroy(slave);
        printf("[c99_modbus] 从站句柄已释放 / Slave handle destroyed\n");
    }

    // [ZH] 断开连接并销毁句柄。
    // [EN] Disconnect and destroy the handle.
    Arm_Disconnect(handle);
    Arm_Destroy(handle);
    printf("[c99_modbus] 示例结束 / Example finished\n");
    return 0;
}