mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-11-10 20:17:30 +00:00
300 lines
12 KiB
C++
300 lines
12 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Execute the production driver against both documented M9 I2C addresses.
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
#include "m9_keyboard_fakes/Arduino.h"
|
|
#include "m9_keyboard_fakes/Wire.h"
|
|
#define HAS_M9_KEYBOARD 1
|
|
#define ESP32 1
|
|
#define PIN_KB_SDA 20
|
|
#define PIN_KB_SCL 21
|
|
uint32_t testMillis = 0;
|
|
TestSerial Serial;
|
|
TwoWire Wire, Wire1;
|
|
#include "../variants/thinknode_m9/M9Keyboard.cpp"
|
|
|
|
static void resetDriver() {
|
|
Wire = TwoWire{};
|
|
Wire1 = TwoWire{};
|
|
Serial.output.clear();
|
|
s_bus = nullptr;
|
|
s_inited = false;
|
|
s_head = s_tail = 0;
|
|
s_guard = M9KeyGuard{};
|
|
// Keep time monotonic across fixtures, including the poll's local timer.
|
|
testMillis += 2000;
|
|
}
|
|
|
|
static void checkController(uint8_t address, bool sensorBus) {
|
|
std::fprintf(stderr, "Testing address 0x%02x on %s\n", address, sensorBus ? "Wire" : "Wire1");
|
|
resetDriver();
|
|
TwoWire& keyboard = sensorBus ? Wire : Wire1;
|
|
keyboard.device = address;
|
|
m9KeyboardBegin();
|
|
assert(Wire1.sda == 20 && Wire1.scl == 21);
|
|
// One read per poll (#625: repeat reads in one poll made keys type twice), so each
|
|
// queued key arrives on its own poll.
|
|
keyboard.keys = {M9_KEY_DOWN, M9_KEY_ENTER, 'a'};
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == M9_KEY_DOWN);
|
|
assert(m9KeyboardReadKey() == 0);
|
|
testMillis += 20;
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == M9_KEY_ENTER);
|
|
testMillis += 20;
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == 'a');
|
|
assert(m9KeyboardReadKey() == 0);
|
|
// The same key again within 70 ms of the last accepted one is the same press (a stale
|
|
// read or a bounce); after that it is a new press.
|
|
keyboard.keys = {'b', 'b'};
|
|
testMillis += 20;
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == 'b');
|
|
testMillis += 20;
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == 0);
|
|
keyboard.keys = {'b'};
|
|
testMillis += 300; // past the same-character window (kM9GuardDefaultMs before a pace is known)
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == 'b');
|
|
assert(m9KeyboardSetBacklight(127));
|
|
assert(keyboard.backlight == 127);
|
|
keyboard.readFails = true;
|
|
testMillis += 20;
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == 0);
|
|
keyboard.readFails = false;
|
|
keyboard.keys = {M9_KEY_UP};
|
|
testMillis += 20;
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == M9_KEY_UP);
|
|
}
|
|
|
|
// ---- #671: the controller, simulated -------------------------------------------
|
|
// Elecrow's controller firmware (ThinkNode-M9-KB-platformio, src/main.cpp): a scan
|
|
// task reads the matrix every 10 ms with no debounce (readMatrix sets keys[] and a
|
|
// changed flag on any difference), and processKeys(), every 10 ms in loop(), latches
|
|
// the key byte on a changed-and-pressed key (sendKeyData: one slot, keyvalue +
|
|
// comdata_flag), the letters on the press itself. An I2C read of reg 0x01 hands
|
|
// the latched byte over once and clears it.
|
|
struct Stroke { uint8_t key; uint32_t down; uint32_t hold; uint32_t chatter; };
|
|
// The contact as the scan sees it: closed while held; `chatter` > 0 makes it open
|
|
// for one scan and close again for one scan as the key comes up (a slow lift).
|
|
static bool contactClosed(const Stroke& s, uint32_t t) {
|
|
if (t < s.down) return false;
|
|
const uint32_t up = s.down + s.hold;
|
|
if (t < up) return true;
|
|
if (s.chatter && t >= up + 10 && t < up + 10 + s.chatter) return true;
|
|
return false;
|
|
}
|
|
struct ControllerSim {
|
|
std::vector<Stroke> strokes;
|
|
std::vector<bool> keys, changed;
|
|
std::vector<uint32_t> sent_ms; // when the controller latched each byte
|
|
void run(TwoWire& bus, uint32_t t0, uint32_t until, std::string& typed, bool real_driver,
|
|
bool (*old_rule)(uint8_t, uint32_t)) {
|
|
keys.assign(strokes.size(), false);
|
|
changed.assign(strokes.size(), false);
|
|
for (uint32_t t = 0; t < until; ++t) {
|
|
if (t % 10 == 0) { // KeyboardScanTask: readMatrix()
|
|
for (size_t i = 0; i < strokes.size(); ++i) {
|
|
const bool p = contactClosed(strokes[i], t);
|
|
if (p != keys[i]) changed[i] = true;
|
|
keys[i] = p;
|
|
}
|
|
}
|
|
if (t % 10 == 5) { // loop(): processKeys()
|
|
for (size_t i = 0; i < strokes.size(); ++i) {
|
|
if (changed[i] && keys[i]) { // State 1: sendKeyData(normalVal)
|
|
bus.keys.clear(); // one slot: a newer key replaces an unread one
|
|
bus.keys.push_back(strokes[i].key);
|
|
sent_ms.push_back(t);
|
|
changed[i] = false;
|
|
}
|
|
if (!keys[i] && changed[i]) changed[i] = false; // State 3: letters send nothing
|
|
}
|
|
}
|
|
testMillis = t0 + t;
|
|
if (real_driver) {
|
|
m9KeyboardPoll();
|
|
for (int k; (k = m9KeyboardReadKey()) != 0;) typed += (char)k;
|
|
} else if (t % 15 == 0 && !bus.keys.empty()) { // a 15 ms poll with the old rule
|
|
const uint8_t k = bus.keys.front();
|
|
bus.keys.pop_front();
|
|
if (old_rule(k, t0 + t)) typed += (char)k;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
// #628's rule: the same key within 70 ms of the last accepted one is the same press.
|
|
static uint8_t s_old_last = 0;
|
|
static uint32_t s_old_ms = 0;
|
|
static bool oldRule(uint8_t k, uint32_t now) {
|
|
if (k == s_old_last && now - s_old_ms < 70) return false;
|
|
s_old_last = k; s_old_ms = now;
|
|
return true;
|
|
}
|
|
// Type `text` at one keystroke every `iki` ms, each key held `hold` ms; every key
|
|
// chatters on the way up when `chatter` is set. Returns what came out.
|
|
static std::string typeText(const char* text, uint32_t iki, uint32_t hold, uint32_t chatter, bool real) {
|
|
resetDriver();
|
|
Wire1.device = 0x6c;
|
|
m9KeyboardBegin();
|
|
s_old_last = 0; s_old_ms = 0;
|
|
ControllerSim sim;
|
|
uint32_t t = 7;
|
|
for (const char* p = text; *p; ++p, t += iki) sim.strokes.push_back({(uint8_t)*p, t, hold, chatter});
|
|
std::string typed;
|
|
sim.run(Wire1, testMillis, t + 400, typed, real, oldRule);
|
|
return typed;
|
|
}
|
|
// Like typeText, with a thinking pause of `pause` ms before each word after the first,
|
|
// the way people actually type a message: quick inside a word, slower between words.
|
|
// A pause shorter than a keystroke is no pause, and a run of spaces gets none.
|
|
static std::string typeWords(const char* text, uint32_t iki, uint32_t pause, uint32_t hold, uint32_t chatter) {
|
|
resetDriver();
|
|
Wire1.device = 0x6c;
|
|
m9KeyboardBegin();
|
|
ControllerSim sim;
|
|
uint32_t t = 7;
|
|
for (const char* p = text; *p; ++p) {
|
|
if (p != text && p[-1] == ' ' && *p != ' ' && pause > iki) t += pause - iki;
|
|
sim.strokes.push_back({(uint8_t)*p, t, hold, chatter});
|
|
t += iki;
|
|
}
|
|
std::string typed;
|
|
sim.run(Wire1, testMillis, t + 400, typed, true, oldRule);
|
|
return typed;
|
|
}
|
|
static std::string typeKeys(const std::vector<uint8_t>& keys, uint32_t iki, uint32_t hold, uint32_t chatter) {
|
|
resetDriver();
|
|
Wire1.device = 0x6c;
|
|
m9KeyboardBegin();
|
|
ControllerSim sim;
|
|
uint32_t t = 7;
|
|
for (uint8_t k : keys) { sim.strokes.push_back({k, t, hold, chatter}); t += iki; }
|
|
std::string typed;
|
|
sim.run(Wire1, testMillis, t + 400, typed, true, oldRule);
|
|
return typed;
|
|
}
|
|
|
|
static void checkChatter() {
|
|
// Reproduced: a slow, deliberate press (held 180 ms) whose contact chatters once on
|
|
// the way up. The controller sends the letter twice, ~190 ms apart, and #628's
|
|
// 70 ms rule lets both through: "hheelllloo".
|
|
const std::string old_slow = typeText("hello", 450, 180, 10, false);
|
|
std::fprintf(stderr, "old rule, slow keys with chatter: \"%s\"\n", old_slow.c_str());
|
|
assert(old_slow == "hheelllloo");
|
|
// The guard now: one character per press, the "ll" typed on purpose kept.
|
|
assert(typeText("hello", 450, 180, 10, true) == "hello");
|
|
assert(typeText("bookkeeper", 450, 200, 10, true) == "bookkeeper");
|
|
// Very slow: a keystroke every 0.6 s, each held 220 ms.
|
|
assert(typeText("deliberate", 600, 220, 10, true) == "deliberate");
|
|
// The limit, kept honest: a key held longer than the 250 ms ceiling that then
|
|
// chatters still types twice. A longer ceiling would eat letters typed twice on
|
|
// purpose; only the controller (it sees the key go up) can debounce that.
|
|
assert(typeText("ab", 600, 300, 10, true) == "aabb");
|
|
// Fast typing, short holds, chatter on every key: still one each, doubles kept.
|
|
assert(typeText("bookkeeper", 170, 70, 10, true) == "bookkeeper");
|
|
assert(typeText("bookkeeper", 170, 70, 0, true) == "bookkeeper");
|
|
// Moderate pace.
|
|
assert(typeText("aardvark scissors", 300, 120, 10, true) == "aardvark scissors");
|
|
// Pauses between words are not the typing pace. A typist at 200 ms a key with a 1 s
|
|
// pause before each word: on beta_100 the pauses pushed the pace, and the window, up
|
|
// to the 250 ms ceiling, so a letter doubled at the same 200 ms was dropped
|
|
// ("hello al, se you son"). With and without chatter, at several paces.
|
|
for (uint32_t iki : {180u, 200u, 220u, 250u}) {
|
|
for (uint32_t pause : {600u, 1000u, 1400u}) {
|
|
assert(typeWords("hello all, see you soon", iki, pause, 90, 0) == "hello all, see you soon");
|
|
assert(typeWords("hello all, see you soon", iki, pause, 90, 10) == "hello all, see you soon");
|
|
}
|
|
}
|
|
// Arrows tapped quickly keep every tap (stepping down a list), as before.
|
|
const std::string downs = typeKeys({M9_KEY_DOWN, M9_KEY_DOWN, M9_KEY_DOWN, M9_KEY_DOWN, M9_KEY_DOWN}, 110, 50, 0);
|
|
assert(downs.size() == 5);
|
|
// A held Delete: the controller repeats it every ~110 ms (REPEAT_DELAY_MS 100 plus
|
|
// its loop); every repeat gets through.
|
|
resetDriver();
|
|
Wire1.device = 0x6c;
|
|
m9KeyboardBegin();
|
|
int dels = 0;
|
|
for (int i = 0; i < 10; ++i) {
|
|
Wire1.keys = {M9_KEY_DEL};
|
|
testMillis += 110;
|
|
m9KeyboardPoll();
|
|
if (m9KeyboardReadKey() == M9_KEY_DEL) ++dels;
|
|
}
|
|
assert(dels == 10);
|
|
// The window: 3/5 of the pace, held between 70 and 250 ms.
|
|
M9KeyGuard g;
|
|
assert(m9KeyGuardWindow(g) == kM9GuardDefaultMs);
|
|
g.pace_ms = 100; assert(m9KeyGuardWindow(g) == 70);
|
|
g.pace_ms = 300; assert(m9KeyGuardWindow(g) == 180);
|
|
g.pace_ms = 900; assert(m9KeyGuardWindow(g) == 250);
|
|
// A pause does not count as the pace.
|
|
M9KeyGuard p;
|
|
assert(m9KeyGuardAccept(p, 'a', 1000));
|
|
assert(m9KeyGuardAccept(p, 'b', 5000));
|
|
assert(p.pace_ms == 0);
|
|
assert(m9KeyGuardAccept(p, 'c', 5300));
|
|
assert(p.pace_ms == 300);
|
|
// A gap over twice the pace counts as twice the pace: a 1.2 s pause at a 200 ms pace
|
|
// moves it to 250, not 450.
|
|
M9KeyGuard q;
|
|
q.last_key = 'a'; q.last_ms = 0; q.pace_ms = 200;
|
|
assert(m9KeyGuardAccept(q, 'b', 1200));
|
|
assert(q.pace_ms == 250);
|
|
// A writer who really slows down still raises the pace within a few keys.
|
|
M9KeyGuard r;
|
|
r.last_key = 'a'; r.last_ms = 0; r.pace_ms = 150;
|
|
uint32_t t = 0;
|
|
const char* slow = "bcdefgh";
|
|
for (const char* c = slow; *c; ++c) { t += 600; assert(m9KeyGuardAccept(r, (uint8_t)*c, t)); }
|
|
assert(m9KeyGuardWindow(r) == kM9GuardMaxMs);
|
|
}
|
|
|
|
// Double-tap SPACE switches the keyboard language when both spaces arrive within
|
|
// kM9DoubleSpaceMs (handleHwKey). On beta_100 that limit was 250 ms, and the guard
|
|
// dropped a second space inside its own window, also 250 ms right after boot, so the
|
|
// shortcut could never fire.
|
|
static void checkDoubleSpace() {
|
|
static_assert(kM9DoubleSpaceMs > kM9GuardMaxMs, "a second space must be able to get past the guard");
|
|
for (uint32_t gap : {260u, 300u, 350u, 420u}) {
|
|
for (uint32_t pace : {0u, 150u, 300u, 900u}) { // 0 = right after boot
|
|
M9KeyGuard g;
|
|
g.pace_ms = pace;
|
|
assert(m9KeyGuardAccept(g, ' ', 10000));
|
|
assert(m9KeyGuardAccept(g, ' ', 10000 + gap)); // the second tap gets through...
|
|
assert(gap < kM9DoubleSpaceMs); // ...in time to switch
|
|
}
|
|
}
|
|
// One slow space press that chatters as it comes up is still one space, so it can
|
|
// neither type two spaces nor switch the language.
|
|
assert(typeText("a b", 450, 180, 10, true) == "a b");
|
|
}
|
|
|
|
int main() {
|
|
checkChatter();
|
|
checkDoubleSpace();
|
|
checkController(0x6c, false);
|
|
checkController(0x6c, true);
|
|
checkController(0x6d, false);
|
|
checkController(0x6d, true);
|
|
resetDriver();
|
|
m9KeyboardBegin();
|
|
assert(!m9KeyboardSetBacklight(255));
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == 0);
|
|
// A controller booting late must be found without rebooting the main MCU.
|
|
Wire1.device = 0x6d;
|
|
Wire1.keys = {M9_KEY_ENTER};
|
|
testMillis += 1001;
|
|
m9KeyboardPoll();
|
|
assert(m9KeyboardReadKey() == M9_KEY_ENTER);
|
|
assert(m9KeyboardSetBacklight(255));
|
|
std::puts("M9 keyboard: both revisions/buses, navigation, text, backlight, late boot and read failure PASS");
|
|
}
|