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
21 changes: 21 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions fixtures/protocol/README.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions fixtures/protocol/extreme.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ffffffffffffffff00000000800000007fffffff00000000ffffffff0000000100000064
1 change: 1 addition & 0 deletions fixtures/protocol/long.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ffffffffffffffff00000000800000007fffffff00000000ffffffff000000010000006400
78 changes: 78 additions & 0 deletions fixtures/protocol/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
1 change: 1 addition & 0 deletions fixtures/protocol/safe-scale.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<netft><prodname>synthetic</prodname><cfgcpf>1e-298</cfgcpf><cfgcpt>1e-298</cfgcpt><scfgfu>N</scfgfu><scfgtu>N-m</scfgtu></netft>
1 change: 1 addition & 0 deletions fixtures/protocol/short.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ffffffffffffffff00000000800000007fffffff00000000ffffffff00000001000000
1 change: 1 addition & 0 deletions fixtures/protocol/unsafe-scale.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<netft><prodname>synthetic</prodname><cfgcpf>1e-300</cfgcpf><cfgcpt>1e-300</cfgcpt><scfgfu>N</scfgfu><scfgtu>N-m</scfgtu></netft>
10 changes: 9 additions & 1 deletion profile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.

---
Expand Down
7 changes: 7 additions & 0 deletions tools/performance/README.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions tools/performance/chart.mjs
Original file line number Diff line number Diff line change
@@ -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();}
44 changes: 44 additions & 0 deletions tools/performance/corpus.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include "detail/protocol.hpp"
#include "detail/xml_config.hpp"
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
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<unsigned char> data;
for (size_t i = 0; i < hex.size(); i += 2)
data.push_back(static_cast<unsigned char>(
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<char>(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";
}
30 changes: 30 additions & 0 deletions tools/performance/python.py
Original file line number Diff line number Diff line change
@@ -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)))
57 changes: 57 additions & 0 deletions tools/performance/sdk.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#include "netft/client.hpp"
#include "support/fake_sensor.hpp"
#include <algorithm>
#include <chrono>
#include <ctime>
#include <iostream>
#include <sys/resource.h>
#include <thread>
#include <vector>
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<double> age;
age.reserve(9000);
auto cpu = std::clock();
auto start = Clock::now();
client.start([&](const netft::Sample &s) {
age.push_back(std::chrono::duration<double, std::micro>(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<double, std::milli>(Clock::now() - stop)
.count();
auto h = client.health();
auto cpu_pct =
100.0 * (std::clock() - cpu) / CLOCKS_PER_SEC /
std::chrono::duration<double>(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";
}
}