Integrating custom Arduino based handware with GameSense

06 Apr 2016

by Dave Astels

When we created GameSense we were thinking about integrating our hardware with games. But we didn’t create a framework that was limited to that. The architecture of the GameSense SDK allows anyone to integrate pretty much whatever custom hardware they want with any game/application that supports GameSense.

This project uses specific hardware and an Arduino compatible controller, but the approach can be applied to any custom hardware with a controller that can be communicated with via a usb/serial port.

Rainbow Cube

I get regular email from MassDrop and a while ago they featured the Rainbow Cube from SeeedStudio. I figured this would be an interesting thing to use with GameSense so I picked one up.

Available as an option was SeeedStudio’s RainbowDuino controller board: an Arduino compatable board with LED driver ciruitry and connectors that treat the cube as a shield. It can also drive SeeedStudio’s 8x8 LED matrix. The approach described in this article needs minor changes to support that instead of the cube.

So that’s all there is to the hardware part. I took the kit option to save a few dollars (and because building electronics kits is fun), but it’s also available fully assembled.

Commands

Now I needed a way to send the RainbowDuino commands telling it what colors to set various LEDs to. The first task was to decide what commands were needed. I decided on ths following:

  • Clear: turn all LEDs off. No data is required.
  • Set color of specific LEDs. This requires a set of (coordinate, color) pairs. Coordinates are (x, y, z) tuples and colors are (r, g, b) tuples.
  • Set the color of all LEDs. Since all LEDs are being set, we can dispense with coordinates and use 64 ordered (r, g, b) tuples.

The first command is obvious, and corresponds to a single function call in the RainbowDuino library. Having the two color setting commands gives the flexibility of using different LEDs separately, or setting them all at once. Setting a single LED is just the simplest case of setting a set of specific LEDs.

Communication

Next, I needed to design the protocol I’d use to send commands to the RainbowDuino. I went with a very basic binary protocol to minimize overhead. Each command is made up of a one byte command (a 1, 2, or ,3 for specific-led, all-leds, and clear) followed by a sequence of bytes that provide the data. Each x, y, z, r, g, and b is a single byte.

For example, setting all LEDs would be a sequence of bytes: 2, r1, g1, b1, … r64, g64, b64. For setting two LEDs the sequence would be: 1, x1, y1, z1, r1, g1, b1, x2, y2, z2, r2, g2, b2. Clear is simply a single byte: 3.

Because the command to set specific LEDS can have a variable amount of data (depending on how many LEDs are to be set) I prepend a byte to the command sequence that is the length of the command + data bytes.

As a concrete example, the full command sequence to set the LED at (0,0,0) to magenta would be:

0x07 0x01 0x00 0x00 0x00 0xFF 0x00 0xFF

Optimization

Since communication defaults to 9600 baud, when many LEDS (or all 64) are set there is a visible ripple effect if you set LED colors as you read the command data from the serial port. To avoid this, I decided to load the entire command (the length byte comes in handy here since it tells how many bytes need to be read) into a buffer, then go through the buffer setting LED colors. The effect of this is quite nice, with no visible ripple/delay.

Using a single byte for length keeps things simple, but does limit the design to a 256 byte buffer. This is quite fine for the set all LEDS command, which requires 193 bytes (the command and 3 bytes for each of the 64 LEDs). For the set specific LEDs command, it imposes a limit of setting 42 LEDs in a single command (6 bytes per LED * 42 is 252). I’m happy with this since if more are needed, I’d probably just use the set all LEDs command.

RaibowDuino Code

/* -*- mode: C;-*-
 *
 * Serial control for 4x4x4 RainbowCube
 *
 * Dave Astels
 * Steelseries
 * 2015
 *
 */

#include <Rainbowduino.h>

byte buffer[256];


void setup()
{
  Serial.begin(9600);
  Rb.init();
}


byte read_byte()
{
  while (Serial.available() == 0);
  return Serial.read();
}


void load_buffer(byte length)
{
  byte index = 0;
  while (length > 0) {
    buffer[index++] = read_byte();
    length--;
  }
}


void loop()
{
  if (Serial.available() > 0) {
    byte length = read_byte();
    byte command = read_byte();
    length--;
    load_buffer(length);
    byte index = 0;
    switch (command) {
    case 1:                 /* set the color of specific LEDs */
      while (index < length) {
        Rb.setPixelZXY(buffer[index+2], buffer[index], buffer[index + 1],
                       buffer[index+3], buffer[index+4], buffer[index+5]);
        index += 6;
      }
      break;
    case 2:                 /* set the colors for all LEDs */
      for (byte z = 0; z < 4; z++) {
        for (byte y = 0; y < 4; y++) {
          for (byte x = 0; x < 4; x++) {
            byte r = buffer[index++];
            byte g = buffer[index++];
            byte b = buffer[index++];
            Rb.setPixelZXY(z, x, y, r, g, b);
          }
        }
      }
      break;
    case 3:                 /* clear the cube */
      Rb.blankDisplay();
      break;
    }
  }
}

Unrestricting Gamesense

Before you can access OS device files in order to communicate with external hardware, GameSense has to be unrestricted. Please see this post for details. You can work with just GoLisp without worrying about this, it is only applicable to using GoLisp in the context of Gamesense. That means you can fiddle and prototype all you lilke in GoLisp, but when you want to access external hardware from GameSense event handlers, you’ll have to unrestrict it first.

Controlling the cube from GoLisp

With the cube and RainbowDuino work done, I turned my attention to integrating with CS:GO using the GameSense SDK. I decided to use the cube’s 64 LEDS to show the number of bullets in the active clip. We already show this on the function keys of the APEX M800 as a bar graph. This gives you a proportional indication of how full your clip is, but I thought it would be cool to show exactly how many bullets remain: 1 bullet - 1 LED. I decided to keep the color changes and flash effects similar to what we use on the M800, with minor changes: white, changing to orange when it’s getting low, to red when it’s very low, and flashing when it’s below 20% remaining.

I’ll be using GoLisp to write the custom handler code, so if you are unfamilair with that, and/or the GameSense GoLisp SDK, please go read up on them at our developer site.

Interfacing with the RainbowDuino

I put the following code in the file hax0rBindings/lib/rainbow_cube.lsp and load it in the event handler file (below).

To talk to the cube from GoLisp I wrote some port management code to abstract away the details:

(define cube-port nil)

;;; open the connection to the arduino.

(define (open-cube-port port-name)
  (unless cube-port
          (set! cube-port (open-output-file port-name))))


;;; close the connection to the arduino

(define (close-cube-port)
  (when cube-port
        (close-output-port cube-port)
        (set! cube-port nil)))


;;; Write to the Rainbowduino

(define (write-to-cube cmd)
  (let ((bytes (cons (length cmd) cmd)))
    (write-bytes (list->bytearray bytes) cube-port)))

This defines a variable to hold the open port, and functions to open and close it. Keep in mind that you will probably have to change the name of the port file to reflect where the RainbowDuino is connected on your system.

write-to-cube gets the length of the list of bytes, prepends it to the list, converts the result to a bytearray and writes it to the port the arduino is connected to.

Next up is a set of functions to abstract the actual serial communication protocol, giving a more pleasant Lispy interface.

(define SET_SOME_LEDS 1)
(define SET_ALL_LEDS 2)
(define CLEAR 3)


;;; set the color of a single LED
;;; position is (x y z)
;;; color is (r g b)

(define (set-one-led position color)
  (write-to-cube (flatten (list SET_SOME_LEDS position color))))


;;; Set the colors of specific LEDs
;;; positions-and-colors is ( ((x y z)(r g b)) ... )

(define (set-some-leds positions-and-colors)
  (write-to-cube (flatten* (list SET_SOME_LEDS positions-and-colors))))


;;; set the color of all LEDs
;;; colors is ( (r g b) ... ), one tuple for each of the 64 LEDs
;;; ordering is X major, Z minor

(define (set-all-leds colors)
  (let ((cmd (flatten* (list SET_ALL_LEDS colors))))
    (write-to-cube cmd)))


;;; Clear the cube

(define (clear-all)
  (write-to-cube (list CLEAR)))

Here you can see the functions to clear the cube (clear-all), set all LEDs (set-all-leds), and set specific LEDs (set-some-leds). I also added a convience function to set a single LED (set-one-led). Each of these functions constructs a list of byte values making up the binary command and passes it to the write-to-cube function.

That’s pretty straight forward and gives a solid foundation for writing more abstract layers. However for the purposes of showing bullets, this much is sufficient.

CS:GO Integration

To do custom GameSense integration in GoLisp, you simply write event handler code in a file in the haX0rBindings directory, naming it for the game: in this case csgo.lsp. I’ll go through the handler file incrementally (shown in it’s entireity at the end of this section).

Connect to and initialize the cube

;;; the filename may need to be changed for your system.

(open-cube-port "/dev/cu.usbserial-AJ02XEN5")
(clear-all)

Simple enough.

Ammo event handler

(handler "CSGO" "UPDATE-AMMO"
         (lambda (data)
           (bullet-count:! csgo-ammo-flasher-with-cube (bullet-count: data))
           (set-value:> csgo-ammo-flasher-with-cube (value: data))))

This is pretty simple as well. It simple grabs the number of bullets and the percent full from the data frame, and sends it to the flasher, which we’ll show next.

Custom flasher that uses the cube

(define csgo-ammo-flasher-with-cube {
  proto*: Flasher
  auto-enable: #t
  bullet-count: 0

Standard for a flasher, with the addition of a slot to hold the actual bullet count.

  bullet-colors: (lambda (c)
                   (map (lambda (i)
                          (if (and (not (nil? bullet-count)) (< i bullet-count))
                              c
                              black-color))
                        (interval 0 63)))

This function computes The colors to use for each of the 64 LEDs based on the number of bullets. LEDs with an index less that the bullet count get the active color, any higher are turned off (i.e. black).

  compute-color: (lambda (percent-full)
                   (cond ((nil? percent-full) black-color)
                         ((< percent-full 20) red-color)
                         ((< percent-full 34) orange-color)
                         (else white-color)))

This computes the color to use when setting LED colors. If you are holding a weapon without ammo (e.g. a knife) percent-full will be nil and so all LEDS should be off/black. Use red if the clip is less than 20% full, orange between 20% and 34%, and white if it’s greater than that. The effect is that LEDS are white when you have plenty of ammo, switching to orange when you are getting low, and to red when you’re low.

  compute-period: (lambda (percent-full)
                    (if (and (not (nil? percent-full))
                             (> percent-full 0)
                             (<= percent-full 20))
                      250
                      0))

This is used to turn the flash effect on (if the result is not zero) and off (if it is zero) as well as set how fast to flash. The 250 is the number of milliseconds in half of a flash cycle, i.e. 500 mS for a full cycle, i.e. flashing at 2 Hz (twice per second). This code turns the flasher on at 2Hz if you are holding a weapon with ammo and it’s at 20% full or less.

  update-color: (lambda (c percent-full)
                  (set-all-leds (bullet-colors c)))

This is the function that actually updates the cube. It uses the function above that computes color values for the 64 LEDs, and sends those colors out to the cube via the set-all-leds function we saw earlier.

  cleanup-function: (lambda (v)
                      (update-color color value))})

Finally, this function resets the cube when the flasher effect is turned off.

Here’s the flasher in it’s entireity (for easy copying):

;;; -*- mode: Scheme -*-

;;; Replace CS:GO even handlers to make use of The Rainbow Cube
;;; Dave Astels
;;; Steelseries
;;; 2015

(load "hax0rBindings/lib/rainbow_cube.lsp")

(define csgo-ammo-flasher-with-cube {
  proto*: Flasher
  auto-enable: #t
  bullet-count: 0

  bullet-colors: (lambda (bullet-count c)
                   (map (lambda (i)
                          (if (and (not (nil? bullet-count)) (< i bullet-count))
                              c
                              black-color))
                        (interval 0 63)))

  compute-color: (lambda (percent-full)
                   (cond ((nil? percent-full) black-color)
                         ((< percent-full 20) red-color)
                         ((< percent-full 34) orange-color)
                         (else white-color)))
  
  compute-period: (lambda (percent-full)
                    (if (and (not (nil? percent-full))
                             (> percent-full 0)
                             (<= percent-full 20))
                      250
                      0))

  update-color: (lambda (c percent-full)
                  (set-all-leds (bullet-colors bullet-count c)))
  
  cleanup-function: (lambda (v)
                      (update-color color value))})


(handler "CSGO" "UPDATE-AMMO"
         (lambda (data)
           (bullet-count:! csgo-ammo-flasher-with-cube (bullet-count: data))
           (set-value:> csgo-ammo-flasher-with-cube (value: data))))



(open-cube-port "/dev/cu.usbserial-AJ02XEN5")
(clear-all)

Summary

That’s all there is to it. Using this general approach, you can integrate any custom hardware that’s controlled by an arduino (well, one that supports serial over USB) or really any hardware that you can communicate with via a usb/serial port. I’m eager to see what you come up with.