From 25713dc2225556b20101347580146f9820d5e831 Mon Sep 17 00:00:00 2001
From: Chad Austin <chad@chadaustin.me>
Date: Sun, 26 Dec 2021 18:44:56 -0800
Subject: [PATCH] Add sym_defer_pr debouncer type (#14948)

---
 data/schemas/keyboard.jsonschema              |   2 +-
 docs/feature_debounce_type.md                 |   1 +
 quantum/debounce/sym_defer_pr.c               |  72 ++++++
 quantum/debounce/tests/rules.mk               |   5 +
 quantum/debounce/tests/sym_defer_pr_tests.cpp | 223 ++++++++++++++++++
 quantum/debounce/tests/testlist.mk            |   1 +
 6 files changed, 303 insertions(+), 1 deletion(-)
 create mode 100644 quantum/debounce/sym_defer_pr.c
 create mode 100644 quantum/debounce/tests/sym_defer_pr_tests.cpp

diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema
index 308f9b782b..2daeaf04b4 100644
--- a/data/schemas/keyboard.jsonschema
+++ b/data/schemas/keyboard.jsonschema
@@ -69,7 +69,7 @@
             "properties": {
                 "debounce_type": {
                     "type": "string",
-                    "enum": ["custom", "eager_pk", "eager_pr", "sym_defer_pk", "sym_eager_pk"]
+                    "enum": ["custom", "eager_pk", "eager_pr", "sym_defer_pk", "sym_defer_pr", "sym_eager_pk"]
                 },
                 "firmware_format": {
                     "type": "string",
diff --git a/docs/feature_debounce_type.md b/docs/feature_debounce_type.md
index f37a785b1e..9cd736a24a 100644
--- a/docs/feature_debounce_type.md
+++ b/docs/feature_debounce_type.md
@@ -116,6 +116,7 @@ Where name of algorithm is one of:
 For use in keyboards where refreshing ```NUM_KEYS``` 8-bit counters is computationally expensive / low scan rate, and fingers usually only hit one row at a time. This could be
 appropriate for the ErgoDox models; the matrix is rotated 90°, and hence its "rows" are really columns, and each finger only hits a single "row" at a time in normal use.
 * ```sym_eager_pk``` - debouncing per key. On any state change, response is immediate, followed by ```DEBOUNCE``` milliseconds of no further input for that key
+* ```sym_defer_pr``` - debouncing per row. On any state change, a per-row timer is set. When ```DEBOUNCE``` milliseconds of no changes have occurred on that row, the entire row is pushed. Can improve responsiveness over `sym_defer_g` while being less susceptible than per-key debouncers to noise.
 * ```sym_defer_pk``` - debouncing per key. On any state change, a per-key timer is set. When ```DEBOUNCE``` milliseconds of no changes have occurred on that key, the key status change is pushed.
 * ```asym_eager_defer_pk``` - debouncing per key. On a key-down state change, response is immediate, followed by ```DEBOUNCE``` milliseconds of no further input for that key. On a key-up state change, a per-key timer is set. When ```DEBOUNCE``` milliseconds of no changes have occurred on that key, the key-up status change is pushed.
 
diff --git a/quantum/debounce/sym_defer_pr.c b/quantum/debounce/sym_defer_pr.c
new file mode 100644
index 0000000000..8b33acc6a2
--- /dev/null
+++ b/quantum/debounce/sym_defer_pr.c
@@ -0,0 +1,72 @@
+/*
+Copyright 2021 Chad Austin <chad@chadaustin.me>
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+Symmetric per-row debounce algorithm. Changes only apply when
+DEBOUNCE milliseconds have elapsed since the last change.
+*/
+
+#include "matrix.h"
+#include "timer.h"
+#include "quantum.h"
+#include <stdlib.h>
+
+#ifndef DEBOUNCE
+#    define DEBOUNCE 5
+#endif
+
+static uint16_t last_time;
+// [row] milliseconds until key's state is considered debounced.
+static uint8_t* countdowns;
+// [row]
+static matrix_row_t* last_raw;
+
+void debounce_init(uint8_t num_rows) {
+    countdowns = (uint8_t*)calloc(num_rows, sizeof(uint8_t));
+    last_raw   = (matrix_row_t*)calloc(num_rows, sizeof(matrix_row_t));
+
+    last_time = timer_read();
+}
+
+void debounce_free(void) {
+    free(countdowns);
+    countdowns = NULL;
+    free(last_raw);
+    last_raw = NULL;
+}
+
+void debounce(matrix_row_t raw[], matrix_row_t cooked[], uint8_t num_rows, bool changed) {
+    uint16_t now       = timer_read();
+    uint16_t elapsed16 = TIMER_DIFF_16(now, last_time);
+    last_time          = now;
+    uint8_t elapsed    = (elapsed16 > 255) ? 255 : elapsed16;
+
+    uint8_t* countdown = countdowns;
+
+    for (uint8_t row = 0; row < num_rows; ++row, ++countdown) {
+        matrix_row_t raw_row = raw[row];
+
+        if (raw_row != last_raw[row]) {
+            *countdown    = DEBOUNCE;
+            last_raw[row] = raw_row;
+        } else if (*countdown > elapsed) {
+            *countdown -= elapsed;
+        } else if (*countdown) {
+            cooked[row] = raw_row;
+            *countdown  = 0;
+        }
+    }
+}
+
+bool debounce_active(void) { return true; }
diff --git a/quantum/debounce/tests/rules.mk b/quantum/debounce/tests/rules.mk
index e908dd6f67..8318b1c668 100644
--- a/quantum/debounce/tests/rules.mk
+++ b/quantum/debounce/tests/rules.mk
@@ -28,6 +28,11 @@ debounce_sym_defer_pk_SRC := $(DEBOUNCE_COMMON_SRC) \
 	$(QUANTUM_PATH)/debounce/sym_defer_pk.c \
 	$(QUANTUM_PATH)/debounce/tests/sym_defer_pk_tests.cpp
 
+debounce_sym_defer_pr_DEFS := $(DEBOUNCE_COMMON_DEFS)
+debounce_sym_defer_pr_SRC := $(DEBOUNCE_COMMON_SRC) \
+	$(QUANTUM_PATH)/debounce/sym_defer_pr.c \
+	$(QUANTUM_PATH)/debounce/tests/sym_defer_pr_tests.cpp
+
 debounce_sym_eager_pk_DEFS := $(DEBOUNCE_COMMON_DEFS)
 debounce_sym_eager_pk_SRC := $(DEBOUNCE_COMMON_SRC) \
 	$(QUANTUM_PATH)/debounce/sym_eager_pk.c \
diff --git a/quantum/debounce/tests/sym_defer_pr_tests.cpp b/quantum/debounce/tests/sym_defer_pr_tests.cpp
new file mode 100644
index 0000000000..bb3333cf7b
--- /dev/null
+++ b/quantum/debounce/tests/sym_defer_pr_tests.cpp
@@ -0,0 +1,223 @@
+/* Copyright 2021 Simon Arlott
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "gtest/gtest.h"
+
+#include "debounce_test_common.h"
+
+TEST_F(DebounceTest, OneKeyShort1) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        {5, {}, {{0, 1, DOWN}}},
+        /* 0ms delay (fast scan rate) */
+        {5, {{0, 1, UP}}, {}},
+
+        {10, {}, {{0, 1, UP}}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyShort2) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        {5, {}, {{0, 1, DOWN}}},
+        /* 1ms delay */
+        {6, {{0, 1, UP}}, {}},
+
+        {11, {}, {{0, 1, UP}}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyShort3) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        {5, {}, {{0, 1, DOWN}}},
+        /* 2ms delay */
+        {7, {{0, 1, UP}}, {}},
+
+        {12, {}, {{0, 1, UP}}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyTooQuick1) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+        /* Release key exactly on the debounce time */
+        {5, {{0, 1, UP}}, {}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyTooQuick2) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        {5, {}, {{0, 1, DOWN}}},
+        {6, {{0, 1, UP}}, {}},
+
+        /* Press key exactly on the debounce time */
+        {11, {{0, 1, DOWN}}, {}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyBouncing1) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+        {1, {{0, 1, UP}}, {}},
+        {2, {{0, 1, DOWN}}, {}},
+        {3, {{0, 1, UP}}, {}},
+        {4, {{0, 1, DOWN}}, {}},
+        {5, {{0, 1, UP}}, {}},
+        {6, {{0, 1, DOWN}}, {}},
+        {11, {}, {{0, 1, DOWN}}}, /* 5ms after DOWN at time 7 */
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyBouncing2) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+        {5, {}, {{0, 1, DOWN}}},
+        {6, {{0, 1, UP}}, {}},
+        {7, {{0, 1, DOWN}}, {}},
+        {8, {{0, 1, UP}}, {}},
+        {9, {{0, 1, DOWN}}, {}},
+        {10, {{0, 1, UP}}, {}},
+        {15, {}, {{0, 1, UP}}}, /* 5ms after UP at time 10 */
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyLong) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        {5, {}, {{0, 1, DOWN}}},
+
+        {25, {{0, 1, UP}}, {}},
+
+        {30, {}, {{0, 1, UP}}},
+
+        {50, {{0, 1, DOWN}}, {}},
+
+        {55, {}, {{0, 1, DOWN}}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, TwoKeysShort) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+        {1, {{0, 2, DOWN}}, {}},
+
+        {6, {}, {{0, 1, DOWN}, {0, 2, DOWN}}},
+
+        {7, {{0, 1, UP}}, {}},
+        {8, {{0, 2, UP}}, {}},
+
+        {13, {}, {{0, 1, UP}, {0, 2, UP}}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, TwoKeysSimultaneous1) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}, {0, 2, DOWN}}, {}},
+
+        {5, {}, {{0, 1, DOWN}, {0, 2, DOWN}}},
+        {6, {{0, 1, UP}, {0, 2, UP}}, {}},
+
+        {11, {}, {{0, 1, UP}, {0, 2, UP}}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, TwoKeysSimultaneous2) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+        {1, {{0, 2, DOWN}}, {}},
+
+        {6, {}, {{0, 1, DOWN}, {0, 2, DOWN}}},
+        {7, {{0, 2, UP}}, {}},
+        {9, {{0, 1, UP}}, {}},
+
+        // Debouncing loses the specific ordering -- both events report simultaneously.
+        {14, {}, {{0, 1, UP}, {0, 2, UP}}},
+    });
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyDelayedScan1) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        /* Processing is very late */
+        {300, {}, {{0, 1, DOWN}}},
+        /* Immediately release key */
+        {300, {{0, 1, UP}}, {}},
+
+        {305, {}, {{0, 1, UP}}},
+    });
+    time_jumps_ = true;
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyDelayedScan2) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        /* Processing is very late */
+        {300, {}, {{0, 1, DOWN}}},
+        /* Release key after 1ms */
+        {301, {{0, 1, UP}}, {}},
+
+        {306, {}, {{0, 1, UP}}},
+    });
+    time_jumps_ = true;
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyDelayedScan3) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        /* Release key before debounce expires */
+        {300, {{0, 1, UP}}, {}},
+    });
+    time_jumps_ = true;
+    runEvents();
+}
+
+TEST_F(DebounceTest, OneKeyDelayedScan4) {
+    addEvents({ /* Time, Inputs, Outputs */
+        {0, {{0, 1, DOWN}}, {}},
+
+        /* Processing is a bit late */
+        {50, {}, {{0, 1, DOWN}}},
+        /* Release key after 1ms */
+        {51, {{0, 1, UP}}, {}},
+
+        {56, {}, {{0, 1, UP}}},
+    });
+    time_jumps_ = true;
+    runEvents();
+}
diff --git a/quantum/debounce/tests/testlist.mk b/quantum/debounce/tests/testlist.mk
index c54c45aa63..f7bd520698 100644
--- a/quantum/debounce/tests/testlist.mk
+++ b/quantum/debounce/tests/testlist.mk
@@ -1,6 +1,7 @@
 TEST_LIST += \
 	debounce_sym_defer_g \
 	debounce_sym_defer_pk \
+	debounce_sym_defer_pr \
 	debounce_sym_eager_pk \
 	debounce_sym_eager_pr \
 	debounce_asym_eager_defer_pk