USB ISR + SPI

I understand that is generally a good idea but in this case I wasn't convinced that it was necessary; a wall of code can turn people off to answering; no one likes reading a novel in a forum post. I wasn't aware it was considered an absolute requirement; that said, I'm happy to provide it.

HID.cpp:

bool WEAK HID_Setup(Setup& setup)
{
    //...
    if (REQUEST_HOSTTODEVICE_CLASS_INTERFACE == requestType)
    {
        //...
        if (HID_SET_REPORT == r)
        {
            uint8_t *data = new uint8_t[setup.wLength];
            if (setup.wLength == USB_RecvControl(data, setup.wLength))
            {
                Joystick.recvState(data);//TODO - refactor; global callback might be better
            }
            delete data;
            return true;
        }
    }
    return false;
}

void Joystick_::sendState(JoyState_t *joySt)
{
    //HID_SendReport(Report number, array of values in same order as HID descriptor, length)
    HID_SendReport(1, joySt->getHIDData(), JoyState::BYTES);
    // The joystick is specified as using report 1 in the descriptor. That's where the "1" comes from
}

void Joystick_::recvState(const uint8_t *data)
{
    if (recvStateCallback)
    {
        (*recvStateCallback)(data);
    }
}

Dsky.h + Dsky.cpp:

    MCP23017 _buttonDetector;
    MCP23017 _slaveSelector;
    uint8_t _currentSlaveSelectPin;

void Dsky::begin(void)
{
    _slaveSelector.begin(B001);
    for(uint8_t i = 0; i < BUTTON_COUNT; i++)//BUTTON_COUNT == 10
    {
        _slaveSelector.pinMode(i, OUTPUT);
    }
    
    _slaveSelector.writeGPIOAB(65535);//All HIGH; Slave Select starts with LOW

    _buttonDetector.begin(B010);
    _buttonDetector.config(LOW, HIGH, LOW, LOW, LOW, LOW, HIGH);
    _buttonDetector.enablePinInterruptOnChange(0);

    readState();//Clear any latent interrupts
}

void Dsky::reset(void)
{
    for (uint8_t button = 0; button < BUTTON_COUNT; button++)
    {
        beginCommand(button);
        SPI.transfer(Command_Reset);
        SPI.transfer(CommandData_Reset);
        endCommand();
    }
}

uint16_t Dsky::readState(void)
{
    return _buttonDetector.readIntCapAB();
}

void Dsky::setDisplay(uint8_t button, const uint8_t data[])
{
    beginCommand(button);
    SPI.transfer(Command_SetDisplayData);
    for (int i = 0; i < 256; i++)
    {
        SPI.transfer(data[i]);
    }
    endCommand();
}

void Dsky::setBrightness(uint8_t button, Brightness brightness)
{
    beginCommand(button);
    SPI.transfer(Command_SetBrightness);
    SPI.transfer(brightness);
    endCommand();
}

void Dsky::setColor(uint8_t button, Color red, Color green, Color blue)
{
    byte colorData = B00000011;
    colorData |= (red << 6);
    colorData |= (green << 4);
    colorData |= (blue << 2);

    beginCommand(button);
    SPI.transfer(Command_SetColor);
    SPI.transfer(colorData);
    endCommand();
}

void Dsky::beginCommand(uint8_t button)
{
    _slaveSelector.digitalWrite(_currentSlaveSelectPin = button, LOW);
}

void Dsky::endCommand(void)
{
    _slaveSelector.digitalWrite(_currentSlaveSelectPin, HIGH);
}

My ino:

JoyState_t joyState;
Dsky dsky;

//INT0 and INT1 are SDA and SCL - can't use them
#define DSKY_INTERRUPT INT2

volatile byte btnId = 0xFF;
volatile byte brightness = 0xFF;
volatile byte colorR = 0xFF;
volatile byte colorG = 0xFF;
volatile byte colorB = 0xFF;
volatile byte contentId = 0xFF;

void setup()
{
    SPI.begin();
    SPI.setBitOrder(MSBFIRST);
    SPI.setDataMode(SPI_MODE2);
    SPI.setClockDivider(SPI_CLOCK_DIV4);

    dsky.begin();

    attachInterrupt(DSKY_INTERRUPT, onDskyButtonChanged, FALLING);

    Joystick.recvStateCallback = &onJoystickReceiveState;
}

void loop()
{
    delay(10); //Allow programmer to break in

    //As per: http://www.microchip.com/forums/m659620.aspx and http://www.raspberrypi.org/forums/viewtopic.php?t=58185&p=457459
    //The MCP23017 will not actually clear INTFx until the state that caused the interrupt resets so basically you're forced to read INTCAPx or GPIOx every time so the interrupt clears and can be re-triggered
    dskyButtonState = dsky.readState();

    if (dskyButtonChanged)
    {
        delay(25); //debounce
        joyState.dskyButtons = dskyButtonState;
        dskyButtonChanged = false;
    }

    if(buttonId != 0xFF)
    {
        dsky.setBrightness(btnId, (Dsky::Brightness)convertBrightness(brightness));
        dsky.setColor(btnId, (Dsky::Color)convertColor(colorR), (Dsky::Color)convertColor(colorG), (Dsky::Color)convertColor(colorB));
        dsky.setDisplay(btnId, convertContentId(contentId));
        btnId = 0xFF;
        brightness = 0xFF;
        colorR = 0xFF;
        colorG = 0xFF;
        colorB = 0xFF;
        contentId = 0xFF;
    }

    Joystick.sendState(&joyState);// Refactor - only send on joystate changed
}

void onDskyButtonChanged(void)
{
    dskyButtonChanged = true;
}

void onJoystickReceiveState(const uint8_t *data)
{
    uint8_t reportId = data[0];
    switch (reportId)
    {
    //...other reports omitted
    case 4://Dsky Button Brightness and Color, 2 bytes
    {
        btnId = data[1];
        brightness = data[2] >> 4;
        colorR = data[2] & 0x0F;
        colorG = data[3] >> 4;
        colorB = data[3] & 0x0F;
        contentId = data[4];
    }
        break;
    }
}

int convertBrightness(byte rawBrightness)
{
    switch (rawBrightness)
    {
    case 1:
        return Dsky::Brightness_OneTwentieth;
    case 2:
        return Dsky::Brightness_OneTenth;
    case 3:
        return Dsky::Brightness_OneSeventh;
    case 4:
        return Dsky::Brightness_OneFifth;
    case 5:
        return Dsky::Brightness_OneThird;
    case 6:
        return Dsky::Brightness_OneHalf;
    case 7:
        return Dsky::Brightness_Full;
    }
}

int convertColor(byte rawColor)
{
    switch (rawColor)
    {
    case 1:
        return Dsky::Color_Off;
    case 2:
        return Dsky::Color_Quarter;
    case 4:
        return Dsky::Color_Half;
    case 8:
        return Dsky::Color_Full;
    }
}

const uint8_t* convertContentId(byte contentId)
{
    switch(contentId)
    {
        case 0:
            return imageData_Stage;//Returns a 256 byte array of screen data
        //...
    }
}

Thanks Peter; I had not considered handshake. I'll try using 2 reports, one input and one output as a handshake.