news 2026/10/15 3:50:31

实时控制学习1

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
实时控制学习1

1、rclcpp::spin(node)背后可以怎么理解?

简单理解:

spin(node) ↓ 创建/使用 Executor ↓ 把 Node 加进去 ↓ 持续等待 ROS2 事件 ↓ 执行 callback

SingleThreadedExecutor

用一个线程依次执行 callback。

比如 Node 有:

Callback A Callback B Callback C

可能执行:

A ↓ B ↓ C

同一时间只有一个 callback 在跑。


MultiThreadedExecutor

一句话:

允许多个 worker thread 并发执行 callback。

例如:

Callback A Callback B Callback C

可以:

Thread1 → A Thread2 → B Thread3 → C

同时执行。

于是慢 callback 不一定堵死所有 callback。


MutuallyExclusive

同一个 group 里的 callback:

不要同时执行

例如:

Callback A Callback B

都修改:

robot_state_

放进:

MutuallyExclusive

可以避免它们同时跑。


Reentrant

表示:

允许并行执行

甚至某些情况下同一个 callback 的多个实例也可能并发。

所以使用它之前:

代码必须真的线程安全

不能因为名字听起来高级就全用 Reentrant。

消息到DDS ↓ 变成可处理事件 ↓ Executor等待/发现 ↓ 调度callback ↓ callback开始执行

所以:

消息到达时间

和:

callback真正执行时间

之间可能有延迟。

这个延迟和:

Executor 线程数 其他callback CPU负载 Callback Group

都有关系。

Linux Process │ ├── Executor │ ├── Worker Thread 1 │ └── Worker Thread 2 │ ├── Node A │ ├── Subscription callback │ └── Timer callback │ ├── Node B │ ├── Service callback │ └── Subscription callback │ └── 自定义Worker Threads

Executor:

管理Node里的callback执行

而 Linux:

真正调度这些线程

所以:

ROS2 Executor ↓ 线程 ↓ Linux scheduler ↓ CPU

这条链一定要记住。

Node = ROS2功能实体 Process = Linux运行实体 Executor = 调度callback Callback = 真正执行的业务函数

2、Topic

Topic = ROS2 里用于“持续数据流”的发布/订阅通信机制。

它们的特点是:

持续产生 ↓ 持续消费

例如:

动捕 50 Hz ↓ 不断发布人体姿态

这就是 Topic。


Publisher 和 Subscriber 不是直接函数调用

这是非常重要的一点。

很多初学者会脑补成:

Publisher ↓ 直接调用 Subscriber callback

实际上不是。

更接近:

Publisher ↓ rclcpp ↓ RMW ↓ DDS ↓ 网络 / 共享内存 / 本机传输 ↓ DDS ↓ RMW ↓ Subscriber ↓ Executor ↓ Callback

所以中间有很多层。

这也是为什么:

Publisher 存在 Subscriber 也存在

不代表:

消息一定成功进入 callback

RMW

ROS Middleware

可以简单理解:

ROS2 对底层 DDS 的统一接口层。

ROS2 上层:

publisher->publish(msg);

你不用直接操作 DDS。

因为:

rclcpp ↓ RMW ↓ 具体 DDS 实现

帮你做了。

所以 ROS2 可以支持不同中间件实现。

例如你以后可能见到:

Fast DDS Cyclone DDS

但上层代码基本不用完全重写。


DDS

DDS 负责发现、匹配和传输发布/订阅数据。

包括:

谁在发布什么 Topic 谁在订阅什么 Topic 消息类型是什么 QoS 是否兼容 数据怎么送过去

所以:

ROS2 Topic

并不是单纯:

一个 socket

它背后是 DDS 通信系统。

MocapDriverNode ↓ publisher.publish(msg) ↓ rclcpp ↓ RMW ↓ DDS Writer ↓ 传输 ↓ DDS Reader ↓ RMW ↓ rclcpp Subscription ↓ Executor发现callback ready ↓ 执行 gmr callback

所以延迟可能来自:

发布端 DDS 网络 订阅端 Executor callback

不是只来自网络。


QoS 不兼容

这和你之前实际排查过的问题高度相关。

例如:

Publisher reliability = best_effort

Subscriber:

reliability = reliable

如果 QoS 请求/提供关系不兼容:

双方能在 graph 中出现

但:

消息可能匹配失败

于是:

Publisher count=1 Subscriber count=1

仍然可能:

收不到数据

所以:

Graph 连接存在,不代表 QoS 数据通路一定成立。


Topic 不适合所有通信

Topic 适合:

连续数据流

但如果是:

“帮我执行一次 reset”

Topic 也能勉强做。

但更合理可能是:

Service

如果是:

“执行一个需要几秒的轨迹,并持续反馈进度”

更适合:

Action

所以你要慢慢形成:

Topic = streaming Service = request / response Action = long-running task

后面逐个讲。


多机 ROS2 网络还可能受什么影响?

例如:

DDS discovery multicast 防火墙 网卡选择 路由 Docker VPN

都可能影响。

所以多机通信排查要分层:

Linux网络 ↓ DDS发现 ↓ ROS2 graph ↓ QoS ↓ Topic实际数据 ↓ callback

你之前学 Linux 网络,就是为了这一层。


一个完整 Topic 排查流程

假设:

/mocap/human_pose没数据。

第一步:

ros2 node list

确认:

Publisher node 在不在 Subscriber node 在不在

第二步:

ros2 topic list

确认 Topic 在不在。

第三步:

ros2 topic info /mocap/human_pose

看:

type publisher count subscriber count QoS

第四步:

ros2 topic hz /mocap/human_pose

确认:

实际有没有数据 频率多少

第五步:

ros2 topic echo /mocap/human_pose

看内容是否正常。如果 ROS2 层确认没发布数据呢?继续往 Publisher 内部查:

上游设备有数据吗? ↓ socket recv 有数据吗? ↓ parser 成功吗? ↓ queue 有数据吗? ↓ publish()真的执行了吗?

可以用:

ss strace 日志 gdb

所以排查路径:

ROS2 ↓ C++ ↓ Linux

全部打通。


Topic 数据路径里哪些地方可能 copy?

概念上可能涉及:

用户消息对象 ↓ ROS2序列化 ↓ DDS buffer ↓ 网络/共享内存 ↓ 反序列化 ↓ Subscriber消息

所以大型消息:

图像 点云

复制成本可能很高。

这也是为什么 ROS2 有:

intra-process communication loaned messages shared memory transport

等优化方向

所以“Topic没数据”只是表象。

你应该拆成:

有没有生产? ↓ 有没有传输? ↓ 有没有接收? ↓ 有没有执行?

3、Service 是什么?

Service = Client 发一次请求,Server 返回一次响应。

例如:

reset_statistics set_mode enable_motor get_status

都很适合 Service。

基本关系:

Client ↓ request Server ↓ response Client

Topic 和 Service 最大区别

Topic:

Publisher ↓ 持续发送数据 ↓ Subscriber

更像:

广播数据流

Service:

Client ↓ “帮我做一件事 / 告诉我一个结果” ↓ Server ↓ 返回结果

所以:

Topic = streaming Service = request / response

适合:

快速请求 ↓ 快速返回

例如:

reset() get_status() set_mode() reload_config() clear_fault()

这些操作通常:

毫秒级

或者很快完成。Service 不适合:

需要几十秒执行 需要持续进度 需要取消 需要反馈

例如:

机械臂走到目标点 执行一条轨迹 导航到某处 标定过程 长时间动作

这些应该优先考虑:

Action

Service 的 request / response 是自定义接口

例如:

ResetStatistics.srv

可能:

Request --- bool success string message

或者:

SetMode.srv int32 mode --- bool success string message

这就是自定义:

.srv

文件。

中间:

---

上面:

Request

下面:

Response

例如:

int32 mode --- bool success string message

意思:

Client发 mode Server返回 success + message

CLI 怎么调用 Service?

例如:

ros2 service list

看服务。

看类型:

ros2 service type /set_mode

查看接口定义:

ros2 interface show my_interfaces/srv/SetMode

调用:

ros2 service call /set_mode my_interfaces/srv/SetMode "{mode: 1}"

这些是很实用的调试工具。


什么时候用 Service?

你以后拿到一个功能需求,可以问:

这是持续数据吗?

是:

Topic

不是,再问:

这个请求能很快完成吗?

是:

Service

如果:

执行时间长 需要进度 需要取消

:

Action

这个判断非常实用。


Action 和 Service 最大区别

先直接对比:

Service = 请求一次 → 等结果 → 很快结束
Action = 发一个目标 Goal → 任务持续执行 → 中途不断 Feedback → 最后 Result → 过程中还能 Cancel

所以:

短任务 → Service 长任务 → Action

4、Action

例如:

执行轨迹 导航到目标点 机械臂移动到某个姿态 自动标定 执行抓取 回零流程

这些任务有共同特点:

不是瞬间完成

而且你往往希望知道:

执行到多少了? 还剩多久? 成功了吗? 失败了吗? 能不能中途取消?

这就是 Action。Action 有四个核心概念

Goal Feedback Result Cancel

分别是:

Goal = 我要你做什么
Feedback = 现在做到哪了
Result = 最终结果是什么
Cancel = 停止这个任务

这四个是 Action 的核心。


Action Server 也依赖 Executor

Action Server 内部也有:

Goal callback Cancel callback Feedback Result handling

这些都和:

Executor Callback Group

有关系。

所以如果执行逻辑写得很差:

Action也一样会卡

常见思路:

Goal callback ↓ 快速接收并验证 ↓ 交给 worker thread / state machine ↓ 后台执行 ↓ 周期发送 feedback ↓ 完成后 result

这样 Executor 可以继续处理:

Cancel 其他Topic Service

Action Server 本质上像状态机

你可以把一个 Goal 想成:

Received ↓ Accepted ↓ Executing ↓ Succeeded

或者:

Executing ↓ Canceled

或者:

Executing ↓ Aborted

所以 Action 天然带有:

任务生命周期

的概念。


Succeeded / Aborted / Canceled

Succeeded = 正常完成
Canceled = 收到取消请求并停止
Aborted = 任务因为错误无法继续

例如:

执行轨迹中 ↓ 电机fault

不是:

Canceled

更像:

Aborted

这种状态区分很有工程价值。


Action 和 Safety Layer 怎么连接?

例如执行轨迹:

Action Goal ↓ trajectory planner/executor ↓ 实时 command ↓ Safety Layer ↓ EtherCAT

如果执行过程中:

joint limit触发 communication timeout driver fault

那么:

Action Server

应该收到下层状态,最终:

Abort Goal

而不是继续显示:

Executing

所以 Action 是上层任务接口,Safety 是底层保护。


Action 和 Topic 可以一起工作

例如:

Action → 管理整条轨迹任务

同时:

Topic → 持续发布机器人状态

这两个不是互斥的。

比如:

/execute_trajectory Action /joint_states Topic /diagnostics Topic

职责各自清楚。


CLI 怎么看 Action?

可以:

ros2 action list

看所有 Action。

看类型:

ros2 action type /execute_trajectory

查看接口:

ros2 interface show my_interfaces/action/ExecuteTrajectory

发送 Goal:

ros2 action send_goal ...

还可以要求显示 Feedback。

Topic = 数据流

例:

/joint_states /mocap/human_pose

Service = 快速问答

例:

/reset_statistics /clear_fault

Action = 长任务 + Feedback + Result + Cancel

例:

/execute_trajectory /calibrate

这三个是 ROS2 接口设计最基础的判断框架。


5、Parameter

Parameter 和 Launch / YAML

Node 里:

声明参数

Launch / YAML:

传具体值

可以理解:

Node = 定义有哪些配置项 YAML = 保存一组配置 Launch = 启动时把配置灌进去

这是非常重要的工程分层。


Parameter 最适合放什么?

很适合:

端口 IP 频率 timeout queue_capacity source_type 文件路径 开关 阈值 QoS配置 日志等级

这些都属于:

部署配置。


参数回调是什么?

ROS2 可以注册参数修改 callback。

例如:

有人 ros2 param set ↓ Node收到参数变更请求 ↓ callback检查是否合法 ↓ Accept / Reject

例如:

queue_capacity = -10

明显不合法。

应该:

Reject

而不是接受后程序崩掉。


Parameter 和环境变量区别

环境变量:

export ROBOT_IP=...

也是配置方式。

但 ROS2 Parameter 有:

Node级管理 CLI查看 Launch集成 YAML集成 运行时修改

所以 ROS2 节点自身配置通常优先 Parameter。

而环境变量更适合:

系统级环境 路径 中间件选择 ROS_DOMAIN_ID

等。


Parameter 和 config 文件区别

其实 YAML Parameter 本身就是一种配置文件方式。

但第三方 SDK 可能还有:

厂商自己的json/yaml/xml

此时可以:

ROS2 Parameter ↓ 告诉程序 config_file 路径 ↓ 程序读取厂商配置

不一定要把所有第三方配置都强行拆成 ROS 参数。


Parameter 也是 API

这是很重要的系统工程思维。

一个 Node 对外不仅有:

Topic Service Action

还有:

Parameter

所以 Node 的公共接口可以画成:

Node API ├── Topics ├── Services ├── Actions └── Parameters

这就是为什么参数命名、默认值、约束都要认真设计。


6、Launch

Launch 管什么?最常见就是:

启动哪些 Node 给每个 Node 什么参数 放在哪个 namespace Topic 怎么 remap 用什么配置文件 什么时候启动 是否条件启动

所以它不是业务逻辑。

它是:

部署逻辑。

概念上:

Node( package='my_pkg', executable='mocap_driver', name='mocap_driver', parameters=[...], )

你先不用背完整语法。

只要知道:

package = 哪个包 executable = 哪个程序 name = 运行时Node名字 parameters = 参数
代码 ↓ 定义能力 YAML ↓ 保存配置 Launch ↓ 组织系统

这个三层结构非常重要。


Namespace

假设你有两台机器人:

robot1 robot2

同一个 Node:

joint_state_publisher

如果都叫:

/joint_states

就容易混。

可以通过 namespace:

/robot1/joint_states /robot2/joint_states

于是同一套软件就能复用。

namespace 就是给名字加一层“前缀”,用来避免重名冲突,或者把相关的东西组织在一起。


Remap

把 Node 原本使用的 ROS 名字,在运行时改成另一个名字。

比如代码里订阅:

cmd_vel

但系统实际控制器需要:

/diff_drive_controller/cmd_vel

Launch 可以做:

cmd_vel → /diff_drive_controller/cmd_vel

而无需改 C++。


Name 和 Namespace 不一样

例如:

namespace = /robot1 name = mocap_driver

最终 Node 可能是:

/robot1/mocap_driver

而 Topic:

human_pose

可能最终变成:

/robot1/human_pose

所以:

Node name

和:

Namespace

要分开理解。


Launch Arguments

你可以理解成:

启动整套系统时的“外部参数”。

比如:

use_sim robot_name config_file source_type

启动时:

ros2 launch my_pkg bringup.launch.py use_sim:=true

于是同一个 Launch:

真机 仿真 不同机器人

都能复用。

robot_bringup ├── launch │ ├── bringup.launch.py │ ├── mocap.launch.py │ └── control.launch.py └── config ├── mocap.yaml ├── control.yaml └── robot.yaml

这种结构很常见。机器人里bringup通常表示:

把整套系统拉起来

包括:

驱动 状态发布 控制器 传感器 诊断

所以你以后看到:

xxx_bringup

一般就知道是部署/启动包。


Launch 和 systemd

这个很重要。

Launch:

管理 ROS2 系统内部节点组合

systemd:

管理 Linux 服务生命周期

可以形成:

systemd ↓ ros2 launch robot_bringup bringup.launch.py ↓ 启动多个 ROS2 Node

也就是:

Linux服务层 → systemd ROS2系统编排层 → Launch

两者不是替代关系。

7、QoS

Quality of Service

可以理解成:

这条 ROS2 通信链路的“传输规则”。

例如同样是 Topic:

/joint_states

你可能希望:

最新数据最重要 偶尔丢一帧可以

而:

/critical_command

你可能希望:

尽量不要丢

这两个 Topic:

数据类型可能一样

但通信要求完全不同。

这就是 QoS。


Volatile

意思:

Subscriber 加入以后,只接收之后的新消息。

例如 Publisher:

10:00 发布 message A

Subscriber:

10:01 才启动

那么:

message A

通常不会自动补给它。

它只能等:

之后的新消息

这就是:

Volatile

Transient Local

可以粗略理解:

Publisher 会保留一定历史数据,让后加入的 Subscriber 也有机会拿到。

例如:

10:00 发布 robot_description/config/state 10:01 Subscriber才启动

如果配置成合适的:

Transient Local

Subscriber可能还能得到之前保存的数据。

有些信息:

不是高频一直发

但后启动节点也必须知道。

例如某些:

静态配置 一次性状态 静态变换

这种情况下:

Transient Local

就有意义。


Deadline

Deadline 可以理解:

我期望数据至少多久来一次。

例如:

期望 10 ms 一条

但:

超过10 ms还没来

就说明:

deadline missed

这非常适合表达:

通信周期要求

Deadline 和 command timeout

非常像,但层级不同。

QoS Deadline:

DDS / ROS2通信层

告诉你:

预期周期没有满足

Command Timeout:

应用Safety层

决定:

超过100ms没新command ↓ safe stop

所以推荐:

Deadline → 通信监控 Command timeout → 安全动作

不要认为有 Deadline 就不用 Safety timeout。


8、TF

header.frame_id

比如:

sensor_msgs/LaserScan sensor_msgs/PointCloud2 geometry_msgs/PoseStamped

里面经常有:

header.frame_id

例如:

frame_id = "camera_link"

意思:

这份位置/数据是在 camera_link 坐标系下表达的。

如果frame_id错了:

数值可能完全没错

但整个系统解释就会错。


TF 树

机器人里通常可以想象成:

map └── odom └── base_link ├── imu_link ├── camera_link ├── lidar_link ├── left_leg_link └── right_leg_link

这就是:

TF tree

每个 frame 都和另一个 frame 建立父子关系。


最重要的三个 frame:map / odom / base_link

这是移动机器人里最经典的一组:

map ↓ odom ↓ base_link

base_link

机器人身体本体坐标系。

比如移动机器人:

机器人中心

人形机器人可能定义在:

pelvis 躯干 base

具体取决于模型。

但核心:

base_link = 机器人本体参考坐标系

odom

odom 是一个局部连续坐标系。

特点:

短时间连续 不会突然跳

但会:

长期漂移

因为它往往来自:

轮式里程计 IMU积分 视觉里程计

这些都会积累误差。

所以:

odom → base_link

告诉你:

根据局部运动估计,机器人现在相对起点在哪里。


map

map可以理解成:

全局参考坐标系。

例如 SLAM / Localization 告诉你:

机器人在整张地图里的真实位置

它长期更准确。

但有一个问题:

定位算法重新校正时 可能产生跳变

例如原来估计:

x = 5.2

定位算法突然发现:

其实应该是 4.8

那:

map → base_link

直接变化会“跳”。


常见结构:

map ↓ odom ↓ base_link

其中:

odom → base_link

保持:

局部连续

而:

map → odom

负责:

修正长期漂移

这样:

控制

可以继续使用连续的 odom。

而:

导航

仍然知道全局 map 位置。


Static TF 和 Dynamic TF

Static TF

固定不变。

比如:

base_link → camera_link base_link → imu_link base_link → lidar_link

只要硬件没有重新安装:

位置关系不变

这种就是:

static transform

通常发布到:

/tf_static

Dynamic TF

会随时间变化。

比如:

odom → base_link

机器人在运动:

位置一直变

还有机械臂:

base_link → shoulder_link → elbow_link → wrist_link

关节转动后:

link之间关系不断变化

这些是:

dynamic transform

通常走:

/tf

/tf 和 /tf_static 区别

简单记:

/tf = 动态坐标变换
/tf_static = 静态坐标变换

比如:

odom → base_link

发/tf

而:

base_link → camera_link

可以发/tf_static


robot_state_publisher

它根据:

URDF + joint_states

计算:

机器人各个 link 之间的 TF

例如:

base_link ↓ left_hip_link ↓ left_knee_link ↓ left_ankle_link

关节角变化时:

TF跟着变化
Listener = 收TF Buffer = 存TF lookupTransform = 查TF

tf2_echo

例如:

ros2 run tf2_ros tf2_echo base_link imu_link

如果正常:

translation rotation

会不断显示。

如果失败:

Could not transform...

你就知道:

TF链路有问题

四十五、Fixed Frame 是什么?

RViz 里经常有:

Fixed Frame

例如:

map odom base_link

它表示:

RViz最终把所有数据转换到哪个统一坐标系显示。

如果 Fixed Frame 选:

map

那所有数据最终都需要能够:

transform → map

origin

子关节/子 link 的安装位置相对 parent 在哪里。

例如:

<origin xyz="0 0 0.3" rpy="0 0 0"/>

意思:

相对 parent 向 z 方向 0.3 m

并且初始没有旋转。

你可以理解成:

机械安装关系

xyz 和 rpy

xyz:

x y z

表示平移。

rpy:

roll pitch yaw

表示旋转。

所以:

<origin xyz="0.2 0 0.5" rpy="0 0 1.57"/>

就表示:

平移 + 旋转

这和上一课 TF2 是完全对应的。


axis

比如:

<axis xyz="0 0 1"/>

表示这个关节围绕:

Z轴

转。

如果:

<axis xyz="0 1 0"/>

就是绕 Y 轴。

所以如果机器人关节方向反了:

axis

就是必须检查的地方之一。


prismatic joint 直线运动关节

例如:

滑轨 升降机构 伸缩机构

不是角度变化,而是:

线位移

URDF + joint_states 怎么变成 TF?

关键角色:

robot_state_publisher

它做:

读取URDF ↓ 知道机械结构 ↓ 订阅joint_states ↓ 知道当前关节角 ↓ 计算各link当前变换 ↓ 发布TF

这就是完整逻辑。


robot_description

可以理解成:

URDF 内容作为一个 ROS 参数提供给系统。

通常:

robot_state_publisher

会拿到:

robot_description

然后解析整个机器人模型。


Xacro

Xacro 可以简单理解:

带变量、宏、条件逻辑的 URDF 模板。

因为纯 URDF XML 很容易:

重复 很长 难维护

所以用 Xacro 简化。


Xacro 里的 macro

你可以把 macro 理解成:

URDF 的函数模板。

比如:

make_leg(name, side)

生成:

hip knee ankle

一整套 link/joint。

Xacro ↓ 解析 ↓ URDF ↓ robot_state_publisher

所以:

Xacro = 生成URDF的工具

collision

描述:

这个 link 用什么几何体参与碰撞检测。

它不一定和 visual 完全一样。

例如视觉 mesh 很复杂:

100万三角形

碰撞可能只用:

简单box/cylinder

提高效率。


inertial

<inertial> 告诉 ROS 2 这个连杆的质量、质心位置、转动惯量。没有它,机器人就无法正确仿真和动力学计算。

mass center of mass inertia

动力学仿真会用到。

比如:

MuJoCo Gazebo 动力学计算

如果惯性参数乱写:

仿真可能非常奇怪

但单纯 RViz 显示可能暂时看不出来。


JointStateBroadcaster 和 robot_state_publisher 不一样

这个以后 ros2_control 会反复遇到。

joint_state_broadcaster

主要负责:

从硬件接口读取关节状态 ↓ 发布 /joint_states

然后:

robot_state_publisher

拿/joint_states:

计算 TF

所以:

Hardware ↓ joint_state_broadcaster ↓ /joint_states ↓ robot_state_publisher ↓ /tf

这条链以后一定要熟。

/controller_manager /joint_state_broadcaster /robot_state_publisher

现在你就应该知道:

controller_manager = 管控制器和硬件接口
joint_state_broadcaster = 把关节状态发出来
robot_state_publisher = 根据URDF+joint_states生成TF

这三个不是重复功能。


URDF 和 TF2 怎么对应?

URDF 中:

parent child origin

最终就是在描述:

parent frame ↓ child frame

的几何关系。

robot_state_publisher:

把URDF结构 转换成动态TF

所以上一课和这一课其实是一套东西。

真实硬件 ↓ Hardware Interface ↓ joint_state_broadcaster ↓ /joint_states ↓ robot_state_publisher ↑ URDF ↓ /tf ↓ RViz

控制另一边:

JointTrajectory / Controller ↓ Hardware Interface ↓ EtherCAT ↓ 电机

这两条链:

状态链 + 控制链

最终在 ros2_control 汇合。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 3:49:38

foobox-cn 手机远程控制:10 分钟打通手机与电脑的音乐桥梁

foobox-cn 手机远程控制&#xff1a;10 分钟打通手机与电脑的音乐桥梁 【免费下载链接】foobox-cn DUI 配置 for foobar2000 项目地址: https://gitcode.com/GitHub_Trending/fo/foobox-cn foobox-cn 是为 foobar2000 打造的 DUI 皮肤方案&#xff0c;配合少量组件即可实…

作者头像 李华
网站建设 2026/10/15 3:48:35

联盟场-提高组-R4 ZYZSC-S-Round 7

QOJ7018 Insertion Sort 原题链接&#xff1a;Insertion Sort 分析 分讨的本质就是有问题的。如果继续容斥的话&#xff0c;那就完蛋了。 正确的式子就是&#xff1a; ansk!(1Cn−k1Cn−11)ansk!\times(1C_{n-k}^{1}\times C_{n-1}^{1}) ansk!(1Cn−k1​Cn−11​) 就是相当于…

作者头像 李华
网站建设 2026/10/15 3:47:41

OpenEMR免费部署指南:从零搭建一套完整的开源电子病历系统

OpenEMR免费部署指南&#xff1a;从零搭建一套完整的开源电子病历系统 【免费下载链接】openemr The most popular open source electronic health records and medical practice management solution. 项目地址: https://gitcode.com/GitHub_Trending/op/openemr OpenE…

作者头像 李华