Adding New Capabilities to Kiibohd

Written by J David Smith
Published on 13 January 2016

One of the reasons I bought my ErgoDox was because I'd be able to hack on it. Initially, I stuck to changing the layout to Colemak and adding bindings for media keys. Doing this with the Kiibohd firmware is reasonably straightforward: clone the repository, change or add some .kll filesKLL itself is pretty straightforward, although the remapping rules at times are cumbersome. They follow the semantics of vim remap rather than the more-sane-for-mapping-the-entire-keyboard noremap, and then recompile and reflash using the provided shell scripts.

This week, I decided to finally add a new capability to my board: LCD status control. One thing that has irked me about the Infinity ErgoDox is that the LCD backlights remain on even when the computer is off. As my computer is in my bedroom, this means that I have two bright nightlights unless I unplug the keyboard before going to bed.

Fortunately, the Kiibohd firmware and KLL language support adding capabilities, which are C functions conforming to a couple of simple rules, and exposing those capabilities for keybinding. This is how the stock Infinity ErgoDox LCD and LED control is implemented, and how I planned to implement my extension. However, the process is rather poorly documented and presented some unexpected hurdles. Ultimately, I got it working and wanted to document the process for posterity here.Once I get a better understanding of the process, I will contribute this information back to the Github wiki The rest of this post will cover in detail how to add a new capability LCDStatus(status) that controls the LCD status. LCDStatus(0/1/2) will turn off/turn on/toggle the LCD.

Background & Setting Up

Before attempting to add a capability, make sure you can compile the stock firmware and flash it to a keyboard successfully. The instructions on the Kiibohd repository are solid, so I won't reproduce them here.

An important note before beginning is that it is possible to connect to the keyboard via a serial port. On Linux, this is typically /dev/ttyACM0. The command screen /dev/ttyACM0 as root will allow one to connect, issue commands, and view debug messages during development.

This post is specifically concerned with implementing an LCDStatus capability. If you don't have an LCD to control the status of, then this obviously will be nonsense. However, much of the material (e.g. on states and state types) may still be of use.

The Skeleton of a Capability

Capabilities in Kiibohd are simply C functions that conform to an API: void functions with three parameters: state, stateType, and args. At the absolute minimum, a capability will look like this:

void my_capability(uint8_t state, uint8_t stateType, uint8_t *args) {
}

The combinations of state and stateType describe the keyboard state:This information is buried in a comment in Macro/PartialMap/kll.h

stateType state meaning
0x00 (Normal)0x00key depressed
0x00 (Normal)0x01key pressed
0x00 (Normal)0x02key held
0x00 (Normal)0x03key released
0x01 (LED)0x00off
0x01 (LED)0x01on
0x02 (Analog)0x00key depressed
0x02 (Analog)0x01key released
0x02 (Analog)0x10 - 0xFFLight Press - Max Press
0x03-0xFEReserved
0xFF (Debug)0xFFPrint capability signature

Every capability should implement support for the debug state. Without this, the capability will not show up in the capList debug command.

void my_capability(uint8_t state, uint8_t stateType, uint8_t *args) {
    if ( state == 0xFF && stateType == 0xFF ) {
        print("my_capability(arg1, arg2)");
        return;
    }
}

Within this skeleton, you can do whatever you want! The full power of C is at your disposal, commander.

Turning Out the Lights

The LCD backlights have three channels corresponding to the usual red, green, and blue. These are unintuitively named FTM0_C0V, FTM0_C1V, and FTM0_C2V.These names refer to the documentation for the LCD itself, so in that context they make sense. To turn them off, we simply zero them out:

void LCD_status_capability(uint8_t state, uint8_t stateType, uint8_t *args) {
    if ( state == 0xFF && stateType == 0xFF ) {
        print("my_capability(arg1, arg2)");
        return;
    }
    FTM0_C0V = 0;
    FTM0_C1V = 0;
    FTM0_C2V = 0;
}

With this addition, we have a capability that adds new functionality! I began by adding this function to Scan/STLcd/lcd_scan.c, because I didn't and still don't want to mess with adding new sources to CMake. Now we can expose this simple capability in KLL:

LCDStatus => LCD_status_capability();

It can be bound to a key just like the built-in capabilities:

U"Delete": LCDStatus();

If you were to compile and flash this firmware, then pressing Delete would now turn off the LCD instead of deleting. On the master half. We will get to communication later.

Adding Some Arguments

The next step in our quest is to add the status argument to the capability. This is pretty straightforward. First, we will update the KLL to reflect the argument we want:

LCDStatus => LCD_status_capability( status : 1 );

The status : 1 in the signature defines the name and size of the argument in bytes. The name isn't used for anything, but should be named something reasonable for all the usual reasons.

Then, our binding becomes:

U"Delete": LCDStatus( 0 );

Processing the arguments in C is, unfortunately, a bit annoying. The third parameter to our function (*args) is an array of uint8_t. Since we only have one argument, we can just dereference it to get the value. However, there are examples of more complicated arguments in lcd_scan.c illustrating how not-nice it can be.

void LCD_status_capability(uint8_t state, uint8_t stateType, uint8_t *args) {
    if ( state == 0xFF && stateType == 0xFF ) {
        print("my_capability(arg1, arg2)");
    }

    uint8_t status = *args;
    if ( status == 0 ) {
        FTM0_C0V = 0;
        FTM0_C1V = 0;
        FTM0_C2V = 0;
    }
}

Figuring out how to restore the LCD to a reasonable state is less straightforward. What I chose to do for my implementation was to grab the last state stored by LCD_layerStackExact_capability and use that capability to restore it. In practice, it doesn't matter if you even can restore it: any key that changes the color of the backlight also changes its magnitude. The default ErgoDox setup has colors for each partial map, and I'd imagine most people would put a function like this off of the main typing map because of its infrequent utility. As a result, the mere act of pressing the modifier to activate this capability will turn the backlight back on. However, I implemented it anyway just in case. layerStackExact uses two variables to track its state:

uint16_t LCD_layerStackExact[4];
uint8_t LCD_layerStackExact_size = 0;

It also defines a struct which it uses to typecast the *args parameter.

typedef struct LCD_layerStackExact_args {
	uint8_t numArgs;
	uint16_t layers[4];
} LCD_layerStackExact_args;

We can turn the LCD back on by calling the capability with the stored state. Note that I copied the array, just to be safe. I'm not sure if it is necessary but I didn't want to have to try to debug corrupted memory.

void LCD_status_capability(uint8_t state, uint8_t stateType, uint8_t *args) {
    if ( state == 0xFF && stateType == 0xFF ) {
        print("my_capability(arg1, arg2)");
    }

    uint8_t status = *args;
    if ( status == 0 ) {
        FTM0_C0V = 0;
        FTM0_C1V = 0;
        FTM0_C2V = 0;
    } else if ( status == 1 ) {
        LCD_layerStackExact_args stack_args;
        stack_args.numArgs = LCD_layerStackExact_size;
        memcpy(stack_args.layers, LCD_layerStackExact, sizeof(LCD_layerStackExact));
        LCD_layerStackExact_capability( state, stateType, (uint8_t*)&stack_args );
    }
}

Now binding a key to LCDStatus(1) would turn on the LCDs.

Creating Some State

Like most mostly-functional programmers, I abhor state. Don't like it. Don't want it. Don't want to deal with it. However, if we want to implement a toggle that's exactly what we'll need. We simply create a global variable (ewww, I know! But we can deal) LCD_status and set it to the appropriate values. Then toggling is as simple as making a recursive call with !LCD_status.

uint8_t LCD_status = 1; // default on
void LCD_status_capability(uint8_t state, uint8_t stateType, uint8_t *args) {
    if ( state == 0xFF && stateType == 0xFF ) {
        print("my_capability(arg1, arg2)");
    }

    uint8_t status = *args;
    if ( status == 0 ) {
        FTM0_C0V = 0;
        FTM0_C1V = 0;
        FTM0_C2V = 0;
        LCD_status = 0;
    } else if ( status == 1 ) {
        LCD_layerStackExact_args stack_args;
        stack_args.numArgs = LCD_layerStackExact_size;
        memcpy(stack_args.layers, LCD_layerStackExact, sizeof(LCD_layerStackExact));
        LCD_layerStackExact_capability( state, stateType, (uint8_t*)&stack_args );
        LCD_status = 1;
    } else if ( status == 2 ) {
        status = !LCD_status;
        LCD_status_capability( state, stateType, &status );
    }
}

Binding a key to LCDStatus(2) will now...do nothing (probably). Why? The problem is that the capability will continuously fire while the key is held down, and the microcontroller is plenty fast enough to fire arbitrarily many times during even a quick tap. So, we will guard the toggleThe other two options move the keyboard to a fixed state and thus don't need to be protected. with an additional condition:

else if ( status == 2 && stateType == 0 && state == 0x03 ) {
    // ...
}

Release (0x03) is the only state that fires only once, so we check for that. Alas, even after fixing this, we still only have one LCD bent to our will! What about the other?

Inter-Keyboard Communication

The two halves of an Infinity ErgoDox are actually completely independent and may be used independently of one another. However, if the two halves are connected then they can communicate by sending messages back and forth.If both halves are separately plugged into the computer, then they can't communicate. I haven't delved into the network code, but I assume it is probably serial like the debug communication.

Very Important Note: You must flash both halves of the keyboard to have matching implementations of the capability when using communication.

The code to communicate changes relatively little from case to case but is rather long to reconstruct by hand. Therefore, I basically just copied it from LCD_layerStackExact_capability, changed the function it referred to, and called it a day. Wonderfully, that worked! Well, sort of. It turns out that not guarding against recursion caused weird issues where it would work with the right-hand being master, but not the left. It took a long time to debug because the error was unrelated to the fix (guarding against the recursive case).

#if defined(ConnectEnabled_define)
  // Only deal with the interconnect if it has been compiled in
  if ( status == 0 || status == 1 ) {
     // skip in the recursive case

    if ( Connect_master )
      {
        // generatedKeymap.h
        extern const Capability CapabilitiesList[];

        // Broadcast LCD_status remote capability (0xFF is the broadcast id)
        Connect_send_RemoteCapability(
            0xFF,
            LCD_status_capability_index,
            state,
            stateType,
            CapabilitiesList[ LCD_status_capability_index ].argCount,
            &status);
      }
  }
#endif

The magic constant LCD_status_capability_index ends up available through some build magic that I haven't delved into yet.

The Final Result

Putting all of that code together, we get:

uint8_t LCD_status = 1; // default on
void LCD_status_capability(uint8_t state, uint8_t stateType, uint8_t *args) {
    if ( state == 0xFF && stateType == 0xFF ) {
        print("my_capability(arg1, arg2)");
    }

    uint8_t status = *args;
    if ( status == 0 ) {
        FTM0_C0V = 0;
        FTM0_C1V = 0;
        FTM0_C2V = 0;
        LCD_status = 0;
    } else if ( status == 1 ) {
        LCD_layerStackExact_args stack_args;
        stack_args.numArgs = LCD_layerStackExact_size;
        memcpy(stack_args.layers, LCD_layerStackExact, sizeof(LCD_layerStackExact));
        LCD_layerStackExact_capability( state, stateType, (uint8_t*)&stack_args );
        LCD_status = 1;
    } else if ( status == 2 && stateType == 0 && state == 0x03 ) {
        status = !LCD_status;
        LCD_status_capability( state, stateType, &status );
    }

#if defined(ConnectEnabled_define)
    // Only deal with the interconnect if it has been compiled in
    if ( status == 0 || status == 1 ) {
       // skip in the recursive case

      if ( Connect_master )
        {
          // generatedKeymap.h
          extern const Capability CapabilitiesList[];

          // Broadcast LCD_status remote capability (0xFF is the broadcast id)
          Connect_send_RemoteCapability(
              0xFF,
              LCD_status_capability_index,
              state,
              stateType,
              CapabilitiesList[ LCD_status_capability_index ].argCount,
              &status);
        }
    }
#endif
}

I have a working implementation of this on my fork of kiibohd. I'm looking forward to adding more capabilities to my keyboard now that I've gotten over the initial learning curve. I've already got a couple in mind. Generic Mod-key lock, anyone?