Skip to content

Instantly share code, notes, and snippets.

@caksoylar
Last active October 7, 2024 21:38
Show Gist options
  • Save caksoylar/2b7396b7c3def3d5735fa5edbb6c98b0 to your computer and use it in GitHub Desktop.
Save caksoylar/2b7396b7c3def3d5735fa5edbb6c98b0 to your computer and use it in GitHub Desktop.
QMK combination dial lock for entering passwords with a rotary encoder

Creating a combination dial lock with a rotary encoder in QMK

Below is a snippet for implementing a combination dial-like mechanism you can use in your QMK keymap. This is useful for entering long passwords using a rotary encoder, or activating secret macros etc. The algorithm looks for a sequence of clockwise (CW) or counter-clockwise (CCW) turns of the rotary encoder for a certain number of ticks/clicks. It executes the macro if all turns are successfully completed, or resets to the beginning if a turn is too long or too short.

For this example we will output a string as a macro, like entering a password. In this case it is "TA DA!":

void emit_passphrase_secret(void) {
    SEND_STRING("TA DA!")
}

Then we define the secret combination, which is the number of ticks in alternating directions. In this example we look for a sequence of three turns: (1) one full turn CW, (2) one half turn CCW, (3) one and a half turns CW. My encoder has twenty ticks for a full turn, so we can write:

#define COMBINATION { 20, 10, 30 }
#define START_CW true

We set the direction for the first turn with the START_CW macro, which is true for CW and false for CCW.

Lastly we define a tolerance value such that turn_ticks +/- tolerance will be accepted if a turn requires turn_ticks ticks. This is necessary because most rotary encoders are not precise in counting ticks; for example mine usually misses the first tick after a direction switch.

#define TOLERANCE 3

Below is the main function that keeps track of the "state" of the lock (i.e. how many turns we have successfully completed), which will be called at each tick of the encoder.

void update_dial(bool direction) {
    const static uint8_t combo[]         = COMBINATION;
    const static size_t  combo_len       = sizeof(combo);
    static bool          direction_to_go = START_CW;
    static uint8_t       state = 0, clicks = 0, grace = 0;

    if (direction == direction_to_go) {              // check correct direction
        if (++clicks == combo[state] - TOLERANCE) {  // increment clicks and check if dialed enough
            clicks = grace  = 0;
            direction_to_go = !direction_to_go;  // flip expected direction
            if (++state == combo_len) {          // increment state then check if we are at the end
                state           = 0;             // emit secret passphrase and reset to start
                direction_to_go = START_CW;
                emit_passphrase_secret();
            }
        }
    } else {                                           // wrong direction so fail, except...
        if (clicks != 0 || ++grace > 2 * TOLERANCE) {  // a few extra ticks just after changing direction is acceptable
            state = clicks = grace = 0;                // reset to start
            direction_to_go        = true;
        }
    }
}

Finally we hook this method up to our encoder_update_user method so it gets updated at each tick:

void encoder_update_user(uint8_t index, bool clockwise) {
    if (index == 0) {  // checking the first and only encoder, in this case
        switch (get_highest_layer(layer_state)) {
            // I'd recommend to check only when in a specific layer, in this case _FUNC
            /* code for other layers... */
            case _FUNC:
                update_dial(clockwise);
                break;
            /* code for other layers... */
        }
    }
}

Security concerns

IMPORTANT: I don't recommend anyone store their passwords in their keyboard's firmware unless you are 100% sure no one else will have physical access to it. This lock is not a guaranteed method of keeping your password safe. For instance someone can imitate your turns to unlock it, or if they know what they are doing dump the keyboard's firmware and extract the password from there.

Also make sure not to commit your passphrase if you are keeping your keymap in a public repository. Here are some tips on storing your secret macros with QMK from drashna: https://github.com/qmk/qmk_firmware/blob/master/users/drashna/keyrecords/secrets.md

Userspace implementation example

For an example implementation using drashna's userspace framework with secrets, you can check out my QMK userspace. rotary_lock.c and rotary_lock.h contains the above implementation using emit_passphrase_secret function defined in secrets.c (which isn't checked in). rules.mk has the compilation rules so that the rotary lock functions are compiled only if secrets.c exists and if USE_SECRETS is defined, so that you can only enable it for certain keyboards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment