6 Commits
Author SHA1 Message Date
Robin Anderson 8a90745e25 Merge pull request #31 from VioletTides/release-1.0.2
Number the release 1.0.2
2026-10-16 20:05:21 -04:00
Robin Anderson ff3f150e00 Number the release 1.0.2 2026-10-16 20:03:12 -04:00
Robin Anderson c6a7f8c2cc Merge pull request #30 from VioletTides/leg-knocks
Count knocks on a leg
2026-10-16 20:03:03 -04:00
Robin Anderson ab331f0a85 Say that surface knocks work on a leg 2026-10-16 19:59:18 -04:00
Robin Anderson 9a0d044d3c Let leg knocks in a pair land up to 6 cm apart
Two knocks on a leg spread wider than two knocks on a desk. In a run of
about 40 leg doubles, 7 pairs landed 3.5 to 8.3 cm apart, and the 3 cm
same-spot rule dropped them. At 6 cm, 25 doubles count instead of 20.
play, noise, and straps still give no actions at twice that distance.

tests/data/leg-doubles.csv.gz holds that run.
2026-10-16 19:42:32 -04:00
Robin Anderson e01afc9833 Count knocks on a leg as surface knocks
Knocks on a leg come in faster than knocks on a desk. Most right-hand
leg knocks moved faster than 1.8 m/s 30 ms before the spike, so the
detector threw them out as arm swings. Surface knocks now get their own
limit of 2.2 m/s. Together and headset knocks keep 1.8 m/s, because at
2.2 m/s a together calibration counted one knock in the noise recording.

tests/data/leg.csv.gz holds doubles and triples knocked on a leg with
each hand, headset worn. It gives 16 actions at 2.2 m/s and 9 at 1.8 m/s.
2026-10-16 19:32:42 -04:00
9 changed files with 23 additions and 8 deletions

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(KnockThru VERSION 1.0.1 LANGUAGES CXX)
project(KnockThru VERSION 1.0.2 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+4 -2
View File
@@ -52,6 +52,8 @@ Any of these three knocks works. Use the same kind for every knock in a set.
![Three ways to knock: knock the controllers together, knock one controller on a table, or knock on the headset. Each one knocks twice, then three times](images/knock-kinds.gif)
A surface knock also works on your leg. Keep the controller where the headset cameras can see it, because knocks out of view get missed more often.
Pause for a moment, then knock at a steady pace. After your last knock, KnockThru waits a moment in case another one follows.
![Timeline: two knocks, a wait of about 0.4 seconds, then passthrough turns on and both controllers buzz. Then three knocks, the same wait, and the style switches with two buzzes](images/timing.gif)
@@ -191,7 +193,7 @@ The log is `~/.config/knockthru/application.log`.
Copy the Linux ARM64 ZIP from the releases page to the Frame, then run:
```sh
unzip -oq knockthru-1.0.1-linux-arm64.zip -d ~/KnockThru
unzip -oq knockthru-1.0.2-linux-arm64.zip -d ~/KnockThru
~/KnockThru/knockthru --install
```
@@ -212,7 +214,7 @@ Steam Frame build (needs Zig 0.16.0) and release ZIP (needs Node.js):
```sh
scripts/build-arm64.sh
node scripts/package.mjs --build-dir build-arm64 --version 1.0.1
node scripts/package.mjs --build-dir build-arm64 --version 1.0.2
```
README images (needs Pillow, numpy, ffmpeg, and Chromium, after the host build):
+1 -1
View File
@@ -5,7 +5,7 @@ set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd)"
build="${1:-$root/build-arm64}"
version="${KNOCKTHRU_VERSION:-1.0.1}"
version="${KNOCKTHRU_VERSION:-1.0.2}"
# KNOCKTHRU_TEST_HOOK=1 builds in the test hook from src/app.cpp. Releases never set it.
test_hook="${KNOCKTHRU_TEST_HOOK:-}"
mkdir -p "$build/bin"
+2 -1
View File
@@ -73,7 +73,8 @@ void merge(AppConfig& config, const json& file, const fs::path& path) {
{"controller_peak", &d.controller_peak}, {"confirmed_peak", &d.confirmed_peak},
{"partner_peak", &d.partner_peak}, {"headset_peak", &d.headset_peak}, {"headset_knock_peak", &d.headset_knock_peak},
{"max_speed_after", &d.max_speed_after}, {"surface_speed_after", &d.surface_speed_after},
{"max_speed_before", &d.max_speed_before}, {"together_distance", &d.together_distance},
{"max_speed_before", &d.max_speed_before}, {"surface_speed_before", &d.surface_speed_before},
{"together_distance", &d.together_distance},
{"same_spot", &d.same_spot}, {"merge_s", &d.merge_s}, {"min_gap_s", &d.min_gap_s}, {"fast_gap_s", &d.fast_gap_s},
{"max_gap_s", &d.max_gap_s}, {"quiet_before_s", &d.quiet_before_s}};
if (!value.is_object()) fail("detector must be an object");
+1 -1
View File
@@ -116,7 +116,7 @@ std::optional<Knock> KnockDetector::judge(Device device, std::size_t i) const {
if (sample.accel < threshold) return std::nullopt;
const auto* before = nearest(history, sample.t - 0.03);
const auto* after = nearest(history, sample.t + 0.03);
if (!before || !after || norm(before->velocity) >= config_.max_speed_before ||
if (!before || !after || norm(before->velocity) >= (surface ? config_.surface_speed_before : config_.max_speed_before) ||
norm(after->velocity) >= (surface ? config_.surface_speed_after : config_.max_speed_after))
return std::nullopt;
return knock;
+2 -1
View File
@@ -25,8 +25,9 @@ struct DetectorConfig {
double max_speed_after = 0.4; // m/s, 30 ms after the spike
double surface_speed_after = 0.55; // m/s, table knocks rebound
double max_speed_before = 1.8; // m/s, 30 ms before the spike
double surface_speed_before = 2.2; // m/s, knocks on a leg come in faster
double together_distance = 0.15; // m, controller separation for a together knock
double same_spot = 0.03; // m, surface knocks in one sequence must land this close
double same_spot = 0.06; // m, surface knocks in one sequence must land this close. Leg knocks spread more than desk knocks.
double merge_s = 0.08; // spikes closer than this are one knock (bounces)
double min_gap_s = 0.15; // closer surface knocks are table bounces
double fast_gap_s = 0.105; // closer together or headset knocks are rebounds
Binary file not shown.
Binary file not shown.
+12 -1
View File
@@ -110,7 +110,8 @@ DetectorConfig test_config() {
{"min_approach_together", &config.min_approach[0]}, {"min_approach_surface", &config.min_approach[1]},
{"min_approach_headset", &config.min_approach[2]},
{"max_speed_after", &config.max_speed_after}, {"surface_speed_after", &config.surface_speed_after},
{"max_speed_before", &config.max_speed_before}, {"same_spot", &config.same_spot},
{"max_speed_before", &config.max_speed_before}, {"surface_speed_before", &config.surface_speed_before},
{"same_spot", &config.same_spot},
{"min_gap_s", &config.min_gap_s}, {"fast_gap_s", &config.fast_gap_s}, {"max_gap_s", &config.max_gap_s},
{"quiet_before_s", &config.quiet_before_s}};
std::stringstream list(set);
@@ -225,6 +226,16 @@ void replay_tests() {
check(surface["surfacex2"] >= 16, "surface: at least 16 surface doubles");
auto headset = replay("headset");
check(headset["headsetx2"] >= 15, "headset: at least 15 headset doubles");
// Doubles and triples knocked on a leg with each hand, headset worn. At
// surface_speed_before 1.8 most right-hand knocks were lost, giving 9 actions.
auto leg = replay("leg");
check(leg["surfacex2"] + leg["surfacex3"] >= 14 && total(leg) == leg["surfacex2"] + leg["surfacex3"],
"leg: at least 14 surface actions, nothing else");
// About 40 doubles knocked on a leg, a few seconds apart, mostly with the
// right hand. At same_spot 0.03, 20 counted.
auto leg_doubles = replay("leg-doubles");
check(leg_doubles["surfacex2"] >= 23 && total(leg_doubles) == leg_doubles["surfacex2"],
"leg-doubles: at least 23 surface doubles, nothing else");
check(total(replay("play")) == 0, "play: no actions");
check(total(replay("noise")) == 0, "noise: no actions");
// The controllers bumping while the wearer fit the strap on each one, in Into the Radius 2.