Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ name: Release packages
expected_version:
description: 'Hand SDK version (transport may use a compatible newer patch)'
required: true
default: '0.3.8'
default: '0.3.9'

permissions:
contents: read
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

[English](CHANGELOG.md) | [简体中文](CHANGELOG.zh-CN.md)

## 0.3.9

- Align explicit Python-policy polling counters and logical transaction latency with the Python reference, including nearest-even percentiles and a bounded device-lifetime window.
- Add force-field health projection and shared-query interval resolution; preserve Native defaults and the transport v0.3.4 minimum.
- Rebuild SDK and consumers. No new hardware acceptance is claimed; see [release notes](RELEASE_NOTES.md).

## 0.3.8

- Correct bounded poll shutdown, logical health generations and final optional-query errors in the explicit Python policy.
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

[English](CHANGELOG.md) | [简体中文](CHANGELOG.zh-CN.md)

## 0.3.9

- 统一显式 Python 策略的轮询计数和逻辑事务延迟,使用与 Python 一致的百分位取整及设备生命周期内的有界窗口。
- 补齐力字段健康投影和共享查询间隔合并,保留 Native 默认行为及 transport v0.3.4 最低依赖。
- SDK 和消费者需要重新编译,不新增实机验收结论,详见[发行说明](RELEASE_NOTES.md)。

## 0.3.8

- 修正显式 Python 策略下的有界停止轮询、健康逻辑代次和可选查询降级后的最终错误记录。
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.16)

project(linkerbot-hand-cpp
VERSION 0.3.8
VERSION 0.3.9
DESCRIPTION "LinkerBot dexterous hand C++ SDK"
LANGUAGES CXX
)
Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ This installs the SDK and communication library into the local `install/` direct
Catch2 and vendor development packages are not required. Run the remaining commands from
this repository's root directory.

Hand SDK 0.3.8 requires transport 0.3.4 or newer in the 0.3 minor line,
Hand SDK 0.3.9 requires transport 0.3.4 or newer in the 0.3 minor line,
for source builds, prebuilt packages and installed consumers.

To reuse a prebuilt transport v0.3.4 SDK, configure with
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ This installs the SDK and communication library into the local `install/` direct
Catch2 and vendor development packages are not required. Run the remaining commands from
this repository's root directory.

Hand SDK 0.3.8 requires transport 0.3.4 or newer in the 0.3 minor line,
Hand SDK 0.3.9 requires transport 0.3.4 or newer in the 0.3 minor line,
for source builds, prebuilt packages and installed consumers.

To reuse a prebuilt transport v0.3.4 SDK, configure with
Expand Down
10 changes: 10 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Release notes

## 0.3.9

- Project Python-policy polling counters independently of native transaction totals. Inactive plans report zero polling counters; active reads, verified angle command feedback and disabled optional queries retain their distinct accounting.
- Measure complete logical transactions from their first transport admission through successful completion, including transport waiting and multi-stage execution. Partial stage success does not add a completed logical transaction or clear a prior logical error. Keep the latest 2048 successful durations across close, reopen and recovery, with Python nearest-even percentile selection. Native latency accounting remains unchanged.
- Add Python-policy force-field health rows with requested, shared-query and achieved rates, query identity and observed support. Optional `PollingChannel::force_fields` entries select named fields at millisecond intervals. Shared queries use the fastest requested interval; field rows must not be summed as physical traffic. L20Lite scalar categories remain independent. Native configurations reject the new selector.

Existing C++ method signatures and Native defaults remain available. Public structure additions require rebuilding the SDK and downstream consumers with matching headers and libraries; binary compatibility with earlier headers is not claimed. The minimum and paired transport remain v0.3.4 (`0e190032d65ed0b315337b45b3779cf4f650ed68`).

Validation covers offline lifecycle, polling, command feedback, multi-stage failure, field-query resolution, bounded latency windows, source/prebuilt/installed consumers, sanitizers, complete static analysis and the twelve-target release matrix. No new physical-device acceptance is claimed. Deployed adapters/drivers, firmware, physical timestamps, mixed CAN/FD topology and multi-device duration tests require separate hardware validation. O20, Python bindings and hand/arm integration are outside this patch. Tactile-stage angle scheduling, extended identity parity and optional automatic reconnect remain separate work.

## 0.3.8

- Bound Python-policy `stop_polling()` by `shutdown_timeout`. Remove unclaimed poll demand, release poll observers and wait for already-claimed polling transactions to finish. Keep coalesced active readers and other devices running. A stop timeout leaves the device open and its polling intent disabled; recovery does not restore the stopped plan.
Expand Down
15 changes: 15 additions & 0 deletions docs/en/guides/python-behavior-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,18 @@ Public Python-policy health generations, including channel ages and recent-error
`HandDeviceConfig::route_queue_capacity` and `HandRuntimeConfig::route_queue_capacity` are optional Python-only settings. Unset selects 256 frames per existing device route inbox; a positive override is propagated through discovery, direct creation and recovery. This differs from `read_waiter_capacity` (active caller capacity) and the Session's transmit capacity. Native construction retains its 512-frame default and rejects the new override. Conflicting capacity for an existing endpoint is rejected without rebinding it. Existing C++ signatures remain available; rebuild SDK and consumers after the appended configuration fields.

This batch is offline preparation. It does not add O20, Python bindings, hand/arm integration or hardware acceptance. Tactile-stage angle scheduling, extended/raw identity fields, O30 field-by-field identity queries and optional automatic reconnect remain separate work.


## Fifth compatibility batch (0.3.9)

Only `RuntimeBehavior::kPython` uses the following health rules. Native counters, latency calculations and defaults are retained.

Polling counters describe the current plan's measurement window. Completing a query increments one read and one eligible feedback sample, even when multiple callers share the query. Publishing a report alone does not count a completed polling read. Angle command feedback follows the selected query-only, command-only or adaptive policy. Before a plan starts and after it stops, polling counters and requested rates are zero. Applying the same plan preserves the window; replacing it resets the window. Disabled optional queries retain their completed counts with zero active query rate.

Global transaction latency records one successful logical operation, including a multi-stage wire exchange, rather than one sample per native request stage. The window contains the most recent 2048 successful operations and uses Python's nearest-index, ties-to-even percentile selection. Timing starts at the first Session submission and includes its send wait. Failed logical operations do not advance successful latency or its timestamp. A caller timeout does not erase a later underlying completion. Successful identity wire acquisition is recorded before subsequent fingerprint validation. This device-local history survives close, reopen and recovery; a failed initial open discards its provisional history. It is independent between devices sharing a Session.

`HandHealth::force_fields` reports each field's requested rate, resolved query, active query rate, achieved query rate and observed support. Requested field intent is distinct from physical traffic. Unknown descriptor support is represented by an empty optional, with zero achieved field rate until observed. Fields sharing a query use the fastest requested interval, not a sum of rates. Independent L20Lite scalar categories retain separate query identities. Descriptor observations can remove unsupported fields from active polling without erasing desired field intent or unrelated counts. Health reads perform no device I/O.

`PollingChannel::force_fields` is an optional field-level selector on `kForce`. For example, `{{"fn", 20ms}, {"fns", 50ms}}` requests 50 Hz and 20 Hz respectively; if both share one wire query, that query runs at 50 Hz. An empty selector preserves the existing channel form and resolves Python's default force fields `fn` and `fns`. Intervals are positive representable whole milliseconds. Invalid or duplicate field names and unsupported explicit fields are rejected before replacing the active plan. Native policy rejects a nonempty field selector. Public methods remain available, but consumers must rebuild for the appended public structure fields.

Offline tests cover counter boundaries, logical success/failure, shared-device isolation, force projection and an independent Python percentile comparison over 6144 rolling-window updates. These tests do not establish hardware timing or throughput. O20, Python bindings, hand/arm integration, tactile-stage angle scheduling and remaining identity/reconnect differences remain outside this batch.
2 changes: 1 addition & 1 deletion docs/en/guides/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ availability of proprietary ARM64 drivers.

Hand SDK packages contain both hand and paired transport static libraries, headers, CMake packages,
documentation and read-only command-line examples. Test executables and proprietary drivers/libraries
are excluded. Versions remain independent. Hand 0.3.8 requires transport 0.3.4 or newer in
are excluded. Versions remain independent. Hand 0.3.9 requires transport 0.3.4 or newer in
the 0.3 minor line in source, prebuilt and installed-consumer builds.


Expand Down
9 changes: 8 additions & 1 deletion docs/en/guides/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A parent-provided `linkerbot::comm` target is reused and version-checked.
The legacy empty `LINKERBOT_COMM_SOURCE_DIR=` still selects package discovery through
`CMAKE_PREFIX_PATH` in AUTO mode. Configure logs identify the selected dependency.
Use separate build directories when switching dependencies to avoid stale CMake caches.
Hand 0.3.8 requires transport 0.3.4 or newer in the 0.3 minor line. Source,
Hand 0.3.9 requires transport 0.3.4 or newer in the 0.3 minor line. Source,
prebuilt and installed-consumer checks reject older patches and other minor lines.
For paired development, keep the source commit pinned in README and CI; record the
actual revision and configuration in test reports.
Expand Down Expand Up @@ -123,3 +123,10 @@ hardware reports apply only to their recorded revision and environment.

`.vscode/` is ignored in full. Keep machine paths and personal launch settings
local; maintained build and test commands belong in CMake, the justfile and docs.


## Python-policy health statistics

`python-health-vectors` compares all 6144 updates of a 2048-sample rolling window against an independent Python rounding oracle. With `LINKERBOT_PYTHON_SDK_DIR` configured, it also checks the reference SDK's `RuntimeMetrics`. The normal test needs only Python's standard library. The reference-SDK mode additionally needs that checkout's dependencies and transport on `PYTHONPATH`. This checks percentile selection, duplicates and eviction; it is not a hardware latency benchmark.

Python-policy communication regressions run in `python-policy-regressions`; `comm` runs the remaining communication cases. The groups partition the original communication selection without omissions or duplicates. Per-process acceptance budgets and individual transaction deadlines are unchanged.
15 changes: 15 additions & 0 deletions docs/zh/guides/python-behavior-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,18 @@ Python 策略公开健康状态、通道采样年龄和最近错误上下文的
新增 `HandDeviceConfig::route_queue_capacity` 和 `HandRuntimeConfig::route_queue_capacity`,仅允许 Python 策略使用。未指定时,现有每设备路由接收队列容量为 256 帧;正值覆盖通过直接构造、发现和恢复传递。它不是 `read_waiter_capacity` 主动等待者容量,也不是 Session 发送队列容量。Native 构造仍使用默认 512 帧,并拒绝新的容量覆盖。同一端点已有不同容量绑定时拒绝复用,不修改其连接。新增结构体字段要求重新编译 SDK 和消费者。

本批为离线准备,不增加 O20、Python 绑定、hand/arm 接入或实机验收。触觉阶段之间的角度调度、扩展及原始身份字段、O30 逐字段身份查询和可选自动重连仍为独立工作。


## 第五批兼容修正(0.3.9)

以下规则仅用于显式 `RuntimeBehavior::kPython`,Native 计数、延迟算法和默认行为保持不变。

轮询统计只描述当前计划的测量窗口。一次查询完成计入一次读取和符合反馈策略的一次反馈,即使多个调用方共享该查询也不重复计数。单独发布报告不增加轮询完成数。角度命令反馈按 query-only、command-only 或 adaptive 策略统计。未开始和停止计划后,轮询计数和请求频率归零;相同计划保留窗口,替换计划重置窗口;降级停用的可选查询保留历史计数,活动查询频率为零。

全局事务延迟按一次成功的逻辑操作统计,多阶段交换不按底层阶段重复计数。窗口保留最近 2048 次成功操作,百分位采用 Python 最近索引、恰好居中时取偶数的算法。计时从首次向 Session 提交开始,包括发送等待。失败逻辑操作不更新成功延迟和时间戳。调用方超时不抹除底层随后成功完成的记录。身份信息在线路上成功获取后、身份指纹校验前记录事务成功。每个设备的历史在关闭、重新打开和恢复间保留;首次打开失败时丢弃临时历史。共享 Session 的设备不共享这一统计窗口。

`HandHealth::force_fields` 分别展示字段请求频率、解析后的查询、活动查询频率、已完成查询频率和观测到的支持状态。字段意图与实际查询分开表达;未知描述符支持状态用空 optional 表示,在确认前字段达成频率为零。共享查询取最快的请求间隔,不相加;L20Lite 标量类别保留独立查询。描述符确认不支持某字段后,可停止相应活动查询,但保留字段意图和其他计数。读取健康状态不触发设备通信。

`PollingChannel::force_fields` 是 `kForce` 上的可选字段选择器。例如 `{{"fn", 20ms}, {"fns", 50ms}}` 分别请求 50 Hz 和 20 Hz;若共用查询,实际查询为 50 Hz。空选择器保留原有通道用法,解析为 Python 默认字段 `fn`、`fns`。间隔为可表示的正整数毫秒。非法名称、重复字段和显式不支持的字段在替换计划前拒绝。Native 策略拒绝非空字段选择器。公开方法保持兼容;公开结构追加字段后,SDK 和消费者需要重新编译。

离线回归覆盖计数边界、逻辑事务成功与失败、共享 Session 的设备隔离、力字段投影,以及 6144 次窗口更新的独立 Python 百分位对照。这些验证不代表硬件时序或吞吐验收。本批不包括 O20、Python 绑定、hand/arm 接入、触觉阶段角度调度以及剩余身份字段和重连差异。
2 changes: 1 addition & 1 deletion docs/zh/guides/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ libstdc++ ABI1 和 C++17。Windows 使用原生架构 runner 与 v143 14.44 工

产物是开发 SDK,Hand 包包含 hand 与配套 transport 静态库、头文件、CMake 包和文档,
保留只读命令行示例;不包含测试程序、厂商驱动或厂商库。
两个项目版本独立。hand 0.3.8 的源码、预构建及安装消费者均要求 transport 0.3.x 且不低于 0.3.4。
两个项目版本独立。hand 0.3.9 的源码、预构建及安装消费者均要求 transport 0.3.x 且不低于 0.3.4。


配套源码固定提交和预构建下载统一使用 v0.3.4 发行版。transport 公共结构有新增字段,
Expand Down
9 changes: 8 additions & 1 deletion docs/zh/guides/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
父项目已经提供 `linkerbot::comm` 时,复用父项目的目标,并检查版本。
显式设置旧参数 `LINKERBOT_COMM_SOURCE_DIR=` 仍可在 AUTO 下通过 `CMAKE_PREFIX_PATH`
使用安装包。配置日志会显示实际选择的依赖;切换依赖时使用独立构建目录,避免旧 CMake 缓存。
hand 0.3.8 要求 transport 0.3.x 且不低于 0.3.4。源码、预构建及安装消费者测试均拒绝更低补丁版本和其他 minor 版本。
hand 0.3.9 要求 transport 0.3.x 且不低于 0.3.4。源码、预构建及安装消费者测试均拒绝更低补丁版本和其他 minor 版本。
源码开发继续使用 README 与 CI 固定的提交,并在测试报告中记录实际版本和构建配置。

### 从源码构建
Expand Down Expand Up @@ -107,3 +107,10 @@ PR 作业执行离线测试和协议检查;main 分支还按工作流的平台

整个 `.vscode/` 目录均被忽略。机器路径和个人启动配置留在本地,项目维护的构建、
测试命令应放在 CMake、justfile 和文档中。


## Python 策略健康统计

`python-health-vectors` 将 2048 样本窗口的全部 6144 次更新与独立 Python 取整算法比较。配置 `LINKERBOT_PYTHON_SDK_DIR` 后,还比较参考 SDK 的 `RuntimeMetrics`。常规测试仅依赖 Python 标准库;参考 SDK 模式需要安装该版本的依赖,并在 `PYTHONPATH` 中提供 transport。测试覆盖百分位、重复值和样本淘汰,不代表硬件延迟基准。

Python 策略通信回归单独放在 `python-policy-regressions`,其余通信测试继续由 `comm` 执行。两组覆盖原通信选择集合,无遗漏、无重复。每个进程的验收预算及单事务超时均未调整。
10 changes: 10 additions & 0 deletions include/linkerbot/hand/api/events.hpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
#pragma once

#include <chrono>
#include <string>
#include <variant>
#include <vector>

#include <linkerbot/hand/api/readings.hpp>
#include <linkerbot/hand/capabilities.hpp>

namespace linkerbot::hand {
struct ForceFieldPolling {
std::string field;
std::chrono::milliseconds interval{100};
};

struct PollingChannel {
TelemetrySemantic semantic = TelemetrySemantic::kAngles;
std::chrono::milliseconds interval{100};
// Used for angles by the explicit Python runtime policy only.
AngleFeedbackPolicy angle_feedback = AngleFeedbackPolicy::kAdaptive;
// Python-only field selection on kForce. Nonempty selections
// use their own intervals, resolved into shared queries at the fastest
// requested interval. Empty retains the existing query-channel API.
std::vector<ForceFieldPolling> force_fields;
};

using PollingPlan = std::vector<PollingChannel>;
Expand Down
12 changes: 12 additions & 0 deletions include/linkerbot/hand/health.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ namespace linkerbot::hand {
LatencyMetrics execution_latency;
};

struct ForceFieldMetrics {
std::string field;
double requested_hz = 0.0;
double query_hz = 0.0;
double achieved_hz = 0.0;
std::optional<TelemetrySemantic> query_channel;
std::optional<bool> supported;
};

// Complete, zero-I/O snapshot for one hand connection generation. The
// runtime returns this value by copy from mutex-protected cached state;
// reading health never sends a frame or waits for a device response.
Expand Down Expand Up @@ -149,6 +158,9 @@ namespace linkerbot::hand {
LatencyMetrics transaction_latency;
can::TransportHealth transport;
transport::SessionHealth session;
// Python policy: field intent and shared-query rates. Never sum these
// rows to estimate physical traffic; channel counters count queries once.
std::vector<ForceFieldMetrics> force_fields;

[[nodiscard]] bool connected() const noexcept { return connection_state == ConnectionState::kConnected; }

Expand Down
Loading