From 456e7bba4f9cfd1fb4d0455d045a9c29ab239b2c Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:15:55 +0800 Subject: [PATCH 01/11] Integrate finite-measurement core candidate with verified snapshot provenance --- CONTRIBUTING.md | 11 +++++++++++ core/SNAPSHOT.sha256 | 8 ++++---- core/UPSTREAM | 4 ++-- core/src/detail/client_impl.cpp | 6 ++++++ core/src/detail/xml_config.cpp | 5 +++++ core/src/types.cpp | 17 +++++++++++++++-- tests/quality/test_sync_core.py | 5 ++++- tools/sync_core.py | 21 +++++++++++++++++---- 8 files changed, 64 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5237e6c..373347f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,3 +124,14 @@ Maintainers must verify the publisher owner, repository, workflow filename, and Keep each pull request limited to one coherent change. Describe the problem, chosen behavior, tests run, supported platforms, hardware involvement, and any core or ROS synchronization impact. Update public documentation and `CHANGELOG.md` when users need to know about a change. All contributions are submitted under the [Apache License 2.0](https://github.com/netft/pyNetFT/blob/main/LICENSE). The prior pyNetFT MIT text and third-party license notices remain in `LICENSES/`. + +### Current core candidate + +The private snapshot uses unpublished upstream commit `8aec517a8d4baed66089e0e9d0928c90f8ebfadb`; `UPSTREAM` marks it `unreleased`. It is not the published v0.3.3 snapshot. Update from a clean upstream checkout using: + +```sh +python tools/sync_core.py sync --source /path/to/netft-cpp --commit 8aec517a8d4baed66089e0e9d0928c90f8ebfadb +python tools/sync_core.py verify +``` + +The manifest checks the selected bytes, including provenance. It detects drift and does not authenticate a remote release. Consumer build files remain private; fixes to core source belong upstream. diff --git a/core/SNAPSHOT.sha256 b/core/SNAPSHOT.sha256 index 6006ee5..9bf7d58 100644 --- a/core/SNAPSHOT.sha256 +++ b/core/SNAPSHOT.sha256 @@ -1,6 +1,6 @@ 3db6fdfbb62d6073fa3f77784235cfe2eff06a1f102fb1d0f3a3f6da02dafa01 CMakeLists.txt c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 LICENSE -926aedecaf33e0d9a178360ea455ee9cbdd54537f89d75835409fdb28fdcd908 UPSTREAM +ab2c5a15a2865ddea6eae3dc59f67251f91e63f5a521396b9f6527be4d52f658 UPSTREAM 40701f04e67bb18052b6c8f1db60cf5d41baab09e5396f5a88c1eb14ad1a7139 cmake/netftConfig.cmake.in e4a52fe7c7e13de95a29bdbd688d295a4a8ddc1805276274b0c14d4092f7253d include/netft/client.hpp 4f7fb383da5456fd8477e1981f4ff3eba9b82ec3a98f3c395e7d482faf75c112 include/netft/discovery.hpp @@ -8,7 +8,7 @@ e4a52fe7c7e13de95a29bdbd688d295a4a8ddc1805276274b0c14d4092f7253d include/netft/ 912892bcb8bb5f95392c9c74983d91aab103b7adc5735505b0ac97e1012893b1 include/netft/status.hpp a7c899858aa7a0a28780504f14c1ecdaebb375607119a03500903f3782f45a68 include/netft/types.hpp 7199f02fa0fd88059323f507edbf073b2a4827c692d1fd824893cbad8ea9215b src/client.cpp -a7e0bcba7a8cd5ccb1ab04658ea735f2b6a3f2bac81c3bce95578e3c14349650 src/detail/client_impl.cpp +1f1fc61f2f34ae57f6b881b9f3cc9c9097036dac5751b9750a8741684ff06e87 src/detail/client_impl.cpp b202cc6e250866e681d16b480911f77d865d9187300111f8482bc4a8b0424b34 src/detail/client_impl.hpp ad8744385642693bb4666c428a5dbd0d6eff350b0079a6c9ff720512e89b6a53 src/detail/fault_latch.cpp be488c557e17044948cd62e6de07bf54862d389a584b40b78336479cecdaf23c src/detail/fault_latch.hpp @@ -19,8 +19,8 @@ fe9034a4a32138c3469005a44a1fbd61cfe41df25207723635a22e8ed376dbf5 src/detail/seq 78098e0c98c84103653b7aa9e2baf689cde656b52e1e13873f7491aed7f35ff5 src/detail/udp_transport.hpp 6c4fb6a8b6bdfc9f1364ac0d76423e9972e1eb30bb987aa6865e191e7d93f9ea src/detail/udp_transport_posix.cpp 9a9e34da6e9d66e0918e85da61ebc60f9ae3806a0471c327f818e8ccd210426e src/detail/udp_transport_windows.cpp -3e908eac916b3cf457387fd878dd77fb46e3b5a9eee37dfc60be788649476475 src/detail/xml_config.cpp +95a05fe378b735b433ea10f438e9f5338120830e054b30ed11cd8db12d760cea src/detail/xml_config.cpp f10ff61cd2cc645abca23a7abce37439273569c6fe139a4fb07041532edb908d src/detail/xml_config.hpp 5a25276d0307c6a341aaee525cc9a66f00cda06e30bd345716ff5ee17a57e3e7 src/discovery.cpp 2598c182a2322925c2d99e1d0bba3b57215e451ba1ecc8df6b108b885f089c1f src/status.cpp -6017859e709c6b80fd5dc444554e497410e1c7d19a12ae05502f0c9922c58762 src/types.cpp +17c9257d857ec91703c055578bf645383b752519c4c6345586da23b6a4c89050 src/types.cpp diff --git a/core/UPSTREAM b/core/UPSTREAM index c7bc616..44e4957 100644 --- a/core/UPSTREAM +++ b/core/UPSTREAM @@ -1,4 +1,4 @@ repository=https://github.com/netft/netft-cpp -tag=v0.3.3 -commit=3259b8576b16fb150b51e9ef9090366c5e6efcba +tag=unreleased +commit=8aec517a8d4baed66089e0e9d0928c90f8ebfadb paths=CMakeLists.txt,LICENSE,cmake,include,src diff --git a/core/src/detail/client_impl.cpp b/core/src/detail/client_impl.cpp index 5ebc20c..3ec09b0 100644 --- a/core/src/detail/client_impl.cpp +++ b/core/src/detail/client_impl.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -631,6 +632,11 @@ Client::Impl::handle_record(const detail::RawRecord &record, sample.torque = {record.tx / calibration.counts_per_torque_unit, record.ty / calibration.counts_per_torque_unit, record.tz / calibration.counts_per_torque_unit}; + const auto finite = [](double value) { return std::isfinite(value); }; + if (!std::all_of(sample.force.begin(), sample.force.end(), finite) || + !std::all_of(sample.torque.begin(), sample.torque.end(), finite)) { + return SessionResult::SensorConfiguration; + } sample.force_unit = calibration.force_unit; sample.torque_unit = calibration.torque_unit; sample.configuration_revision = configuration.revision; diff --git a/core/src/detail/xml_config.cpp b/core/src/detail/xml_config.cpp index 3e6bb03..dd40fef 100644 --- a/core/src/detail/xml_config.cpp +++ b/core/src/detail/xml_config.cpp @@ -278,6 +278,11 @@ SensorConfiguration parse_sensor_configuration(std::string_view xml) { configuration.calibration.counts_per_torque_unit = parse_positive_count(fields[2], "cfgcpt"); configuration.calibration.force_unit = parse_force_unit(fields[3]); configuration.calibration.torque_unit = parse_torque_unit(fields[4]); + try { + validate(configuration.calibration); + } catch (const std::invalid_argument &error) { + throw DiscoveryError(error.what()); + } configuration.source = CalibrationSource::Sensor; configuration.revision = 1; return configuration; diff --git a/core/src/types.cpp b/core/src/types.cpp index 3f2824b..76d376a 100644 --- a/core/src/types.cpp +++ b/core/src/types.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace netft { @@ -26,11 +27,23 @@ void require_port(std::string_view name, int value) { } } +void require_safe_count(std::string_view name, double value) { + require_positive_finite(name, value); + // INT32_MIN has the largest magnitude. Round the minimum divisor up so + // every raw count remains representable, including under directed rounding. + constexpr double magnitude = -static_cast(std::numeric_limits::min()); + constexpr double minimum = (magnitude / std::numeric_limits::max()) * + (1.0 + std::numeric_limits::epsilon()); + if (value < minimum) { + throw std::invalid_argument(std::string{name} + " cannot represent the full raw count range"); + } +} + } // namespace void validate(const Calibration &calibration) { - require_positive_finite("counts_per_force_unit", calibration.counts_per_force_unit); - require_positive_finite("counts_per_torque_unit", calibration.counts_per_torque_unit); + require_safe_count("counts_per_force_unit", calibration.counts_per_force_unit); + require_safe_count("counts_per_torque_unit", calibration.counts_per_torque_unit); if (calibration.force_unit == ForceUnit::Unknown || calibration.torque_unit == TorqueUnit::Unknown) { throw std::invalid_argument("calibration units must be known"); diff --git a/tests/quality/test_sync_core.py b/tests/quality/test_sync_core.py index 70c11fd..a4af60f 100644 --- a/tests/quality/test_sync_core.py +++ b/tests/quality/test_sync_core.py @@ -34,13 +34,16 @@ def test_sync_removes_files_outside_the_selected_snapshot( git(source, "add", ".") git(source, "commit", "-m", "fixture") git(source, "tag", "v-test") + git(source, "remote", "add", "origin", "https://github.com/netft/netft-cpp.git") destination = tmp_path / "core" destination.mkdir() stale = destination / "removed-upstream-path.txt" stale.write_text("stale\n", encoding="utf-8") - sync_core.sync(source, destination, "v-test") + sync_core.sync(source, destination, None, candidate=git(source, "rev-parse", "HEAD")) assert not stale.exists() sync_core.verify(destination) + + assert "tag=unreleased" in (destination / "UPSTREAM").read_text() diff --git a/tools/sync_core.py b/tools/sync_core.py index 17d6adb..094ebbe 100644 --- a/tools/sync_core.py +++ b/tools/sync_core.py @@ -3,6 +3,7 @@ import argparse import hashlib +import re import shutil import subprocess import tempfile @@ -45,8 +46,18 @@ def verify(root: Path) -> None: raise SystemExit("core snapshot checksum mismatch") -def sync(source: Path, destination: Path, tag: str) -> None: - commit = git(source, "rev-parse", f"{tag}^{{commit}}") +def sync(source: Path, destination: Path, tag: str | None, *, candidate: str | None = None) -> None: + if candidate is not None and not re.fullmatch(r"[0-9a-f]{40}", candidate): + raise SystemExit("candidate must be a full lowercase commit SHA") + if git(source, "remote", "get-url", "origin") != "https://github.com/netft/netft-cpp.git": + raise SystemExit("source repository mismatch") + reference = candidate or tag + if not reference: + raise SystemExit("source identity is required") + commit = git(source, "rev-parse", f"{reference}^{{commit}}") + if candidate is not None and commit != candidate: + raise SystemExit("candidate identity mismatch") + tag = "unreleased" if candidate else tag head = git(source, "rev-parse", "HEAD") if head != commit: raise SystemExit(f"{source} HEAD does not match {tag}") @@ -101,12 +112,14 @@ def main() -> None: subparsers = parser.add_subparsers(dest="command", required=True) sync_parser = subparsers.add_parser("sync") sync_parser.add_argument("--source", type=Path, required=True) - sync_parser.add_argument("--tag", required=True) + identity = sync_parser.add_mutually_exclusive_group(required=True) + identity.add_argument("--tag") + identity.add_argument("--commit") subparsers.add_parser("verify") arguments = parser.parse_args() root = Path(__file__).resolve().parents[1] if arguments.command == "sync": - sync(arguments.source.resolve(), root / "core", arguments.tag) + sync(arguments.source.resolve(), root / "core", arguments.tag, candidate=arguments.commit) else: verify(root / "core") From 6710e48f944b16b7bc6e5a515ae11c5e65dc49eb Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:21:26 +0800 Subject: [PATCH 02/11] Require example dependencies and pin release build provenance --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/codeql.yml | 8 ++++---- .github/workflows/coverage.yml | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b9dc0b..90ef954 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Verify controlled core snapshot @@ -85,7 +85,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Configure @@ -113,7 +113,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Configure @@ -145,7 +145,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Verify pinned CMake version @@ -169,7 +169,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Configure @@ -205,7 +205,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Configure diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 33c2546..34becf0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -35,18 +35,18 @@ jobs: persist-credentials: false - name: Initialize Python CodeQL if: matrix.language == 'python' - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Initialize C++ CodeQL with inline suppressions if: matrix.language == 'c-cpp' - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} packs: ${{ matrix.suppression-pack }} - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 if: matrix.language == 'c-cpp' with: cache: true @@ -54,4 +54,4 @@ jobs: if: matrix.language == 'c-cpp' run: pixi run install - name: Analyze - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 56341cb..06f51e6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -20,13 +20,13 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: prefix-dev/setup-pixi@v0.10.0 + - uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0 with: cache: true - name: Generate Python and native coverage run: pixi run coverage - name: Upload Python coverage - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 continue-on-error: true with: files: coverage.xml @@ -34,7 +34,7 @@ jobs: flags: python use_oidc: true - name: Upload native coverage - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 continue-on-error: true with: files: coverage-native.xml From 1a807620c9995c46c6c572d75fdcc8881154664a Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:31:40 +0800 Subject: [PATCH 03/11] Integrate checked timing and clarify runtime control boundaries --- CONTRIBUTING.md | 4 ++-- core/SNAPSHOT.sha256 | 11 +++++---- core/UPSTREAM | 2 +- core/src/detail/client_impl.cpp | 12 ++++++---- core/src/detail/time.hpp | 29 +++++++++++++++++++++++ core/src/detail/udp_transport_posix.cpp | 3 ++- core/src/detail/udp_transport_windows.cpp | 6 +++-- core/src/types.cpp | 27 ++++++++++++++++----- 8 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 core/src/detail/time.hpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 373347f..f0f1806 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,10 +127,10 @@ All contributions are submitted under the [Apache License 2.0](https://github.co ### Current core candidate -The private snapshot uses unpublished upstream commit `8aec517a8d4baed66089e0e9d0928c90f8ebfadb`; `UPSTREAM` marks it `unreleased`. It is not the published v0.3.3 snapshot. Update from a clean upstream checkout using: +The private snapshot uses unpublished upstream commit `91f012c5d6f9b63902765ccbec3437cb286c15e1`; `UPSTREAM` marks it `unreleased`. It is not the published v0.3.3 snapshot. Update from a clean upstream checkout using: ```sh -python tools/sync_core.py sync --source /path/to/netft-cpp --commit 8aec517a8d4baed66089e0e9d0928c90f8ebfadb +python tools/sync_core.py sync --source /path/to/netft-cpp --commit 91f012c5d6f9b63902765ccbec3437cb286c15e1 python tools/sync_core.py verify ``` diff --git a/core/SNAPSHOT.sha256 b/core/SNAPSHOT.sha256 index 9bf7d58..cfc2959 100644 --- a/core/SNAPSHOT.sha256 +++ b/core/SNAPSHOT.sha256 @@ -1,6 +1,6 @@ 3db6fdfbb62d6073fa3f77784235cfe2eff06a1f102fb1d0f3a3f6da02dafa01 CMakeLists.txt c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 LICENSE -ab2c5a15a2865ddea6eae3dc59f67251f91e63f5a521396b9f6527be4d52f658 UPSTREAM +7b27434afaf998a250f54302fc7ead8416d0bfb550147cbc4c87858f64318864 UPSTREAM 40701f04e67bb18052b6c8f1db60cf5d41baab09e5396f5a88c1eb14ad1a7139 cmake/netftConfig.cmake.in e4a52fe7c7e13de95a29bdbd688d295a4a8ddc1805276274b0c14d4092f7253d include/netft/client.hpp 4f7fb383da5456fd8477e1981f4ff3eba9b82ec3a98f3c395e7d482faf75c112 include/netft/discovery.hpp @@ -8,7 +8,7 @@ e4a52fe7c7e13de95a29bdbd688d295a4a8ddc1805276274b0c14d4092f7253d include/netft/ 912892bcb8bb5f95392c9c74983d91aab103b7adc5735505b0ac97e1012893b1 include/netft/status.hpp a7c899858aa7a0a28780504f14c1ecdaebb375607119a03500903f3782f45a68 include/netft/types.hpp 7199f02fa0fd88059323f507edbf073b2a4827c692d1fd824893cbad8ea9215b src/client.cpp -1f1fc61f2f34ae57f6b881b9f3cc9c9097036dac5751b9750a8741684ff06e87 src/detail/client_impl.cpp +d4a7c89afa6a98ef687e7ecb266c8ce3287ec2f01c4f14d29278cf3cce140640 src/detail/client_impl.cpp b202cc6e250866e681d16b480911f77d865d9187300111f8482bc4a8b0424b34 src/detail/client_impl.hpp ad8744385642693bb4666c428a5dbd0d6eff350b0079a6c9ff720512e89b6a53 src/detail/fault_latch.cpp be488c557e17044948cd62e6de07bf54862d389a584b40b78336479cecdaf23c src/detail/fault_latch.hpp @@ -16,11 +16,12 @@ f5953a7fc4fd5eee56f7aa3568fe7417010e3e767cfbb0ee5922ee5f8aa2a9aa src/detail/pro 8ef13600d67b2bb77181c5403e9b51b7f48b6f8ef06381fb1f07c5b9b2a90d8a src/detail/protocol.hpp fe9034a4a32138c3469005a44a1fbd61cfe41df25207723635a22e8ed376dbf5 src/detail/sequence.cpp 72daab01251425dd7fd5d26334760f4937f94e674a16df0fb38301b0ee78b9c1 src/detail/sequence.hpp +8fdc8f3373008f41ea032478ac44ef5274013eeb4ea56f5fdd265895672ee0ae src/detail/time.hpp 78098e0c98c84103653b7aa9e2baf689cde656b52e1e13873f7491aed7f35ff5 src/detail/udp_transport.hpp -6c4fb6a8b6bdfc9f1364ac0d76423e9972e1eb30bb987aa6865e191e7d93f9ea src/detail/udp_transport_posix.cpp -9a9e34da6e9d66e0918e85da61ebc60f9ae3806a0471c327f818e8ccd210426e src/detail/udp_transport_windows.cpp +f6e7e14de70a366e3d25b139dee8f061cdfff4c5173c78b831e564125b81d4b2 src/detail/udp_transport_posix.cpp +d48e67880a9c0db5f2c9b4045cc7caddb7fdfbb934cb676d25914c9cf2d5cfe5 src/detail/udp_transport_windows.cpp 95a05fe378b735b433ea10f438e9f5338120830e054b30ed11cd8db12d760cea src/detail/xml_config.cpp f10ff61cd2cc645abca23a7abce37439273569c6fe139a4fb07041532edb908d src/detail/xml_config.hpp 5a25276d0307c6a341aaee525cc9a66f00cda06e30bd345716ff5ee17a57e3e7 src/discovery.cpp 2598c182a2322925c2d99e1d0bba3b57215e451ba1ecc8df6b108b885f089c1f src/status.cpp -17c9257d857ec91703c055578bf645383b752519c4c6345586da23b6a4c89050 src/types.cpp +2b59ff7f0c88207e47d295168eaf4383fbecc008b5e4f1346fb63fddfe02b72b src/types.cpp diff --git a/core/UPSTREAM b/core/UPSTREAM index 44e4957..e643a8f 100644 --- a/core/UPSTREAM +++ b/core/UPSTREAM @@ -1,4 +1,4 @@ repository=https://github.com/netft/netft-cpp tag=unreleased -commit=8aec517a8d4baed66089e0e9d0928c90f8ebfadb +commit=91f012c5d6f9b63902765ccbec3437cb286c15e1 paths=CMakeLists.txt,LICENSE,cmake,include,src diff --git a/core/src/detail/client_impl.cpp b/core/src/detail/client_impl.cpp index 3ec09b0..c8f7167 100644 --- a/core/src/detail/client_impl.cpp +++ b/core/src/detail/client_impl.cpp @@ -1,4 +1,5 @@ #include "detail/client_impl.hpp" +#include "detail/time.hpp" #include #include @@ -302,12 +303,13 @@ void Client::Impl::bias() { } bool Client::Impl::wait_for_first_sample(const std::chrono::duration timeout) { + const auto deadline = detail::checked_deadline(std::chrono::steady_clock::now(), timeout); std::unique_lock data_lock(data_mutex_); const auto captured_generation = generation_; if (captured_generation == 0) { return false; } - first_sample_cv_.wait_for(data_lock, timeout, [this, captured_generation] { + first_sample_cv_.wait_until(data_lock, deadline, [this, captured_generation] { return generation_ != captured_generation || delivered_generation_ == captured_generation || stopping_ || faulted(); }); @@ -407,7 +409,9 @@ void Client::Impl::run() noexcept { } std::unique_lock data_lock(data_mutex_); - first_sample_cv_.wait_for(data_lock, backoff, [this] { return stopping_.load(); }); + first_sample_cv_.wait_until(data_lock, + detail::checked_deadline(detail::SteadyClock::now(), backoff), + [this] { return stopping_.load(); }); data_lock.unlock(); backoff = std::min(backoff * 2.0, config_.reconnect_max_delay); } @@ -462,7 +466,7 @@ Client::Impl::SessionOutcome Client::Impl::receive_session() { } const auto timeout = config_.receive_timeout; - auto deadline = std::chrono::steady_clock::now() + timeout; + auto deadline = detail::checked_deadline(std::chrono::steady_clock::now(), timeout); std::array buffer{}; while (!stopping_) { const auto now = std::chrono::steady_clock::now(); @@ -512,7 +516,7 @@ Client::Impl::SessionOutcome Client::Impl::receive_session() { consecutive_malformed_ = 0; } received_valid_record = true; - deadline = received_at + timeout; + deadline = detail::checked_deadline(received_at, timeout); if (const auto outcome = handle_record(record, received_at)) { return {*outcome, {}, received_valid_record}; } diff --git a/core/src/detail/time.hpp b/core/src/detail/time.hpp new file mode 100644 index 0000000..b5c1aae --- /dev/null +++ b/core/src/detail/time.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +namespace netft::detail { +using SteadyClock = std::chrono::steady_clock; + +inline SteadyClock::duration checked_duration(std::chrono::duration duration) { + const auto ticks = static_cast(duration.count()) * SteadyClock::period::den / + SteadyClock::period::num; + if (!std::isfinite(ticks) || ticks < 0 || + ticks >= static_cast(SteadyClock::duration::max().count())) { + throw std::invalid_argument("duration is outside the supported steady-clock range"); + } + return SteadyClock::duration{static_cast(ticks)}; +} + +inline SteadyClock::time_point checked_deadline(SteadyClock::time_point origin, + std::chrono::duration duration) { + const auto ticks = checked_duration(duration); + if (origin > SteadyClock::time_point::max() - ticks) { + throw std::invalid_argument("deadline is outside the supported steady-clock range"); + } + return origin + ticks; +} +} // namespace netft::detail diff --git a/core/src/detail/udp_transport_posix.cpp b/core/src/detail/udp_transport_posix.cpp index 5af2b7f..923f7f8 100644 --- a/core/src/detail/udp_transport_posix.cpp +++ b/core/src/detail/udp_transport_posix.cpp @@ -1,3 +1,4 @@ +#include "detail/time.hpp" #include "detail/udp_transport.hpp" #include @@ -106,7 +107,7 @@ std::size_t UdpTransport::receive(std::uint8_t *data, const std::size_t capacity } pollfd descriptor{socket, POLLIN, 0}; - const auto deadline = std::chrono::steady_clock::now() + timeout; + const auto deadline = checked_deadline(std::chrono::steady_clock::now(), timeout); auto remaining = timeout; int poll_result{}; if (wait_started_hook != nullptr) { diff --git a/core/src/detail/udp_transport_windows.cpp b/core/src/detail/udp_transport_windows.cpp index 1330f54..7209db2 100644 --- a/core/src/detail/udp_transport_windows.cpp +++ b/core/src/detail/udp_transport_windows.cpp @@ -1,3 +1,4 @@ +#include "detail/time.hpp" #include "detail/udp_transport.hpp" #ifndef NOMINMAX @@ -145,8 +146,9 @@ std::size_t UdpTransport::receive(std::uint8_t *data, const std::size_t capacity if (wait_started_hook != nullptr) { wait_started_hook(wait_started_context); } - const auto bounded_timeout = std::max(timeout, std::chrono::duration::zero()); - const auto deadline = std::chrono::steady_clock::now() + bounded_timeout; + static_cast(checked_duration(timeout)); + const auto bounded_timeout = timeout; + const auto deadline = checked_deadline(std::chrono::steady_clock::now(), bounded_timeout); constexpr int kShutdownCheckIntervalMilliseconds = 50; constexpr auto kShutdownCheckInterval = std::chrono::milliseconds{kShutdownCheckIntervalMilliseconds}; diff --git a/core/src/types.cpp b/core/src/types.cpp index 76d376a..5698a53 100644 --- a/core/src/types.cpp +++ b/core/src/types.cpp @@ -1,4 +1,5 @@ #include "netft/types.hpp" +#include "detail/time.hpp" #include #include @@ -21,6 +22,20 @@ void require_positive_finite(std::string_view name, double value) { } } +void require_safe_timeout(std::string_view name, double value, bool http = false) { + require_positive_finite(name, value); + if (detail::checked_duration(std::chrono::duration{value}) <= + detail::SteadyClock::duration::zero()) { + throw std::invalid_argument(std::string{name} + " is below the steady-clock resolution"); + } + static_cast( + detail::checked_deadline(detail::SteadyClock::now(), std::chrono::duration{value})); + if (http && std::ceil(static_cast(value) * 1000.0L) >= + static_cast(std::numeric_limits::max())) { + throw std::invalid_argument(std::string{name} + " is outside the HTTP timer range"); + } +} + void require_port(std::string_view name, int value) { if (value < 1 || value > 65535) { throw std::invalid_argument(std::string{name} + " must be in the range 1..65535"); @@ -56,12 +71,12 @@ void validate(const Config &config) { } require_port("rdt_port", config.rdt_port); require_port("http_port", config.http_port); - require_positive_finite("receive_timeout", config.receive_timeout.count()); - require_positive_finite("configuration_connect_timeout", - config.configuration_connect_timeout.count()); - require_positive_finite("configuration_timeout", config.configuration_timeout.count()); - require_positive_finite("reconnect_initial_delay", config.reconnect_initial_delay.count()); - require_positive_finite("reconnect_max_delay", config.reconnect_max_delay.count()); + require_safe_timeout("receive_timeout", config.receive_timeout.count()); + require_safe_timeout("configuration_connect_timeout", + config.configuration_connect_timeout.count(), true); + require_safe_timeout("configuration_timeout", config.configuration_timeout.count(), true); + require_safe_timeout("reconnect_initial_delay", config.reconnect_initial_delay.count()); + require_safe_timeout("reconnect_max_delay", config.reconnect_max_delay.count()); if (config.reconnect_max_delay < config.reconnect_initial_delay) { throw std::invalid_argument("reconnect_max_delay must not be below reconnect_initial_delay"); } From 09e045e56290c570f7f8d2fb52fc63515904dc2d Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:42:32 +0800 Subject: [PATCH 04/11] docs: explain bounded capture and legacy migration choices --- docs/api.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index 13b31b1..ae930c6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -109,11 +109,11 @@ If a callback raises, pyNetFT stops the run and stores the original exception. ` | `recovery_policy` | `RecoveryPolicy` | `RECONNECT` | Reconnect after recoverable failures or stop on the first fault. | | `calibration_override` | `Calibration \| None` | `None` | Complete independently verified calibration; `None` discovers it from the sensor. | -`Calibration(counts_per_force_unit, counts_per_torque_unit, force_unit, torque_unit)` is immutable. Both counts must be finite and positive. A calibration override is complete; force or torque values cannot be overridden independently. +`Calibration(counts_per_force_unit, counts_per_torque_unit, force_unit, torque_unit)` is immutable. Both counts must be finite and positive. In the unreleased candidate they must also keep every signed 32-bit raw count representable after conversion. A calibration override is complete; force or torque values cannot be overridden independently. `SensorConfiguration(product_name, calibration, source, revision)` is immutable. `source` distinguishes sensor discovery from a caller override. `revision` changes when an effective configuration change is observed. -Validation occurs when `Config` and `queue_size` are passed to `Client`. `queue_size` must be greater than zero. `sensor_host` must contain a non-whitespace host, and both ports must be in `1..65535`. All five configured timeout and reconnect-delay values must be finite and greater than zero; `reconnect_max_delay` must be at least `reconnect_initial_delay`. `sample_rate_limit_hz` must be finite and non-negative. A calibration override must have finite positive counts and neither unit may be `UNKNOWN`. +Validation occurs when `Config` and `queue_size` are passed to `Client`. `queue_size` must be greater than zero. `sensor_host` must contain a non-whitespace host, and both ports must be in `1..65535`. All five configured timeout and reconnect-delay values must be finite and greater than zero; the unreleased candidate also checks native clock resolution/range and HTTP millisecond range; `reconnect_max_delay` must be at least `reconnect_initial_delay`. `sample_rate_limit_hz` must be finite and non-negative. A calibration override must have finite positive counts and neither unit may be `UNKNOWN`. The per-read timeout accepted by `samples()` must be finite, non-negative, and representable by the native monotonic clock; invalid values raise `ConfigurationError`. `wait_for_first_sample()` applies the same timeout validation. @@ -184,3 +184,22 @@ NetFTError Its methods are `connect()`, `disconnect()`, `bias()`, `get_data()`, `get_converted_data()`, and `start_streaming(duration=10, delay=0.1, print_data=True)`. Reading or biasing before `connect()` raises `ConnectionError`. `Response` has mutable `rdt_sequence`, `ft_sequence`, `status`, and list-valued `FTData` fields. See the [2.0 migration guide](https://github.com/netft/pyNetFT/blob/main/docs/migration-2.md) for exact replacements. + +## Choose latest-value access or capture + +For displays and feedback that need current data, use `queue_size=1` and read `latest_sample()`; this does not consume the iterator queue. A latest-value view intentionally skips intermediate measurements. For capture, give one iterator consumer a queue sized for the longest expected consumer stall: delivered rate multiplied by stall seconds, plus headroom. At a synthetic 2000 samples/s and a 0.5 s stall, 4096 entries provides headroom; this is a sizing example, not a hardware specification. + +```python +from itertools import islice +from pynetft import Client, Config + +with Client(Config(sensor_host="192.168.1.1"), queue_size=4096) as client: + captured = list(islice(client.samples(timeout=1.0), 1000)) + health = client.health() + if health.python_queue_dropped_count or health.lost_count: + raise RuntimeError("capture contains queue drops or missing RDT packets") +``` + +This example bounds retained samples and every iterator wait. Increasing queue size cannot eliminate network loss or sustain a consumer slower than the producer indefinitely. Inspect rate limiting, device status, malformed/out-of-order counters and recorded sequence numbers for your capture criteria. `python_queue_dropped_count` measures delivery-queue eviction; `lost_count` measures the native RDT sequence observation. Neither alone proves a complete measurement history. + +Legacy users should migrate `connect`/`disconnect` to `Client` context management, `get_data` to `samples` for capture, and `get_converted_data` to `Sample.wrench`. `latest_sample()` is suitable for polling the newest value. Preserve units explicitly: legacy manual calibration uses N/Nmm, whereas discovered samples carry the configured units. See the migration guide linked above for the full compatibility policy. From cdf4c79248b7926de0c064d7a418a84547a4caa9 Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:51:41 +0800 Subject: [PATCH 05/11] docs: update seven-repository synchronization guidance --- CONTRIBUTING.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0f1806..48fcd09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,13 +85,17 @@ Pass the authorized address only through `NETFT_SENSOR_HOST`. Never commit it. T ## Cross-repository synchronization -The `netft` organization maintains three separate repositories: +The `netft` organization maintains seven separate repositories: - `netft-cpp` is the standalone C++ core and source of truth for shared protocol behavior; - `pyNetFT` contains a pinned private core snapshot, Python bindings, and the Python public API; and -- `ros-netft` contains its own private core snapshot and ROS integration. +- `ros-netft` contains its own private core snapshot and ROS integration; +- `netft-cli` owns terminal workflows and its reviewed core adaptation; +- `netft-viewer` owns desktop interaction, companion and packaging; +- `netft-docs` maintains shared user guides and version-pinned references at https://netft.dev; and +- `.github` maintains organization navigation, common templates and issue routing. -There is no automatic runtime or build-time dependency between these repositories. Synchronize a released core into each consumer through separate, reviewable pull requests. Record the exact upstream tag and commit, port consumer-specific integration independently, run that repository's full supported tests, and document intentional differences. Do not claim the snapshots update in lockstep. +There is no automatic runtime or build-time dependency between these repositories. Synchronize a released core into each consumer through separate, reviewable pull requests. Record the exact upstream tag and commit, port consumer-specific integration independently, run the necessary checks for the affected contract and supported release platforms, and document intentional differences. Do not claim the snapshots update in lockstep. ## Release workflow From e973c0c5c6da8f59d7f0ae68a5ad1d28a3050e4d Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:56:47 +0800 Subject: [PATCH 06/11] docs: consolidate unreleased change notes --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2057bc..e1a5dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project are documented in this file. ## Unreleased +- Integrate the verified unpublished SDK candidate's calibration and checked-time protections. +- Support exact-commit snapshot synchronization and pin reviewed build actions. +- Explain latest-value versus bounded capture queues, drop counters, legacy migration and seven-repository maintenance. + ## 2.1.1 - 2026-08-14 ### Fixed From 3b49989f0061b9b253d2015f0036cae467319bea Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:44:50 +0800 Subject: [PATCH 07/11] Preserve snapshot bytes under Windows line-ending conversion [skip ci] --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9506cd5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Preserve the byte inventory when Windows enables core.autocrlf. +/core/** -text From b969ad2241318c0a82519fc2f7d7a10a6b01dd76 Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:50:17 +0800 Subject: [PATCH 08/11] Verify native snapshot bytes before platform wheel builds --- .github/workflows/wheels.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3bbdecd..5a2a955 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -46,6 +46,8 @@ jobs: with: python-version: "3.14" cache: pip + - name: Verify controlled core snapshot + run: python tools/sync_core.py verify - name: Install wheel build frontend run: python -m pip install cibuildwheel==3.4.1 auditwheel - name: Build CPython 3.10 x86_64 wheel @@ -85,6 +87,8 @@ jobs: with: python-version: "3.14" cache: pip + - name: Verify controlled core snapshot + run: python tools/sync_core.py verify - name: Install wheel build frontend run: python -m pip install cibuildwheel==3.4.1 delocate - name: Build CPython 3.10 native macOS wheel @@ -116,6 +120,8 @@ jobs: with: python-version: "3.14" cache: pip + - name: Verify controlled core snapshot + run: python tools/sync_core.py verify - name: Install wheel build frontend run: python -m pip install cibuildwheel==3.4.1 pefile - name: Build CPython 3.10 Windows wheel @@ -154,6 +160,8 @@ jobs: with: python-version: "3.14" cache: pip + - name: Verify controlled core snapshot + run: python tools/sync_core.py verify - name: Install wheel build frontend run: python -m pip install cibuildwheel==3.4.1 auditwheel - name: Build supported wheels @@ -192,6 +200,8 @@ jobs: with: python-version: "3.14" cache: pip + - name: Verify controlled core snapshot + run: python tools/sync_core.py verify - name: Install wheel build frontend run: python -m pip install cibuildwheel==3.4.1 delocate - name: Build supported native macOS wheels @@ -222,6 +232,8 @@ jobs: with: python-version: "3.14" cache: pip + - name: Verify controlled core snapshot + run: python tools/sync_core.py verify - name: Install wheel build frontend run: python -m pip install cibuildwheel==3.4.1 pefile - name: Build supported Windows wheels From 2f0d7291eb1f8c991d10592becbfed977c632059 Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:04:20 +0800 Subject: [PATCH 09/11] Diagnose snapshot drift and allow focused Windows wheel acceptance [skip ci] --- .github/workflows/wheels.yml | 14 ++++++++++---- tools/sync_core.py | 10 +++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5a2a955..07668c0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -5,6 +5,11 @@ on: push: branches: [main] workflow_dispatch: + inputs: + windows_only: + description: Build only the Windows CPython 3.10 smoke wheel + type: boolean + default: false permissions: contents: read @@ -15,6 +20,7 @@ concurrency: jobs: curl-build: + if: ${{ !inputs.windows_only }} runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -109,7 +115,7 @@ jobs: if-no-files-found: error windows-smoke: - if: github.event_name == 'pull_request' + if: ${{ always() && !cancelled() && ((github.event_name == 'pull_request' && needs.curl-build.result == 'success') || inputs.windows_only) }} needs: curl-build runs-on: windows-2025 steps: @@ -141,7 +147,7 @@ jobs: if-no-files-found: error wheels: - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && !inputs.windows_only needs: curl-build strategy: fail-fast: false @@ -181,7 +187,7 @@ jobs: if-no-files-found: error macos-wheels: - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && !inputs.windows_only needs: curl-build strategy: fail-fast: false @@ -221,7 +227,7 @@ jobs: if-no-files-found: error windows-wheels: - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && !inputs.windows_only needs: curl-build runs-on: windows-2025 steps: diff --git a/tools/sync_core.py b/tools/sync_core.py index 094ebbe..992acdd 100644 --- a/tools/sync_core.py +++ b/tools/sync_core.py @@ -43,7 +43,15 @@ def verify(root: Path) -> None: f"{digest(path)} {path.relative_to(root).as_posix()}" for path in snapshot_files(root) ] if actual != expected: - raise SystemExit("core snapshot checksum mismatch") + expected_by_path = {line.split(" ", 1)[1]: line.split(" ", 1)[0] for line in expected} + actual_by_path = {line.split(" ", 1)[1]: line.split(" ", 1)[0] for line in actual} + differences = [ + f"{name}: expected={expected_by_path.get(name, 'missing')} " + f"actual={actual_by_path.get(name, 'missing')}" + for name in sorted(expected_by_path.keys() | actual_by_path.keys()) + if expected_by_path.get(name) != actual_by_path.get(name) + ] + raise SystemExit("core snapshot checksum mismatch\n" + "\n".join(differences)) def sync(source: Path, destination: Path, tag: str | None, *, candidate: str | None = None) -> None: From a7a30188cae954cd8ec02247463f300f3cd5cdbe Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:05:45 +0800 Subject: [PATCH 10/11] Use platform-independent ordering for controlled snapshot manifests [skip ci] --- tests/quality/test_sync_core.py | 18 ++++++++++++++++++ tools/sync_core.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/quality/test_sync_core.py b/tests/quality/test_sync_core.py index a4af60f..2fafda7 100644 --- a/tests/quality/test_sync_core.py +++ b/tests/quality/test_sync_core.py @@ -47,3 +47,21 @@ def test_sync_removes_files_outside_the_selected_snapshot( sync_core.verify(destination) assert "tag=unreleased" in (destination / "UPSTREAM").read_text() + + +def test_snapshot_manifest_order_is_platform_independent(tmp_path: Path) -> None: + from pathlib import PureWindowsPath + + names = ["cmake/config.in", "UPSTREAM", "LICENSE", "CMakeLists.txt"] + for name in names: + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"snapshot\n") + # Windows Path comparison folds case; a Linux-generated manifest must not. + assert sorted(map(PureWindowsPath, names)) != sorted( + map(PureWindowsPath, names), key=lambda path: path.as_posix() + ) + sync_core.write_manifest(tmp_path) + recorded = (tmp_path / "SNAPSHOT.sha256").read_text().splitlines() + assert [line.split(" ", 1)[1] for line in recorded] == sorted(names) + sync_core.verify(tmp_path) diff --git a/tools/sync_core.py b/tools/sync_core.py index 992acdd..7685d70 100644 --- a/tools/sync_core.py +++ b/tools/sync_core.py @@ -26,7 +26,8 @@ def digest(path: Path) -> str: def snapshot_files(root: Path) -> list[Path]: return sorted( - path for path in root.rglob("*") if path.is_file() and path.name != "SNAPSHOT.sha256" + (path for path in root.rglob("*") if path.is_file() and path.name != "SNAPSHOT.sha256"), + key=lambda path: path.relative_to(root).as_posix(), ) From 91f2231c6160be7056048b80583be326294abcf2 Mon Sep 17 00:00:00 2001 From: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:31:31 +0800 Subject: [PATCH 11/11] fix: prepare Python 2.1.2 with published SDK 0.3.4 --- CHANGELOG.md | 4 +++- CMakeLists.txt | 2 +- CONTRIBUTING.md | 4 ++-- core/CMakeLists.txt | 2 +- core/SNAPSHOT.sha256 | 4 ++-- core/UPSTREAM | 4 ++-- docs/api.md | 4 ++-- pixi.toml | 4 ++-- pyproject.toml | 2 +- tests/artifact/test_wheel.py | 2 +- tests/python/test_import.py | 2 +- 11 files changed, 18 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a5dbd..cbc20a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ All notable changes to this project are documented in this file. ## Unreleased -- Integrate the verified unpublished SDK candidate's calibration and checked-time protections. +## 2.1.2 - 2026-10-08 + +- Integrate netft-cpp 0.3.4's calibration and checked-time protections. - Support exact-commit snapshot synchronization and pin reviewed build actions. - Explain latest-value versus bounded capture queues, drop counters, legacy migration and seven-repository maintenance. diff --git a/CMakeLists.txt b/CMakeLists.txt index a2b3116..6626d27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(pynetft VERSION 2.1.1 LANGUAGES CXX) +project(pynetft VERSION 2.1.2 LANGUAGES CXX) option(PYNETFT_BUILD_TESTING "Build pyNetFT native tests" OFF) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48fcd09..c617bfb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,10 +131,10 @@ All contributions are submitted under the [Apache License 2.0](https://github.co ### Current core candidate -The private snapshot uses unpublished upstream commit `91f012c5d6f9b63902765ccbec3437cb286c15e1`; `UPSTREAM` marks it `unreleased`. It is not the published v0.3.3 snapshot. Update from a clean upstream checkout using: +The private snapshot uses the exact netft-cpp `v0.3.4` release commit `1ce68a08a9387d9a36d22c3c259e44533199c347`; `UPSTREAM` and its content manifest record that release identity. Update from a clean upstream checkout using: ```sh -python tools/sync_core.py sync --source /path/to/netft-cpp --commit 91f012c5d6f9b63902765ccbec3437cb286c15e1 +python tools/sync_core.py sync --source /path/to/netft-cpp --tag v0.3.4 python tools/sync_core.py verify ``` diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index b24722c..e7a6ce4 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(netft VERSION 0.3.3 LANGUAGES CXX) +project(netft VERSION 0.3.4 LANGUAGES CXX) include(CTest) include(GNUInstallDirs) diff --git a/core/SNAPSHOT.sha256 b/core/SNAPSHOT.sha256 index cfc2959..84f2eab 100644 --- a/core/SNAPSHOT.sha256 +++ b/core/SNAPSHOT.sha256 @@ -1,6 +1,6 @@ -3db6fdfbb62d6073fa3f77784235cfe2eff06a1f102fb1d0f3a3f6da02dafa01 CMakeLists.txt +b2329e8fa84798edb7ddf912d6ea79455bd8181d607b28ae94297e2f409398f4 CMakeLists.txt c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 LICENSE -7b27434afaf998a250f54302fc7ead8416d0bfb550147cbc4c87858f64318864 UPSTREAM +ce471ce02972730df08fb35bc3c3ea60346dc4e79f7780ad1d55126d1cb61395 UPSTREAM 40701f04e67bb18052b6c8f1db60cf5d41baab09e5396f5a88c1eb14ad1a7139 cmake/netftConfig.cmake.in e4a52fe7c7e13de95a29bdbd688d295a4a8ddc1805276274b0c14d4092f7253d include/netft/client.hpp 4f7fb383da5456fd8477e1981f4ff3eba9b82ec3a98f3c395e7d482faf75c112 include/netft/discovery.hpp diff --git a/core/UPSTREAM b/core/UPSTREAM index e643a8f..a257e23 100644 --- a/core/UPSTREAM +++ b/core/UPSTREAM @@ -1,4 +1,4 @@ repository=https://github.com/netft/netft-cpp -tag=unreleased -commit=91f012c5d6f9b63902765ccbec3437cb286c15e1 +tag=v0.3.4 +commit=1ce68a08a9387d9a36d22c3c259e44533199c347 paths=CMakeLists.txt,LICENSE,cmake,include,src diff --git a/docs/api.md b/docs/api.md index ae930c6..5032b27 100644 --- a/docs/api.md +++ b/docs/api.md @@ -109,11 +109,11 @@ If a callback raises, pyNetFT stops the run and stores the original exception. ` | `recovery_policy` | `RecoveryPolicy` | `RECONNECT` | Reconnect after recoverable failures or stop on the first fault. | | `calibration_override` | `Calibration \| None` | `None` | Complete independently verified calibration; `None` discovers it from the sensor. | -`Calibration(counts_per_force_unit, counts_per_torque_unit, force_unit, torque_unit)` is immutable. Both counts must be finite and positive. In the unreleased candidate they must also keep every signed 32-bit raw count representable after conversion. A calibration override is complete; force or torque values cannot be overridden independently. +`Calibration(counts_per_force_unit, counts_per_torque_unit, force_unit, torque_unit)` is immutable. Both counts must be finite and positive. Since 2.1.2 they must also keep every signed 32-bit raw count representable after conversion. A calibration override is complete; force or torque values cannot be overridden independently. `SensorConfiguration(product_name, calibration, source, revision)` is immutable. `source` distinguishes sensor discovery from a caller override. `revision` changes when an effective configuration change is observed. -Validation occurs when `Config` and `queue_size` are passed to `Client`. `queue_size` must be greater than zero. `sensor_host` must contain a non-whitespace host, and both ports must be in `1..65535`. All five configured timeout and reconnect-delay values must be finite and greater than zero; the unreleased candidate also checks native clock resolution/range and HTTP millisecond range; `reconnect_max_delay` must be at least `reconnect_initial_delay`. `sample_rate_limit_hz` must be finite and non-negative. A calibration override must have finite positive counts and neither unit may be `UNKNOWN`. +Validation occurs when `Config` and `queue_size` are passed to `Client`. `queue_size` must be greater than zero. `sensor_host` must contain a non-whitespace host, and both ports must be in `1..65535`. All five configured timeout and reconnect-delay values must be finite and greater than zero; this version also checks native clock resolution/range and HTTP millisecond range; `reconnect_max_delay` must be at least `reconnect_initial_delay`. `sample_rate_limit_hz` must be finite and non-negative. A calibration override must have finite positive counts and neither unit may be `UNKNOWN`. The per-read timeout accepted by `samples()` must be finite, non-negative, and representable by the native monotonic clock; invalid values raise `ConfigurationError`. `wait_for_first_sample()` applies the same timeout validation. diff --git a/pixi.toml b/pixi.toml index 0363c54..9813b1e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -68,6 +68,6 @@ check = { depends-on = [ "native-tidy", ] } wheel = "python -m build --sdist --wheel --no-isolation" -wheel-check = "python tools/check_wheel.py dist/pynetft-2.1.1-*.whl" -sdist-check = "python tools/check_sdist.py dist/pynetft-2.1.1.tar.gz" +wheel-check = "python tools/check_wheel.py dist/pynetft-2.1.2-*.whl" +sdist-check = "python tools/check_sdist.py dist/pynetft-2.1.2.tar.gz" hardware-test = "python tools/hardware_test.py" diff --git a/pyproject.toml b/pyproject.toml index a0a7d32..48fde5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "scikit_build_core.build" [project] name = "pynetft" -version = "2.1.1" +version = "2.1.2" description = "Python client for ATI Net F/T Ethernet force/torque sensors" readme = "README.md" requires-python = ">=3.10" diff --git a/tests/artifact/test_wheel.py b/tests/artifact/test_wheel.py index 76ae632..f740e71 100644 --- a/tests/artifact/test_wheel.py +++ b/tests/artifact/test_wheel.py @@ -24,7 +24,7 @@ def test_native_extension_and_type_marker_are_installed() -> None: import pynetft from pynetft import _native - assert pynetft.__version__ == "2.1.1" + assert pynetft.__version__ == "2.1.2" assert _native.__version__ == pynetft.__version__ assert resources.files("pynetft").joinpath("py.typed").is_file() diff --git a/tests/python/test_import.py b/tests/python/test_import.py index cb064aa..dd89128 100644 --- a/tests/python/test_import.py +++ b/tests/python/test_import.py @@ -5,7 +5,7 @@ def test_installed_version_and_native_module_agree() -> None: - assert pynetft.__version__ == "2.1.1" + assert pynetft.__version__ == "2.1.2" assert version("pynetft") == pynetft.__version__ assert _native.__version__ == pynetft.__version__