From e008cc093317eee8f3a60bdde8e2e34cb03aae73 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 1/2] docs: clarify repository ownership and upstream-first fixes --- profile/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/profile/README.md b/profile/README.md index 51ee58e..b77032a 100644 --- a/profile/README.md +++ b/profile/README.md @@ -37,6 +37,10 @@ Component repositories own source, releases, version-specific notes, and contribution instructions. Shared user guides and references live at [netft.dev](https://netft.dev). +The organization contains seven repositories: five runtime components above, +[netft-docs](https://github.com/netft/netft-docs) for shared user documentation, +and [.github](https://github.com/netft/.github) for organization navigation and policies. + ## How the projects fit together ![ATI Net F/T sensor connected to the Net F/T core and four downstream products](assets/ecosystem.svg) @@ -66,7 +70,11 @@ architecture, language, and middleware combinations. ## Support and contributions Report bugs, request features, or ask project-specific questions in the issue -tracker of the relevant repository. Contributions are welcome through pull +tracker of the relevant repository. Protocol, calibration and recovery defects +belong in `netft-cpp`; binding, CLI, ROS or Viewer behavior belongs in its consumer +repository. Shared guide and reference corrections belong in `netft-docs`, and +organization navigation belongs in `.github`. SDK fixes precede verified downstream +snapshot upgrades; each consumer owns its adaptation and release validation. Contributions are welcome through pull requests; review that project's contribution guide before submitting one. --- From 5b356f3269fd5fff9dc76f5180c6f84412e51ab4 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 2/2] chore: add bounded probes, synthetic corpus and shared review templates --- .github/ISSUE_TEMPLATE/bug_report.md | 21 ++++++++ .github/pull_request_template.md | 11 ++++ CONTRIBUTING.md | 9 ++++ fixtures/protocol/README.md | 7 +++ fixtures/protocol/extreme.hex | 1 + fixtures/protocol/long.hex | 1 + fixtures/protocol/manifest.json | 78 ++++++++++++++++++++++++++++ fixtures/protocol/safe-scale.xml | 1 + fixtures/protocol/short.hex | 1 + fixtures/protocol/unsafe-scale.xml | 1 + tools/performance/README.md | 7 +++ tools/performance/chart.mjs | 21 ++++++++ tools/performance/corpus.cpp | 44 ++++++++++++++++ tools/performance/python.py | 30 +++++++++++ tools/performance/sdk.cpp | 57 ++++++++++++++++++++ 15 files changed, 290 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md create mode 100644 fixtures/protocol/README.md create mode 100644 fixtures/protocol/extreme.hex create mode 100644 fixtures/protocol/long.hex create mode 100644 fixtures/protocol/manifest.json create mode 100644 fixtures/protocol/safe-scale.xml create mode 100644 fixtures/protocol/short.hex create mode 100644 fixtures/protocol/unsafe-scale.xml create mode 100644 tools/performance/README.md create mode 100644 tools/performance/chart.mjs create mode 100644 tools/performance/corpus.cpp create mode 100644 tools/performance/python.py create mode 100644 tools/performance/sdk.cpp diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..87de939 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Report a reproducible component or cross-repository defect +title: '' +labels: '' +assignees: '' +--- + +## Behavior and reproduction + +Describe the expected and observed behavior, with a minimal reproduction. + +## Component and environment + +Include the component version/commit, OS/architecture, and ROS distribution if relevant. For a source build, include the vendored SDK UPSTREAM identity and any consumer adaptation. Include units, calibration revision and relevant health/drop counters when reporting acquisition or recording problems. + +Use synthetic samples and redact private addresses, credentials and laboratory recordings. Report security vulnerabilities through the component's SECURITY.md process. + +## Related components + +Link related issues when a shared-core defect affects multiple consumers. Protocol/calibration/recovery fixes start in netft-cpp; each downstream repository owns its snapshot/adaptation and release acceptance. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..c967b22 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Problem and resulting behavior + +Describe the trigger and observable change. + +## Cross-repository impact + +List affected consumers and the exact SDK source/manifest/patch identity if changed. Explain additive format fields, support or release dependencies; identify unreleased candidate behavior. + +## Necessary validation + +List the finite checks actually run and their results. Explain platform or external acceptance gaps. Do not rerun unrelated whole suites or claim skipped checks passed. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b2b74cb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Cross-repository contributions + +The organization has five runtime components, netft-docs, and this navigation/policy repository. Component-specific contribution and security instructions take precedence for their code. Shared user documentation lives at https://netft.dev. + +Protocol/calibration/recovery changes start in netft-cpp. Each consumer maintainer verifies its exact SDK commit, inventory hash, license and private adaptation before release. CLI owns its bias/socket-retention adaptation; Python, ROS and Viewer own delivery, realtime and packaging boundaries respectively. Documentation maintainers verify source versions and candidate/released identities before regenerating references. + +Release SDK fixes first, then consumers against the approved release commit, then version-pinned references and organization navigation. Keep repositories independently versioned. A coordinated change description must identify affected repositories, owners by component, source commits, necessary finite validation, compatibility effects and remaining platform acceptance. Use the common issue/PR templates where a repository does not supply its own. + +Report source and product defects to the relevant repository as described in profile/README.md. Shared guide/reference issues go to netft-docs; navigation/policy issues go here. Consult the component compatibility table for exact support commitments. Do not infer production signing, deployment permissions or all-platform support from a single local build. diff --git a/fixtures/protocol/README.md b/fixtures/protocol/README.md new file mode 100644 index 0000000..607ddd7 --- /dev/null +++ b/fixtures/protocol/README.md @@ -0,0 +1,7 @@ +# Synthetic fault corpus + +Schema 1 contains synthetic inputs only. The three hex files are exact RDT bytes (36/35/37 bytes), including signed count extremes and sequence rollover. XML fixtures distinguish unsafe finite positive scales from valid small scales. `manifest.json` defines expected transport/calibration semantics and separate consumer-level queue/file failures. No private hardware capture is included. + +All four downstream consumers carry the same verified SDK source, so its protocol/discovery regressions define decoding expectations. Existing relevant cases: SDK `Protocol.DecodesTwoComplementAxisBoundaries`, `Protocol.RejectsNullAndMalformedRecords`, `Calibration.RejectsScalesThatOverflowRawRange`, `RdtSequence.HandlesFirstContiguousRolloverGapsAndOrdering`; Python integration stream/recovery and bounded queue tests; CLI OutputFile/Recorder cases; ROS SI-overflow fault; Viewer Recorder pause/overflow/finalization cases. File-failure entries describe injected conditions, not network byte strings. + +Use these inputs for reproductions or future consumer regressions. This corpus does not force repositories to download another repository at test time and does not replace their fake sensors. `tools/performance/` probes remain opt-in. Keep additions minimal and map every expected behavior to a component's observable contract. diff --git a/fixtures/protocol/extreme.hex b/fixtures/protocol/extreme.hex new file mode 100644 index 0000000..ae219c3 --- /dev/null +++ b/fixtures/protocol/extreme.hex @@ -0,0 +1 @@ +ffffffffffffffff00000000800000007fffffff00000000ffffffff0000000100000064 diff --git a/fixtures/protocol/long.hex b/fixtures/protocol/long.hex new file mode 100644 index 0000000..c84f5db --- /dev/null +++ b/fixtures/protocol/long.hex @@ -0,0 +1 @@ +ffffffffffffffff00000000800000007fffffff00000000ffffffff000000010000006400 diff --git a/fixtures/protocol/manifest.json b/fixtures/protocol/manifest.json new file mode 100644 index 0000000..01dec61 --- /dev/null +++ b/fixtures/protocol/manifest.json @@ -0,0 +1,78 @@ +{ + "schema_version": 1, + "synthetic": true, + "rdt": [ + { + "file": "extreme.hex", + "accepted": true, + "raw_axes": [ + -2147483648, + 2147483647, + 0, + -1, + 1, + 100 + ], + "rdt_sequence": 4294967295 + }, + { + "file": "short.hex", + "accepted": false + }, + { + "file": "long.hex", + "accepted": false + } + ], + "calibration": [ + { + "file": "unsafe-scale.xml", + "accepted": false, + "fault": "SensorConfiguration/DiscoveryError" + }, + { + "file": "safe-scale.xml", + "accepted": true + } + ], + "sequences": [ + { + "values": [ + 4294967294, + 4294967295, + 0, + 1 + ], + "lost": 0, + "out_of_order": 0 + }, + { + "values": [ + 1, + 3, + 2, + 3 + ], + "lost": 1, + "out_of_order": 1, + "duplicate": 1 + } + ], + "local_failures": [ + { + "case": "queue-full", + "cli": "recording exit 8; retain partial", + "viewer": "Error; stop acceptance; retain partial" + }, + { + "case": "data-final-created-before-promotion", + "cli": "I/O exit 6; do not replace destination", + "viewer": "Error under Refuse; do not replace destination" + }, + { + "case": "metadata-write-failed", + "cli": "I/O exit 6; preserve completed data", + "viewer": "Error; preserve completed CSV" + } + ] +} diff --git a/fixtures/protocol/safe-scale.xml b/fixtures/protocol/safe-scale.xml new file mode 100644 index 0000000..cc60bf5 --- /dev/null +++ b/fixtures/protocol/safe-scale.xml @@ -0,0 +1 @@ +synthetic1e-2981e-298NN-m diff --git a/fixtures/protocol/short.hex b/fixtures/protocol/short.hex new file mode 100644 index 0000000..66db6a8 --- /dev/null +++ b/fixtures/protocol/short.hex @@ -0,0 +1 @@ +ffffffffffffffff00000000800000007fffffff00000000ffffffff00000001000000 diff --git a/fixtures/protocol/unsafe-scale.xml b/fixtures/protocol/unsafe-scale.xml new file mode 100644 index 0000000..6f35ac1 --- /dev/null +++ b/fixtures/protocol/unsafe-scale.xml @@ -0,0 +1 @@ +synthetic1e-3001e-300NN-m diff --git a/tools/performance/README.md b/tools/performance/README.md new file mode 100644 index 0000000..4f91b80 --- /dev/null +++ b/tools/performance/README.md @@ -0,0 +1,7 @@ +# Bounded performance probes + +These opt-in probes are outside CI. Use the consumer's locked Pixi environment and already built SDK; never point the probes at hardware. Each SDK/Python probe runs exactly three synthetic rates (100/1000/7000 Hz), three one-second repetitions. The highest rate deliberately sleeps in the consumer to expose backlog. Chart uses six axes, a 60 s window, first/min/max/last aggregation per 33 ms (the current producer contract), and three repeats of 120 measured frames after prefill. + +Run `pixi run python /path/to/.github/tools/performance/python.py` in pyNetFT and `pixi run node /path/to/.github/tools/performance/chart.mjs` in Viewer. Build `sdk.cpp` with the SDK's existing `test/support/fake_sensor.cpp`, `fake_http_server.cpp`, private protocol source and built static SDK using its locked compiler, include directories and curl/pthread dependencies. The SDK library build type must be reported; this run used Debug SDK with an O2 harness. Output is JSON lines. Use an external 60 s process timeout. + +Callback handoff latency starts at the SDK receive timestamp; it excludes time queued in the kernel. Python age includes native queue and conversion. CPU and peak RSS include the fake producer and runtime, and peak RSS is a process high-water mark across repeats. Chart timings cover model append and six series/plot-array materializations, not GPU/canvas rasterization. Heap/RSS are observations, not leak evidence or per-sample allocation counts. No raw allocation count is claimed. These probes provide a finite baseline, not a hardware throughput certification or long-duration stability guarantee. diff --git a/tools/performance/chart.mjs b/tools/performance/chart.mjs new file mode 100644 index 0000000..d96f3ca --- /dev/null +++ b/tools/performance/chart.mjs @@ -0,0 +1,21 @@ +// Run from Viewer with its locked Node/Vite. Fixed three scenes, three repeats. +import {createRequire} from 'node:module'; +import {resolve} from 'node:path'; +import {performance} from 'node:perf_hooks'; +const require=createRequire(resolve('package.json')); +const {createServer}=await import(require.resolve('vite')); +const server=await createServer({server:{middlewareMode:true}}); +try { + const {ChartModel}=await server.ssrLoadModule('/app/renderer/model/chart-model.ts'); + const axes=['Fx','Fy','Fz','Tx','Ty','Tz']; + // PlotAggregator sends first/min/max/last per 33 ms, not raw sensor samples. + for(const rate of [100,1000,7000])for(let repeat=0;repeat<3;repeat++){ + const model=new ChartModel(60000);const n=Math.min(4,Math.ceil(rate*.033));let tick=0; + const batch=()=>({type:'plot_batch',payload:{axes:axes.map(axis=>({axis,points:Array.from({length:n},(_,i)=>({hostTimeNs:String(BigInt(tick*33000000+i*1000000)),value:Math.sin(tick+i)}))}))}}); + for(;tick<1819;tick++)model.append(batch()); + const durations=[];const cpu=process.cpuUsage();const start=performance.now(); + for(let f=0;f<120;f++,tick++){let at=performance.now();model.append(batch());for(const a of axes)model.series(a).map(p=>[p.timeMs,p.value]);durations.push(performance.now()-at);} + durations.sort((a,b)=>a-b);const used=process.cpuUsage(cpu);const p=q=>durations[Math.min(durations.length-1,Math.floor(q*durations.length))]; + console.log(JSON.stringify({rate,repeat,points:model.pointCount,p50_ms:p(.5),p95_ms:p(.95),p99_ms:p(.99),cpu_ms:(used.user+used.system)/1000,elapsed_ms:performance.now()-start,rss_bytes:process.memoryUsage().rss,heap_bytes:process.memoryUsage().heapUsed})); + } +}finally{await server.close();} diff --git a/tools/performance/corpus.cpp b/tools/performance/corpus.cpp new file mode 100644 index 0000000..10c5f38 --- /dev/null +++ b/tools/performance/corpus.cpp @@ -0,0 +1,44 @@ +#include "detail/protocol.hpp" +#include "detail/xml_config.hpp" +#include +#include +#include +#include +#include +int main(int argc, char **argv) { + if (argc != 2) + return 2; + std::string root = argv[1]; + for (auto name : {"extreme", "short", "long"}) { + std::ifstream f(root + "/" + name + ".hex"); + std::string hex; + f >> hex; + std::vector data; + for (size_t i = 0; i < hex.size(); i += 2) + data.push_back(static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16))); + bool accepted = false; + try { + auto r = netft::detail::decode_record(data.data(), data.size()); + accepted = true; + if (r.fx != INT32_MIN || r.fy != INT32_MAX) + return 1; + } catch (const netft::detail::ProtocolError &) { + } + if (accepted != (std::string(name) == "extreme")) + return 1; + } + for (auto name : {"unsafe-scale", "safe-scale"}) { + std::ifstream f(root + "/" + name + ".xml"); + std::string xml{std::istreambuf_iterator(f), {}}; + bool accepted = false; + try { + netft::detail::parse_sensor_configuration(xml); + accepted = true; + } catch (const std::exception &) { + } + if (accepted != (std::string(name) == "safe-scale")) + return 1; + } + std::cout << "5 corpus decoding/calibration cases passed\n"; +} diff --git a/tools/performance/python.py b/tools/performance/python.py new file mode 100644 index 0000000..a5c38c6 --- /dev/null +++ b/tools/performance/python.py @@ -0,0 +1,30 @@ +"""Fixed nine-second loopback baseline; only talks to its own UDP fake sensor.""" +import json,resource,socket,struct,threading,time,statistics +from pynetft import Client,Config,Calibration,ForceUnit,TorqueUnit +for rate in (100,1000,7000): + for repeat in range(3): + sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);sock.bind(('127.0.0.1',0));sock.settimeout(.05) + done=threading.Event() + def producer(): + peer=None;sequence=0;next_at=time.monotonic() + while not done.is_set(): + if peer is None: + try: + request,address=sock.recvfrom(64) + if request[2:4]==b'\x00\x02':peer=address + except socket.timeout:continue + sequence+=1;sock.sendto(struct.pack('>III6i',sequence,sequence,0,100,-200,300,10,-20,30),peer) + next_at+=1/rate;done.wait(max(0,next_at-time.monotonic())) + worker=threading.Thread(target=producer);worker.start() + config=Config(sensor_host='127.0.0.1',rdt_port=sock.getsockname()[1],calibration_override=Calibration(1e6,1e6,ForceUnit.NEWTON,TorqueUnit.NEWTON_METER)) + client=Client(config,queue_size=128);age=[];cpu=time.process_time();start=time.monotonic() + try: + client.start();samples=client.samples(timeout=.2) + while time.monotonic()-start<1: + s=next(samples);age.append((time.monotonic_ns()-s.received_at_ns)/1000) + if rate==7000:time.sleep(.0005) + health=client.health();stop=time.monotonic();client.stop();stop_ms=(time.monotonic()-stop)*1000 + finally: + client.stop();done.set();worker.join(1);sock.close() + age.sort();p=lambda q:age[min(len(age)-1,int(q*len(age)))] if age else 0 + print(json.dumps(dict(rate=rate,repeat=repeat,samples=len(age),p50_us=p(.5),p95_us=p(.95),p99_us=p(.99),cpu_percent=100*(time.process_time()-cpu)/(time.monotonic()-start),rss_peak_kib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss,queue_drops=health.python_queue_dropped_count,lost=health.lost_count,stop_ms=stop_ms))) diff --git a/tools/performance/sdk.cpp b/tools/performance/sdk.cpp new file mode 100644 index 0000000..e341f47 --- /dev/null +++ b/tools/performance/sdk.cpp @@ -0,0 +1,57 @@ +#include "netft/client.hpp" +#include "support/fake_sensor.hpp" +#include +#include +#include +#include +#include +#include +#include +using Clock = std::chrono::steady_clock; +int main() { + for (int rate : {100, 1000, 7000}) + for (int repeat = 0; repeat < 3; ++repeat) { + netft::test::FakeSensor sensor(rate); + netft::Config config; + config.sensor_host = sensor.host(); + config.rdt_port = sensor.rdt_port(); + config.http_port = sensor.http_port(); + netft::Client client(config); + std::vector age; + age.reserve(9000); + auto cpu = std::clock(); + auto start = Clock::now(); + client.start([&](const netft::Sample &s) { + age.push_back(std::chrono::duration(Clock::now() - + s.received_at) + .count()); + if (rate == 7000) + std::this_thread::sleep_for(std::chrono::microseconds(200)); + }); + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto stop = Clock::now(); + client.stop(); + const auto stop_ms = + std::chrono::duration(Clock::now() - stop) + .count(); + auto h = client.health(); + auto cpu_pct = + 100.0 * (std::clock() - cpu) / CLOCKS_PER_SEC / + std::chrono::duration(Clock::now() - start).count(); + std::sort(age.begin(), age.end()); + auto p = [&](double q) { + return age.empty() + ? 0 + : age[std::min(age.size() - 1, size_t(q * age.size()))]; + }; + struct rusage usage{}; + getrusage(RUSAGE_SELF, &usage); + std::cout << "{\"rate\":" << rate << ",\"repeat\":" << repeat + << ",\"samples\":" << age.size() << ",\"p50_us\":" << p(.50) + << ",\"p95_us\":" << p(.95) << ",\"p99_us\":" << p(.99) + << ",\"cpu_percent\":" << cpu_pct + << ",\"rss_peak_kib\":" << usage.ru_maxrss + << ",\"lost\":" << h.lost_count << ",\"stop_ms\":" << stop_ms + << "}\n"; + } +}