步骤

新建并编译

复制之前的文件,或者新建,名字为:
WsfSimpleSocketProcessor.cpp
WsfSimpleSocketProcessor.hpp
加到WsfProcessorTypes.cpp里:

#include "WsfSimpleSocketProcessor.hpp"
AddCoreType("MY_SIMPLE_SOCKET_PROCESSOR", ut::make_unique<WsfSimpleSocketProcessor>(aScenario));

重新编译:

rm -rf buil
mkdir -p buil
cmake -S src -B buil
cmake --build buil --target wsf_util -j10
cmake --build buil --target wsf -j12
cmake --build buil --target wsf_parser -j12
cmake --build buil --target mission -j12


python文件逻辑

python文件用于强化学习训练,主函数在python文件,通过他启动afsim的进程,进行训练。
强化学环境习有三个主要函数:init,reset,step。

init

由于是示例,非真正强化学习训练,所以这部分没有定义什么,如果是真正强化学习训练,需要定义状态空间和动作空间。

reset

重置环境,开始一个新的 episode。

    reset 的流程:
    1. 关闭旧 episode;
    2. 启动 Python socket 服务端;
    3. 启动 AFSIM 进程;
    4. 等待 AFSIM 连接;
    5. 等待 AFSIM 发来第一帧 OBS;
    6. 返回初始 obs。
关闭旧 episode

close()函数。
1、先关闭与 AFSIM 的 socket 连接;
如果self.conn非空,说明连接还在,需要关闭。

		if self.conn is not None:
            try:
                self.conn.close()
            except Exception:
                pass
            self.conn = None

2、关闭 Python socket 服务端
判断self.server_sock

       # 关闭 Python socket 服务端。
        if self.server_sock is not None:
            try:
                self.server_sock.close()
            except Exception:
                pass
            self.server_sock = None

3、关闭AFSIM 进程;
如果self.proc还在,则先终止进程,再杀死进程,稍作等待。

       # 关闭 AFSIM 进程。
        if self.proc is not None:
            if self.proc.poll() is None:
                self.proc.terminate()
                try:
                    self.proc.wait(timeout=2.0)
                except subprocess.TimeoutExpired:
                    self.proc.kill()
                    self.proc.wait()
            self.proc = None

4、关闭日志

        # 关闭日志文件。
        if self.log_fp is not None:
            try:
                self.log_fp.close()
            except Exception:
                pass
            self.log_fp = None
启动 Python socket 服务端;

_start_server()#
1、创建 TCP socket
AF_INET 表示 IPv4,SOCK_STREAM 表示 TCP
self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2、设置端口复用
正常情况下,如果程序刚刚退出,或者异常中断,端口 6000 可能不会立刻释放。操作系统会让这个端口短时间处于一种等待状态,比如 TIME_WAIT。
setsockopt设置 socket 选项
socket.SO_REUSEADDR:允许复用本地地址和端口
self.server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
3、绑定 IP 和端口
self.server_sock.bind((HOST, PORT))
4、开始监听连接
self.server_sock.listen(1)

启动 AFSIM 进程;
def _start_afsim_process(self):
        """
        启动 AFSIM 进程。

        Python 通过 subprocess.Popen 启动 AFSIM。
        这相当于在终端中执行:

            cd /home/ubuntu/afsim_ws/afsim-src/afsim/swdev
            ./buil/mission -sm /home/ubuntu/afsim_ws/project1/13-rl-platform/two_platform_logger.txt

        每个 episode 会单独生成一个日志文件。
        """
        # 每个 episode 单独保存一个日志文件。
        log_path = os.path.join(BASE_DIR, f"afsim_episode_{self.episode_id}.log")
        # 打开日志文件。
        # AFSIM 的标准输出和错误输出都会写到这里。
        self.log_fp = open(log_path, "w", encoding="utf-8")

        # 启动 AFSIM 进程。
        self.proc = subprocess.Popen(
            [AFSIM_EXE, "-sm", MISSION_FILE],
            cwd=AFSIM_WORKDIR,# 指定 AFSIM 的运行目录。
            stdout=self.log_fp,# 把 AFSIM 的标准输出写到日志文件。
            stderr=subprocess.STDOUT,# 把错误输出也合并写到日志文件。
            text=True# 文本模式。
        )

        print(f"[Python] started AFSIM episode {self.episode_id}")
等待 AFSIM 连接 Python。
def _accept_connection(self):
        """
        等待 AFSIM 连接 Python socket 服务端。

        AFSIM 侧启动后,应该主动连接:
            127.0.0.1:6000

        如果 30 秒内没有连接成功,就会抛出超时异常。
        """
        # 设置 accept 等待超时时间。
        self.server_sock.settimeout(30.0)
        # 阻塞等待 AFSIM 连接。
        # 成功后返回:
        # self.conn:与 AFSIM 通信的 socket
        # addr:AFSIM 客户端地址
        self.conn, addr = self.server_sock.accept()

        print(f"[Python] AFSIM connected from {addr}")
等待 AFSIM 发来第一帧 OBS;
    def _wait_obs(self):
        """
        等待并读取一帧新的 observation。

        流程:
        1. 从 socket 中读一行;
        2. 解析成 obs 字典;
        3. 更新 current_obs 和 current_step_id;
        4. 返回 obs。
        """
        # 从 socket 中读取一条消息。
        line = self._read_line(timeout=30.0)
        # 把消息解析成 observation。
        obs = self._parse_obs(line)

        self.current_obs = obs# 保存当前 observation。
        self.current_step_id = obs["step_id"]# 保存当前 step_id。

        return obs

step

执行一个动作,并等待 AFSIM 返回下一帧 observation。

    step 的流程:
    1. 检查 socket 是否已经建立;
    2. 对 action 做限幅;
    3. 发送 ACT 消息给 AFSIM;
    4. 等待 AFSIM 推进一步后返回 OBS;
    5. 计算 reward 和 done;
    6. 返回 obs, reward, done, info。
检查 socket 是否已经建立
        if self.conn is None:# 如果还没有 reset,就不能 step。
            raise RuntimeError("env is not reset")
对 action 做限幅
        heading_delta_deg = float(action)# 把 action 转成 float。
        heading_delta_deg = clamp(# 限制动作范围,避免一次转向过大。
            heading_delta_deg,
            -MAX_HEADING_DELTA_DEG,
            MAX_HEADING_DELTA_DEG
        )

        step_id = self.current_step_id# 当前动作对应的 step_id。
发送 ACT 消息给 AFSIM

self._send_line(f"ACT {step_id} {heading_delta_deg}")

    def _send_line(self, line):
        """
        向 AFSIM 发送一行文本。

        当前协议要求每条消息以 \\n 结尾。
        如果传入的 line 没有换行符,这里会自动补上。
        """
        if not line.endswith("\n"):# 确保消息以换行符结尾。
            line += "\n"
        # sendall 会尽量把整条消息完整发送出去。
        self.conn.sendall(line.encode("utf-8"))
等待 AFSIM 推进一步后返回 OBS
        # 等待 AFSIM 推进一步后发下一帧 OBS
        try:
            obs = self._wait_obs()
        except Exception as e:
            # AFSIM 自然结束或 socket 关闭
            return None, 0.0, True, {"reason": str(e)}
    def _wait_obs(self):
        """
        等待并读取一帧新的 observation。

        流程:
        1. 从 socket 中读一行;
        2. 解析成 obs 字典;
        3. 更新 current_obs 和 current_step_id;
        4. 返回 obs。
        """
        # 从 socket 中读取一条消息。
        line = self._read_line(timeout=30.0)
        # 把消息解析成 observation。
        obs = self._parse_obs(line)

        self.current_obs = obs# 保存当前 observation。
        self.current_step_id = obs["step_id"]# 保存当前 step_id。

        return obs
    def _read_line(self, timeout=30.0):
         """
        从 socket 中读取一行文本。

        当前协议约定:
        每条消息以换行符 \\n 结尾。

        所以这里不断 recv(1),每次读取 1 个字节,
        直到读到换行符为止。

        返回值:
            去掉换行符后的字符串。
        """
        # 设置读取超时时间。
        self.conn.settimeout(timeout)
        # 用于保存接收到的字节。
        chunks = []

        while True:
            # 每次读取 1 个字节。
            ch = self.conn.recv(1)
            # 如果 recv 返回空,说明对端 socket 已关闭。
            if not ch:
                raise RuntimeError("socket closed by AFSIM")
            # 如果读到换行符,说明一条消息结束。
            if ch == b"\n":
                break
            # 否则继续累积数据。
            chunks.append(ch)
        # 把字节拼接成完整字符串,并去掉两端空白。
        return b"".join(chunks).decode("utf-8").strip()
计算 reward 和 done

自行计算

返回 obs, reward, done, info

自行计算

afsim文件逻辑

.hpp

原来的必须有的函数不变。
加上函数:

   std::string mSocketHost;          // Python server IP,默认 127.0.0.1
   int mSocketPort;                  // Python server 端口,默认 5000
   int mSocketFd;                    // socket 文件描述符
   bool mSocketConnected;            // 是否已连接 Python

   double mMaxHeadingDeltaDeg;       // 限制动作范围,例如 [-10, 10]
   double mSocketTimeoutSec;         // 等待 Python 动作超时时间
   int mRlStepIndex;                 // 当前 RL 步号

   // 连接 Python socket server
   bool ConnectToPython();

   // 发送一行字符串,自动带 \n
   bool SendLineToPython(const std::string& aLine);

   // 接收一行字符串,直到读到 \n
   bool ReceiveLineFromPython(std::string& aLine, double aTimeoutSec);

   // 发送 observation,接收 action
   bool RequestActionFromPython(
      int aStepId,
      double aSimTime,
      const RelativeState& aState,
      double& aHeadingDeltaDeg);

.cpp

afsim的主要步骤还是:ProcessInput和Update

ProcessInput

读取txt需要的参数,和之前一样。

Update

1、执行父类Update。
2、获取所属平台
3、获取目标平台
4、获得运动的对象mover,转成 WsfWaypointMover
5、Socket RL:发送 observation,等待 Python 返回 action
6、控制飞机转向,waypointMover->TurnToHeading
7、记录和打印。
这里边,新内容是5。

Socket RL:发送 observation,等待 Python 返回 action
    // ==========================================================
    // Socket RL:发送 observation,等待 Python 返回 action
    // ==========================================================
    // 当前 RL 步号
    int stepId = mRlStepIndex;
    double headingDeltaDeg = 0.0;
    bool gotAction = RequestActionFromPython(
                            stepId,
                            aSimTime,
                            state,
                            headingDeltaDeg);
    if (!gotAction)
    {
        std::cout << "[RL] Timeout waiting action for step "
                << stepId
                << ", use headingDeltaDeg = 0"
                << std::endl;

        headingDeltaDeg = 0.0;
    }

RequestActionFromPython函数:
1、判断是否与python连接
2、构造 observation 消息,并发送。
3、接收python消息。
4、解析,简单校验

判断是否与python连接
    if (!ConnectToPython())
    {
        std::cout << "[Socket] Cannot connect to Python, use action = 0."
                  << std::endl;
        return false;
    }
// 连接 Python函数,AFSim / C++ 这一侧作为 socket 客户端,主动去连接 Python 启动的 socket 服务端
bool WsfSimpleSocketProcessor::ConnectToPython()
{
    // ------------------------------------------------------------
    // 1. 如果已经连接过 Python,就不重复连接
    // ------------------------------------------------------------

    // mSocketConnected 表示逻辑上是否已经连接
    // mSocketFd >= 0 表示 socket 文件描述符有效
    //
    // 两个条件都满足,说明当前已经有可用连接,
    // 直接返回 true 即可。
    if (mSocketConnected && mSocketFd >= 0)
    {
        return true;
    }
    // 在 Linux 里,socket 本质上也被当成一种“文件描述符”。// ------------------------------------------------------------
    // 2. 创建 socket
    // ------------------------------------------------------------

    // 在 Linux 里,socket 本质上也被当成一种“文件描述符”。
    //
    // AF_INET     :使用 IPv4
    // SOCK_STREAM :使用 TCP
    // 0           :使用默认协议,对于 SOCK_STREAM 来说就是 TCP
    //
    // 创建成功后,返回一个非负整数,例如 3、4、5。
    // 创建失败时,返回 -1。
    mSocketFd = ::socket(AF_INET, SOCK_STREAM, 0);

    if (mSocketFd < 0)// 如果 socket 创建失败,打印错误原因并返回 false。
    {
        std::cout << "[Socket] socket() failed: "
                  << std::strerror(errno) << std::endl;
        return false;
    }
    // ------------------------------------------------------------
    // 3. 准备 Python 服务端地址结构
    // ------------------------------------------------------------

    // sockaddr_in 是 IPv4 地址结构体。
    // 后面 connect() 需要用这个结构体指定:
    // 连接哪个 IP、哪个端口。
    sockaddr_in serverAddr;
    // 把 serverAddr 整体清零,避免里面有随机脏数据。
    std::memset(&serverAddr, 0, sizeof(serverAddr));
    // 指定地址族为 IPv4。
    serverAddr.sin_family = AF_INET;
    // 设置 Python 服务端端口。
    //
    // mSocketPort 是普通整数,例如 6000。
    // htons() 的作用是把主机字节序转换成网络字节序。
    //
    // 网络通信中,端口号必须使用网络字节序。
    serverAddr.sin_port = htons(static_cast<uint16_t>(mSocketPort));
    // ------------------------------------------------------------
    // 4. 把字符串 IP 转换成网络地址格式
    // ------------------------------------------------------------

    // mSocketHost 一般是 "127.0.0.1"。
    //
    // inet_pton 的作用:
    // 把字符串形式的 IP 地址转换成二进制网络地址。
    //
    // 例如:
    // "127.0.0.1"
    // 转成 serverAddr.sin_addr 里面需要的二进制格式。
    //
    // 参数含义:
    // AF_INET                    :IPv4
    // mSocketHost.c_str()         :字符串 IP
    // &serverAddr.sin_addr        :转换后的结果存放位置
    int ok = ::inet_pton(AF_INET, mSocketHost.c_str(), &serverAddr.sin_addr);
    // inet_pton 返回值:
    // 1  :转换成功
    // 0  :IP 字符串格式不对
    // -1 :系统错误
    //
    // 所以 ok <= 0 表示转换失败。
    if (ok <= 0)
    {
        std::cout << "[Socket] invalid socket_host: "
                  << mSocketHost << std::endl;
        // 创建 socket 成功了,但 IP 转换失败,
        // 所以这里要关闭 socket,避免文件描述符泄漏。
        ::close(mSocketFd);
        mSocketFd = -1;
        return false;
    }
    // ------------------------------------------------------------
    // 5. 连接 Python 服务端
    // ------------------------------------------------------------

    // connect() 用于客户端主动连接服务端。
    //
    // 当前 C++ / AFSim 是客户端。
    // Python 是服务端。
    //
    // connect 成功后,mSocketFd 就可以用来 send / recv。
    //
    // 参数说明:
    // mSocketFd:
    //     前面 socket() 创建出来的 socket 文件描述符。
    //
    // reinterpret_cast<sockaddr*>(&serverAddr):
    //     connect() 需要的是通用 sockaddr* 类型,
    //     但我们这里使用的是 IPv4 专用 sockaddr_in,
    //     所以需要强制转换。
    //
    // sizeof(serverAddr):
    //     地址结构体大小。
    if (::connect(mSocketFd,
                  reinterpret_cast<sockaddr*>(&serverAddr),
                  sizeof(serverAddr)) < 0)
    {
        // connect 返回负数,说明连接失败。
        //
        // 常见原因:
        // 1. Python 服务端没有启动;
        // 2. Python 监听端口和这里的 mSocketPort 不一致;
        // 3. IP 地址写错;
        // 4. 端口被防火墙或系统限制;
        // 5. Python 服务端已经退出;
        // 6. Python 服务端还没执行到 listen()。
        std::cout << "[Socket] connect() failed to "
                  << mSocketHost << ":"
                  << mSocketPort
                  << " error="
                  << std::strerror(errno)
                  << std::endl;
        // 连接失败后,要关闭 socket。
        // 否则这个 socket 文件描述符会一直占用系统资源。
        ::close(mSocketFd);
        mSocketFd = -1;// 重置 socket 文件描述符。
        return false;
    }
    // ------------------------------------------------------------
    // 6. 连接成功,更新状态
    // ------------------------------------------------------------

    // 标记当前 socket 已经成功连接 Python。
    mSocketConnected = true;

    std::cout << "[Socket] Connected to Python server "
              << mSocketHost << ":"
              << mSocketPort
              << std::endl;

    return true;
}
构造 observation 消息,并发送
    std::ostringstream obs;
    obs << "OBS "
        << aStepId << " "
        << aSimTime << " "
        << aState.range << " "
        << aState.relativeBearingDeg << " "
        << aState.elevationDeg << " "
        << aState.closingSpeed << " "
        << aState.courseDeg;

    // ------------------------------------------------------------
    // 4. 发送 OBS 给 Python
    // ------------------------------------------------------------

    // SendLineToPython() 会把字符串发送给 Python。
    //
    // 注意:
    // SendLineToPython() 内部通常会自动补 '\n'。
    // 因为 Python 侧是按行读取的。
    //
    // 实际发送出去大概是:
    //
    //   OBS 12 60.0 180000.0 -3.2 0.0 450.0 90.0\n
    if (!SendLineToPython(obs.str()))
    {
        std::cout << "[Socket] Failed to send OBS, use action = 0."
                  << std::endl;
        return false;
    }

// 发送一行文本给 Python
// 参数:
//   aLine:要发送的字符串,例如:
//          "OBS 1 5.0 200000.0 -2.5 0.0 450.0 90.0"
//
// 返回值:
//   true  :发送成功
//   false :发送失败
bool WsfSimpleSocketProcessor::SendLineToPython(const std::string& aLine)
{
    // ------------------------------------------------------------
    // 1. 检查 socket 是否已经连接
    // ------------------------------------------------------------

    // mSocketConnected 表示逻辑上是否已经连接 Python
    // mSocketFd 是 socket 文件描述符
    //
    // 如果还没连接,或者 socket 文件描述符无效,
    // 那么不能发送数据,直接返回 false。
    if (!mSocketConnected || mSocketFd < 0)
    {
        return false;
    }
    // ------------------------------------------------------------
    // 2. 准备要发送的消息
    // ------------------------------------------------------------

    // 复制一份字符串,避免直接修改传入参数 aLine。
    std::string msg = aLine;

    // 当前通信协议约定:
    // 每条消息以换行符 '\n' 作为结束标志。
    //
    // Python 侧的 _read_line() 会一直读取 socket,
    // 直到读到 '\n' 才认为一条消息结束。
    //
    // 所以这里要确保 msg 最后一定有 '\n'。
    if (msg.empty() || msg[msg.size() - 1] != '\n')
    {
        msg += "\n";
    }

    // ------------------------------------------------------------
    // 3. 获取底层字符数据指针
    // ------------------------------------------------------------

    // c_str() 返回 C 风格字符串指针。
    // send() 是 C 系统调用,需要 const char* 类型的数据指针。
    const char* data = msg.c_str();
    size_t totalSent = 0;// totalSent 表示当前已经成功发送了多少字节。
    size_t totalSize = msg.size();// totalSize 表示这条消息总共有多少字节需要发送。

    // ------------------------------------------------------------
    // 4. 循环发送,直到整条消息全部发完
    // ------------------------------------------------------------

    // 注意:
    // send() 不保证一次就把所有数据发送完。
    //
    // 例如 msg 有 100 个字节,
    // send() 可能一次只发送出去 40 个字节。
    //
    // 所以这里要用 while 循环,
    // 不断发送剩余部分,直到 totalSent == totalSize。
    while (totalSent < totalSize)
    {
        // 发送剩余数据。
        //
        // 参数说明:
        //   mSocketFd:
        //      已经连接到 Python 的 socket 文件描述符。
        //
        //   data + totalSent:
        //      从尚未发送的位置开始发送。
        //
        //   totalSize - totalSent:
        //      剩余还没有发送的字节数。
        //
        //   0:
        //      默认发送方式,不使用特殊 flag。
        ssize_t n = ::send(
            mSocketFd,
            data + totalSent,
            totalSize - totalSent,
            0);

        // --------------------------------------------------------
        // 5. 判断 send 是否失败
        // --------------------------------------------------------

        // send 返回值:
        //   n > 0 :本次成功发送了 n 个字节
        //   n = 0 :一般表示连接异常或没有发送出数据
        //   n < 0 :发送失败
        //
        // 当前代码把 n <= 0 都认为是发送失败。
        if (n <= 0)
        {
            std::cout << "[Socket] send() failed: "
                      << std::strerror(errno)
                      << std::endl;
            // 标记 socket 已经不可用。
            // 后续逻辑可以根据 mSocketConnected 判断是否需要重连。
            mSocketConnected = false;
            return false;
        }
        // 本次成功发送了 n 个字节,
        // 更新已经发送的总字节数。
        totalSent += static_cast<size_t>(n);
    }

    return true;
}
接收python消息
   // ReceiveLineFromPython() 会等待 Python 发来一行数据。
    //
    // mSocketTimeoutSec 是超时时间。
    //
    // Python 应该返回类似:
    //
    //   ACT 12 5.0
    //
    // 含义是:
    //   ACT             消息类型,表示 action
    //   12              这个动作对应 step_id = 12
    //   5.0             航向调整量 heading_delta_deg
    if (!ReceiveLineFromPython(line, mSocketTimeoutSec))
    {
        std::cout << "[Socket] Failed to receive ACT, use action = 0."
                  << std::endl;
        return false;
    }

    // ------------------------------------------------------------
    // 6. 解析 Python 返回的 ACT 消息
    // ------------------------------------------------------------

    // 约定 Python 返回格式:
    //
    //   ACT step heading_delta_deg
    //
    // 例如:
    //
    //   ACT 12 5.0
    //
    // 这里用 istringstream 按空格拆分字符串。
    std::istringstream iss(line);

    std::string tag;// 消息类型。正常情况下应该是 "ACT"。
    int actionStepId = -1;// Python 返回动作对应的 step_id。
    double actionValue = 0.0;// Python 返回的动作值。

    // 从 line 中解析三个字段。
    //
    // 如果 line = "ACT 12 5.0"
    //
    // 解析后:
    //   tag = "ACT"
    //   actionStepId = 12
    //   actionValue = 5.0
    iss >> tag >> actionStepId >> actionValue;
// 从 Python 接收一行文本
//
// 参数:
//   aLine:输出参数,用来保存从 Python 收到的一整行内容
//          例如收到:ACT 12 5.0
//
//   aTimeoutSec:接收超时时间,单位是秒
//                例如 30.0 表示最多等待 30 秒
//
// 返回值:
//   true  :成功收到一整行
//   false :接收失败、超时、连接断开
bool WsfSimpleSocketProcessor::ReceiveLineFromPython(
    std::string& aLine,
    double aTimeoutSec)
{
    // ------------------------------------------------------------
    // 1. 清空输出字符串
    // ------------------------------------------------------------

    // 每次接收前,先把 aLine 清空。
    // 避免残留上一次接收到的数据。
    aLine.clear();
    // ------------------------------------------------------------
    // 2. 检查 socket 是否已经连接
    // ------------------------------------------------------------

    // mSocketConnected 表示逻辑上是否已经连接 Python。
    // mSocketFd 是 socket 文件描述符。
    //
    // 如果还没连接,或者 socket 文件描述符无效,
    // 就无法接收数据,直接返回 false。
    if (!mSocketConnected || mSocketFd < 0)
    {
        return false;
    }
    // ------------------------------------------------------------
    // 3. 循环接收字符,直到遇到换行符 '\n'
    // ------------------------------------------------------------

    // 当前通信协议规定:
    // Python 发来的每条消息以 '\n' 结尾。
    //
    // 所以这里每次 recv 1 个字符,
    // 一直读到 '\n',才认为一整行接收完成。
    while (true)
    {
        // --------------------------------------------------------
        // 4. 使用 select 等待 socket 可读
        // --------------------------------------------------------

        // fd_set 是 select 使用的文件描述符集合。
        // 可以理解为:
        // “我要监听哪些 socket 是否有数据可读”。
        fd_set readSet;
        FD_ZERO(&readSet);// 清空 readSet。
        // 把当前 socket 文件描述符加入 readSet。
        // 表示接下来要监听 mSocketFd 是否可读。
        FD_SET(mSocketFd, &readSet);

        // --------------------------------------------------------
        // 5. 设置 select 超时时间
        // --------------------------------------------------------

        // timeval 是 select 使用的超时时间结构体。
        timeval timeout;
        // 秒部分。
        // 例如 aTimeoutSec = 30.5,
        // timeout.tv_sec = 30。
        timeout.tv_sec = static_cast<int>(aTimeoutSec);
        // 微秒部分。
        // 例如 aTimeoutSec = 30.5,
        // aTimeoutSec - timeout.tv_sec = 0.5,
        // timeout.tv_usec = 500000。
        timeout.tv_usec = static_cast<int>(
            (aTimeoutSec - timeout.tv_sec) * 1000000.0);

        // --------------------------------------------------------
        // 6. 调用 select 等待数据到来
        // --------------------------------------------------------

        // select 的作用:
        // 在真正 recv 之前,先判断 socket 是否有数据可读。
        //
        // 这样做的好处:
        // 1. 不会一直卡死在 recv;
        // 2. 可以设置超时时间;
        // 3. 如果 Python 长时间不发动作,AFSim 可以检测到超时。
        int ret = ::select(mSocketFd + 1, &readSet, nullptr, nullptr, &timeout);
        //   ret > 0 :有 socket 可读
        //   ret = 0 :超时,没有数据
        //   ret < 0 :select 调用失败
        
        // --------------------------------------------------------
        // 7. select 失败
        // --------------------------------------------------------
        if (ret < 0)
        {
            std::cout << "[Socket] select() failed: "
                      << std::strerror(errno)
                      << std::endl;
            // select 失败后,认为连接不可用了。
            mSocketConnected = false;
            return false;
        }

        if (ret == 0)
        {
            std::cout << "[Socket] receive timeout" << std::endl;
            return false;
        }
         // --------------------------------------------------------
        // 9. socket 可读,开始 recv 一个字符
        // --------------------------------------------------------

        // 用来保存本次接收到的 1 个字符。
        char ch = '\0';
        // 从 socket 中读取 1 个字节。
        //
        // 参数说明:
        //   mSocketFd:
        //      已连接的 socket 文件描述符。
        //
        //   &ch:
        //      接收到的数据存放到 ch 里。
        //
        //   1:
        //      本次只读取 1 个字节。
        //
        //   0:
        //      默认接收方式。
        ssize_t n = ::recv(mSocketFd, &ch, 1, 0);
        // --------------------------------------------------------
        // 10. 判断 recv 是否失败或连接关闭
        // --------------------------------------------------------

        // recv 返回值:
        //   n > 0 :成功读取 n 个字节
        //   n = 0 :对端正常关闭连接
        //   n < 0 :读取失败
        //
        // 当前每次只读 1 个字节,
        // 所以正常情况下 n 应该是 1。
        if (n <= 0)
        {
            std::cout << "[Socket] recv() failed or connection closed."
                      << std::endl;
            mSocketConnected = false;
            return false;
        }
        // --------------------------------------------------------
        // 11. 如果读到换行符,说明一整行结束
        // --------------------------------------------------------

        // Python 发送时会在每条消息后面加 '\n'。
        // 所以 C++ 读到 '\n' 时,就认为一条完整消息接收完成。
        if (ch == '\n')
        {
            return true;
        }
        // --------------------------------------------------------
        // 12. 普通字符加入 aLine
        // --------------------------------------------------------

        // 如果不是换行符,就把这个字符追加到 aLine 后面。
        //
        // 例如 Python 发送:
        //   ACT 12 5.0\n
        //
        // C++ 会依次收到:
        //   A
        //   C
        //   T
        //   空格
        //   1
        //   2
        //   空格
        //   5
        //   .
        //   0
        //
        // 最后 aLine 变成:
        //   "ACT 12 5.0"
        aLine.push_back(ch);
    }
}

编译运行

1、对应修改一下txt,
2、重新编译并运行。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐