Search code examples
arduinoasciiarduino-c++

Arduino bluetooth keyboard immolator sending unexpected ASCII characters


I have two buttons attached to accelerometers and Adafruit Bluefruit nRF52 Feather boards. I have written a program in the Arduino IDE to basically read acceleration during a button press, map the acceleration to a scale of 0-4, and then send a corresponding character (over Bluetooth) to my laptop. Button one simply sends 0, 1, 2, 3, or 4; and button two should send q, w, e, r, or t but herein lies the problem. With the following code:

/*********************************************************************
Target = Adafruit NRF52 Feather
Button wired to A4/Pin28
LED wired to A2/Pin4
6050 IMU sensor wired to I2C 
*********************************************************************/

/*
 *  map 0-30.0 to 0-4 
 *  
 *  Device 1 gets 0,1,2,3,4
 *  Device 2 gets q,w,e,r,t
 */
 
#include <bluefruit.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>

#define ACCEL_BUFFER_SIZE     50  // was 100 but really the interesting stuff happens in the last 20 or so samples
#define SENSOR_UPDATE_DELAY   1   // ms = 1000Sps
#define KEYPRESS_DELAY        4   // how long between keypress down and back up.  was 5

#define BUTTON_PIN            28// A4
#define LED_PIN               4 // A2

#define BOX_NUMBER_SWITCH     16
#define REPEAT_BUTTON_DELAY   500 // numebr fo milliseconds delay min between registered presses.

#define MIN_ACCEL 0.0
#define MAX_ACCEL 30.0

#define DEBUG

// Define bit mapping for key descriptors.
//  Kana    Compose   Scroll   Caps   Num
#define KEYMASK_KANA      0x16  
#define KEYMASK_COMPOSE   0x08  
#define KEYMASK_SCROLLOCK 0x04  
#define KEYMASK_CAPSLOCK  0x02  
#define KEYMASK_NUMLOCK   0x01  

#define OFFSET            10.7  // Offset in read value to remove to zero things out.
#define POST_SAMPLES      3     // number of samples to collect after button press.  this adds Xms delay to button press timing.

Adafruit_MPU6050  mpu;                      // IMU / Accelerometer
BLEDis            bledis;                   // Bluetooth descriptor characteristic
BLEHidAdafruit    blehid;                   // Bluetooth HID characteristic.

double prevAccelVal = 0;                    // Store our previously read value to allow for a little smoothing
double accelBuffer[ACCEL_BUFFER_SIZE];      // Circular buffer for accelerometer data.
unsigned int accelBufferUpto = 0;           // Pointer to current buffer position.
unsigned long lastSensorReadingTime = 0;    // Last time we read accelerometer data.
bool hasKeyPressed = false;                 //
bool currLedState = false;                  // Current state of LED
//char txChar = '>';                          // Character to send to signify button press. not used.
unsigned char boxNumber = 0;                // Our own box number,  will be set to 1 or 2
char numbers[] = {'0','1','2','3','4','q','w','e','r','t'};       // Little cheat to help us get ascii characters from integers...
unsigned long lastButtonPress = 0;                                // Record time of last button press.
unsigned char ledCmdMask[] = {KEYMASK_CAPSLOCK,KEYMASK_NUMLOCK};  // Set the key descriptors to look out for per box number

/*
 * Read state of slide switch to decide which box number we are.
 */
unsigned char readBoxNumber()
{
  unsigned char ret = 0;
  pinMode(BOX_NUMBER_SWITCH,INPUT);         // Make sure GPIO is set as an input
  if(digitalRead(BOX_NUMBER_SWITCH) == LOW) // If its LOW....
  {
    ret = 1;                                  // we are box #1
  }
  else                                      // Else...
  {
    ret = 2;                                  // we are box #2
  }
  return ret;
}

/* 
 * Initialise the IMU 
 */
void initImu()
{
  // Try to initialize!
  if (!mpu.begin()) 
  {
    while (1)   // death loop on failure.
    {
      Serial.println("Failed to find MPU6050 chip");
      delay(10000);
    }
  }
  mpu.setAccelerometerRange(MPU6050_RANGE_16_G);  // Make sure high bandwidth
  mpu.setGyroRange(MPU6050_RANGE_500_DEG);        // we are not using gyro anyway...
  mpu.setFilterBandwidth(MPU6050_BAND_260_HZ);    // 260HZ should be ok i think?!
}

/* 
 *  Initialise Led
 */
void initLed()
{
  pinMode(LED_PIN,OUTPUT);
}

/* 
 *  Set value of Led
 */
void setLed(bool ledState)
{
  if(ledState == true)
  {
    digitalWrite(LED_PIN,HIGH);   // Set LED on.
  }
  else
  {
    digitalWrite(LED_PIN,LOW);    // Set LED off.
  }
}

/* 
 *  Take a readings from accelerometer,  calc absolute value and add to array.
 */
void updateReading()
{
  sensors_event_t a, g, temp;
  mpu.getEvent(&a, &g, &temp);

  /*
   * We want absolute magnitude of acceleration such that force works in any orientation.
   * We should do:
   * AccelMagnitude = SQRT (AccellX^2 + AccellY^2 + AccellZ^2)
   */
  double accelMagnitudeS = 0.0;                 
  accelMagnitudeS = (a.acceleration.x * a.acceleration.x);
  accelMagnitudeS += (a.acceleration.y * a.acceleration.y);
  accelMagnitudeS += (a.acceleration.z * a.acceleration.z);

  double accelMagnitude = sqrt(accelMagnitudeS);      // Take the square root to get total magnitude.
  accelMagnitude = abs(accelMagnitude-OFFSET);        // Remove offset and take absolute value - we done care about acceleration direction.
  double accelMagnitudeLpf = ((accelMagnitude *0.75) + (prevAccelVal * 0.25));  // low pass value by using 3/4 of current value added to 1/4 previous
  accelBuffer[accelBufferUpto] = accelMagnitudeLpf;   // Store our new value 
  prevAccelVal = accelMagnitudeLpf;                   // Save previous value as our new value for next time.
  accelBufferUpto++;                                  // increment and wrap our circular buffer pointer.
  if(accelBufferUpto >= ACCEL_BUFFER_SIZE)
    accelBufferUpto = 0;
}

/* 
 *  initialise the button.
 */
void initButton()
{
  pinMode(BUTTON_PIN,INPUT);
}

/* 
 *  check if button is pressed.  return true if yes,  false if not.
 */
bool isButtonPressed()
{
  if(digitalRead(BUTTON_PIN) == LOW)  // button has been pressed.
  {
    return true;          
  }
  return false;
}

/* 
 *  Find the maximum value in our acceleration data buffer.
 */
double findMaxValue()
{
  double maxVal = 0.0;
  for(int i=0;i<ACCEL_BUFFER_SIZE;i++)    // Loop through our buffer
  {
    if(accelBuffer[i] > maxVal)           // if current value is bigger than our stored max...
    { 
      maxVal = accelBuffer[i];                // update our maximum value.
    }
  }
  return maxVal;
}

// print out the array for test purposes
// start at accelBufferUpto+1 and loop back circular buffer.
void printAccelArray()
{
  unsigned int arrayPointer = accelBufferUpto;  // We want to start printing from the upto value not start of array
                                                // so that the printed values are aligned newest value last.
  for(int i=0;i<ACCEL_BUFFER_SIZE;i++)          // Loop through array
  {
    Serial.println(accelBuffer[arrayPointer]);    // Print current value.
    arrayPointer = arrayPointer + 1;              // increment and wrap array  upto pointer.
    if(arrayPointer >= ACCEL_BUFFER_SIZE)
      arrayPointer = 0;
  }
}

/* 
 * Setup 
 */
void setup() 
{
  Serial.begin(115200);
  //while ( !Serial ) delay(10);   // for nrf52840 with native usb

  Serial.println("Button smashhhhh...");
  Serial.println("--------------------------------\n");

  initButton();         // Initialise Button gpio
  initImu();            // Initialise IMU
  initLed();            // Initialise LED GPIO
  boxNumber = readBoxNumber();  //Work out which boix number we are.

  Serial.print("Box number: ");
  Serial.println(boxNumber);

  Bluefruit.begin();
  Bluefruit.setTxPower(4);    // Check bluefruit.h for supported values

  // Configure and Start Device Information Service
  bledis.setManufacturer("UOSA");
  bledis.setModel("SmashyButton");
  bledis.begin();

  /* Start BLE HID
   * Note: Apple requires BLE device must have min connection interval >= 20m ( The smaller the connection interval the faster we could send data).
   * However for HID and MIDI device, Apple could accept min connection interval  up to 11.25 ms. Therefore BLEHidAdafruit::begin() will try to set the min and max
   * connection interval to 11.25  ms and 15 ms respectively for best performance.
   */
  blehid.begin();

  blehid.setKeyboardLedCallback(set_keyboard_led);  // Set callback for set LED from central

  /* Set connection interval (min, max) to your perferred value.
   * Note: It is already set by BLEHidAdafruit::begin() to 11.25ms - 15ms
   * min = 9*1.25=11.25 ms, max = 12*1.25= 15 ms 
   */
  /* Bluefruit.Periph.setConnInterval(9, 12); */

  // Set up and start advertising
  startAdv();
}

/* 
 * Start bluetooth advertising 
 */
void startAdv(void)
{  
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();
  Bluefruit.Advertising.addAppearance(BLE_APPEARANCE_HID_KEYBOARD);
  
  // Include BLE HID service
  Bluefruit.Advertising.addService(blehid);

  // There is enough room for the dev name in the advertising packet
  Bluefruit.Advertising.addName();
  
  /* Start Advertising
   * - Enable auto advertising if disconnected
   * - Interval:  fast mode = 20 ms, slow mode = 152.5 ms
   * - Timeout for fast mode is 30 seconds
   * - Start(timeout) with timeout = 0 will advertise forever (until connected)
   * 
   * For recommended advertising interval
   * https://developer.apple.com/library/content/qa/qa1931/_index.html   
   */
  Bluefruit.Advertising.restartOnDisconnect(true);
  Bluefruit.Advertising.setInterval(32, 244);    // in unit of 0.625 ms
  Bluefruit.Advertising.setFastTimeout(30);      // number of seconds in fast mode
  Bluefruit.Advertising.start(0);                // 0 = Don't stop advertising after n seconds
}

/* 
 *  send a keypress over bluetooth HID
 */
void sendKeypress(char ch)
{
    blehid.keyPress(ch);
    delay(KEYPRESS_DELAY);    // Delay a bit after a report
    blehid.keyRelease();
}

/*  
 *  loop 
 */
void loop() 
{
  unsigned long currTime = millis();                          // Check millis time.
  if(currTime >= (lastSensorReadingTime+SENSOR_UPDATE_DELAY)) // if its time to udpate accelerometer readings...
  {
    updateReading();                                          // update....
    lastSensorReadingTime = millis();
  }

  unsigned long timeSinceLastButtonPress = millis() - lastButtonPress;  // Calculate numebr of ms since last button press.
  if (isButtonPressed() == true && (timeSinceLastButtonPress >= REPEAT_BUTTON_DELAY)) // check if button has been pressed and enough time has elapsed
  {

    for(int i=0;i<POST_SAMPLES;i++)   // collect 5 more samples...
    {
      delay(1);         // this is crude!
      updateReading();   
    }
    
    double maxVal = findMaxValue();                                   // Get max acceleration value
    int intMaxVal = (int)min(maxVal,MAX_ACCEL);                       // cap it to defined max to prevent map from doing something stupid
    unsigned int newMaxVal = map(intMaxVal,MIN_ACCEL,MAX_ACCEL,0,4);  // Map our range onto 0-4.
    newMaxVal = min(newMaxVal,4);                                     // make sure we are max 4!
    if(boxNumber == 2)                                                // add offset for box number 2.
    {
      if(newMaxVal == 0)
      {
        newMaxVal = 'q';
      }
      else if(newMaxVal == 1)
      {
        newMaxVal = 'w';
      }
      else if(newMaxVal == 2)
      {
        newMaxVal = 'e';
      }
      else if(newMaxVal == 3)
      {
        newMaxVal = 'r';
      }
      else if(newMaxVal == 4)
      {
        newMaxVal = 't';
      }
    }
    sendKeypress(numbers[newMaxVal]);                                 // Send out keypress
    lastButtonPress = millis();                                       // Update our recorded last keypress time

    #ifdef DEBUG                                                      // Print stuff for debug...  comment out DEBUG define to remove.
    printAccelArray();
    Serial.print("Max val:");     Serial.println(maxVal);
    Serial.print("New max val:"); Serial.println(newMaxVal);
    #endif
  }
}

/**
 * Callback invoked when received Set LED from central.
 * Must be set previously with setKeyboardLedCallback()
 *
 * The LED bit map is as follows: (also defined by KEYBOARD_LED_* )
 *    Kana (4) | Compose (3) | ScrollLock (2) | CapsLock (1) | Numlock (0)
 */
void set_keyboard_led(uint16_t conn_handle, uint8_t led_bitmap)
{
  (void) conn_handle;

  if ( led_bitmap & ledCmdMask[(boxNumber-1)] ) // dont forget to remove box number offset (array start at zero - we use box 1 or 2)
  {
    setLed(true);   // Set led on                             
  }
  else
  {
    setLed(false);  // Set led off
  }
}

I get the expected behaviour out of button one, but I only receive the "|" (long line) character from button two. I have never worked in C++ or with Arduino before. I had help initially making the code (which used only numbers 0-9), but now I am on my own... I think the key spots in the code are

char numbers[] = {'0','1','2','3','4','q','w','e','r','t'};

and

    if(boxNumber == 2)                                                // add offset for box number 2.
    {
      if(newMaxVal == 0)
      {
        newMaxVal = 'q';
      }
      else if(newMaxVal == 1)
      {
        newMaxVal = 'w';
      }
      else if(newMaxVal == 2)
      {
        newMaxVal = 'e';
      }
      else if(newMaxVal == 3)
      {
        newMaxVal = 'r';
      }
      else if(newMaxVal == 4)
      {
        newMaxVal = 't';
      }
    }

If I replace the letters in these key spots with the numbers 5-9, the program runs as expected. For reasons I won't bore you with, I need numbers from one button and letters from the other. Does anyone know how to make this code work properly?


Solution

  • The problem is here

    sendKeypress(numbers[newMaxVal]);
    

    You are accessing the numbersArray with newMaxVal which in the case of button two is not an integer value but a character.

    You say that it works if you replace

      if(newMaxVal == 0)
      {
        newMaxVal = 'q';
      }
    

    with

      if(newMaxVal == 0)
      {
        newMaxVal = 5;
      }
    

    etc. So why don't you do that? It seems like the correct solution. Why do say that it works as expected, yet for some reason this is unacceptable?

    EDIT

    So based on the comment below this seems possible

    char numbers[2][8] = {
        {'1','2','3','4','5','6','7','8'},
        {'q','w','e','r','t','y','u','i'},
    };
    

    then

    sendKeypress(numbers[boxNumber - 1][newMaxVal - 1]);
    

    No need for any complicated if this and if that logic. Just one line of code.