diff --git a/AirSensor.cpp b/AirSensor.cpp index 0eb742f..881d949 100644 --- a/AirSensor.cpp +++ b/AirSensor.cpp @@ -1,74 +1,75 @@ // // // - #include "AirSensor.h" +// Sets the output pins to switch the charlieplexed array of LEDs. void AirSensor::changeLight(int light) { switch (light) { - case 0: - pinMode(IR_A, OUTPUT); - pinMode(IR_B, OUTPUT); - pinMode(IR_C, INPUT); + case 0: + pinMode(IR_A, OUTPUT); + pinMode(IR_B, OUTPUT); + pinMode(IR_C, INPUT); - digitalWrite(IR_A, LOW); - digitalWrite(IR_B, HIGH); - digitalWrite(IR_C, LOW); - break; - case 1: - pinMode(IR_A, OUTPUT); - pinMode(IR_B, OUTPUT); - pinMode(IR_C, INPUT); + digitalWrite(IR_A, LOW); + digitalWrite(IR_B, HIGH); + digitalWrite(IR_C, LOW); + break; + case 1: + pinMode(IR_A, OUTPUT); + pinMode(IR_B, OUTPUT); + pinMode(IR_C, INPUT); - digitalWrite(IR_A, HIGH); - digitalWrite(IR_B, LOW); - digitalWrite(IR_C, LOW); - break; - case 2: - pinMode(IR_A, INPUT); - pinMode(IR_B, OUTPUT); - pinMode(IR_C, OUTPUT); + digitalWrite(IR_A, HIGH); + digitalWrite(IR_B, LOW); + digitalWrite(IR_C, LOW); + break; + case 2: + pinMode(IR_A, INPUT); + pinMode(IR_B, OUTPUT); + pinMode(IR_C, OUTPUT); - digitalWrite(IR_A, LOW); - digitalWrite(IR_B, LOW); - digitalWrite(IR_C, HIGH); - break; - case 3: - pinMode(IR_A, INPUT); - pinMode(IR_B, OUTPUT); - pinMode(IR_C, OUTPUT); + digitalWrite(IR_A, LOW); + digitalWrite(IR_B, LOW); + digitalWrite(IR_C, HIGH); + break; + case 3: + pinMode(IR_A, INPUT); + pinMode(IR_B, OUTPUT); + pinMode(IR_C, OUTPUT); - digitalWrite(IR_A, LOW); - digitalWrite(IR_B, HIGH); - digitalWrite(IR_C, LOW); - break; - case 4: - pinMode(IR_A, OUTPUT); - pinMode(IR_B, INPUT); - pinMode(IR_C, OUTPUT); + digitalWrite(IR_A, LOW); + digitalWrite(IR_B, HIGH); + digitalWrite(IR_C, LOW); + break; + case 4: + pinMode(IR_A, OUTPUT); + pinMode(IR_B, INPUT); + pinMode(IR_C, OUTPUT); - digitalWrite(IR_A, LOW); - digitalWrite(IR_B, LOW); - digitalWrite(IR_C, HIGH); - break; - case 5: - pinMode(IR_A, OUTPUT); - pinMode(IR_B, INPUT); - pinMode(IR_C, OUTPUT); + digitalWrite(IR_A, LOW); + digitalWrite(IR_B, LOW); + digitalWrite(IR_C, HIGH); + break; + case 5: + pinMode(IR_A, OUTPUT); + pinMode(IR_B, INPUT); + pinMode(IR_C, OUTPUT); - digitalWrite(IR_A, HIGH); - digitalWrite(IR_B, LOW); - digitalWrite(IR_C, LOW); - break; - default: - turnOffLight(); - break; + digitalWrite(IR_A, HIGH); + digitalWrite(IR_B, LOW); + digitalWrite(IR_C, LOW); + break; + default: + turnOffLight(); + break; } } +// Sets all output pins to high-impedance to turn off all LEDs void AirSensor::turnOffLight() { pinMode(IR_A, INPUT); @@ -78,35 +79,43 @@ void AirSensor::turnOffLight() int AirSensor::getValue(int sensor, bool light) { - digitalWrite(MUX_A, bitRead(sensor, 0)); - digitalWrite(MUX_B, bitRead(sensor, 1)); - digitalWrite(MUX_C, bitRead(sensor, 2)); - - if(light) - { - changeLight(sensor); - } - else - { - turnOffLight(); - } + // Turn on light corresponding to read sensor + if (light) + { + changeLight(sensor); + } + else + { + turnOffLight(); + } +#ifdef IR_SENSOR_MULTIPLEXED + // Set multiplexer to corresponding sensor + digitalWrite(MUX_A, bitRead(sensor, 0)); + digitalWrite(MUX_B, bitRead(sensor, 1)); + digitalWrite(MUX_C, bitRead(sensor, 2)); + // Return sensor value return analogRead(SENSOR_IN); +#else + return analogRead(ir_sensor_pins[sensor]); +#endif } -AirSensor::AirSensor(int requiredSamples, int skippedSamples) : thresholds{10000, 10000, 10000, 10000, 10000, 10000}, calibrationSamples{0, 0, 0, 0, 0, 0}, skippedSamples{0, 0, 0, 0, 0, 0}, samplesToAcquire(requiredSamples), samplesToSkip(skippedSamples), calibrated{0, 0, 0, 0, 0, 0}, allCalibrated(false) +AirSensor::AirSensor(int requiredSamples, int skippedSamples) : thresholds{ 10000, 10000, 10000, 10000, 10000, 10000 }, calibrationSamples{ 0, 0, 0, 0, 0, 0 }, skippedSamples{ 0, 0, 0, 0, 0, 0 }, samplesToAcquire(requiredSamples), samplesToSkip(skippedSamples), calibrated{ 0, 0, 0, 0, 0, 0 }, allCalibrated(false) { + // Load config values EEPROM.get(12, deadzone); EEPROM.get(16, alpha); } +// Check if all IR sensors are calibrated. If they are, set a flag to not need to re-check it bool AirSensor::isCalibrated() { - if(!allCalibrated) + if (!allCalibrated) { - for(int i = 0; i < 6; i++) + for (int i = 0; i < 6; i++) { - if(!calibrated[i]) + if (!calibrated[i]) return false; } allCalibrated = true; @@ -116,23 +125,32 @@ bool AirSensor::isCalibrated() bool AirSensor::getSensorState(int sensor) { + // Flash the LED and read the IR sensor int value = getValue(sensor, true); turnOffLight(); - if(allCalibrated || calibrated[sensor]){ - sensorValues[sensor] = (float) value * EMA_AIRSENSOR_ALPHA + sensorValues[sensor] * (1 - EMA_AIRSENSOR_ALPHA); + // If the sensor is calibrated, Store its current filtered value. + // We are using an exponential moving average to filter out environmental noise. Setting alpha to 1 disables it. + if (allCalibrated || calibrated[sensor]) { + sensorValues[sensor] = (float)value * alpha + sensorValues[sensor] * (1 - alpha); return sensorValues[sensor] < thresholds[sensor]; } else { - if(skippedSamples[sensor] > samplesToSkip) + // If it is not calibrated, perform calibration: + // Skip the first few samples. This might not be required, but improved performance in my case. + // This might be due to wiring mistakes I made - I'm leaving the code in either way as it can't hurt. + if (skippedSamples[sensor] > samplesToSkip) { - if(value < thresholds[sensor]) thresholds[sensor] = value; - if(++calibrationSamples[sensor] > samplesToAcquire) + // Keep the minimum value seen by the sensor + if (value < thresholds[sensor]) thresholds[sensor] = value; + // If we have enough samples: + if (++calibrationSamples[sensor] > samplesToAcquire) { + // Consider the sensor calibrated. Finalize calibration for this sensor. sensorValues[sensor] = value; calibrated[sensor] = true; - thresholds[sensor] -= AIR_SENSOR_THRESHOLD_SUBTRACT; + thresholds[sensor] -= deadzone; }; } else @@ -143,13 +161,14 @@ bool AirSensor::getSensorState(int sensor) } } +// Using data from air sensors, compute the height of the player's hand, from 0 (not present) to 1 (highest possible position). float AirSensor::getHandPosition() { float total = 0; float sensorsTriggered = 0; - for(int i = 0; i < 6; i++) + for (int i = 0; i < 6; i++) { - if(getSensorState(i)) + if (getSensorState(i)) { sensorsTriggered++; total += i + 1; @@ -182,7 +201,7 @@ float AirSensor::getAlpha() void AirSensor::recalibrate() { - for(int i = 0; i < 6; i++) + for (int i = 0; i < 6; i++) { thresholds[i] = 0; calibrationSamples[i] = 0; diff --git a/AirSensor.h b/AirSensor.h index 4692043..8caf847 100644 --- a/AirSensor.h +++ b/AirSensor.h @@ -12,8 +12,9 @@ #include "PinConfig.h" #include -#define AIR_SENSOR_THRESHOLD_SUBTRACT 5 -#define EMA_AIRSENSOR_ALPHA 0.4f +#ifndef IR_SENSOR_MULTIPLEXED + int ir_sensor_pins[6] = {AIR_SENSOR_0_PIN, AIR_SENSOR_1_PIN, AIR_SENSOR_2_PIN, AIR_SENSOR_3_PIN, AIR_SENSOR_4_PIN, AIR_SENSOR_5_PIN}; +#endif class AirSensor { diff --git a/CapacitiveSensor.cpp b/CapacitiveSensor.cpp index dddd227..3f7fb55 100644 --- a/CapacitiveSensor.cpp +++ b/CapacitiveSensor.cpp @@ -5,6 +5,7 @@ http://playground.arduino.cc/Main/CapacitiveSensor Copyright (c) 2009 Paul Bagder Updates for other hardare by Paul Stoffregen, 2010-2016 + Several modifications to better suit OpeNITHM by Jonathan Montineri, 2019 vim: set ts=4: Permission is hereby granted, free of charge, to any person obtaining a @@ -43,13 +44,11 @@ CapacitiveSensor::CapacitiveSensor(uint8_t sendPin, uint8_t receivePin1, uint8_t { // initialize this instance's variables error = 1; + retVal = new unsigned int[2]; + loopTimingFactor = 310; // determined empirically - a hack - CS_Timeout_Millis = (200 * (float)loopTimingFactor * (float)F_CPU) / 16000000; - // Serial.print("timwOut = "); - // Serial.println(CS_Timeout_Millis); - // get pin mapping and port for send Pin - from PinMode function in core #ifdef NUM_DIGITAL_PINS @@ -57,20 +56,21 @@ CapacitiveSensor::CapacitiveSensor(uint8_t sendPin, uint8_t receivePin1, uint8_t if (receivePin1 >= NUM_DIGITAL_PINS) error = -1; if (receivePin2 >= NUM_DIGITAL_PINS) error = -1; #endif - pinMode(sendPin, OUTPUT); // sendpin to OUTPUT - pinMode(receivePin1, INPUT); // receivePin to INPUT - pinMode(receivePin2, INPUT); // receivePin to INPUT - sBit = PIN_TO_BITMASK(sendPin); // get send pin's ports and bitmask - sReg = PIN_TO_BASEREG(sendPin); // get pointer to output register + pinMode(sendPin, OUTPUT); // sendpin to OUTPUT + pinMode(receivePin1, INPUT); // receivePins to INPUT + pinMode(receivePin2, INPUT); + + // Get pin bitmask and registers + + sBit = PIN_TO_BITMASK(sendPin); + sReg = PIN_TO_BASEREG(sendPin); r1Bit = PIN_TO_BITMASK(receivePin1); r1Reg = PIN_TO_BASEREG(receivePin1); r2Bit = PIN_TO_BITMASK(receivePin2); r2Reg = PIN_TO_BASEREG(receivePin2); - - retVal = new unsigned int[2]; } // Public Methods ////////////////////////////////////////////////////////////// @@ -81,17 +81,18 @@ unsigned int* CapacitiveSensor::sense(uint8_t samples) total1 = 0; total2 = 0; + // This code has very strict timing - disable interrupts noInterrupts(); for (uint8_t i = 0; i < samples; i++) { - if (!SenseOneCycle()) return nullptr; // variable over timeout + if (!SenseOneCycle()) return nullptr; // Poll capacitive sensors repeatedly } interrupts(); retVal[0] = total1; retVal[1] = total2; + // Return the pair of values for the 2 sensors that were polled return retVal; - } // Private Methods ///////////////////////////////////////////////////////////// @@ -99,60 +100,61 @@ unsigned int* CapacitiveSensor::sense(uint8_t samples) int CapacitiveSensor::SenseOneCycle(void) { - - DIRECT_WRITE_LOW(sReg, sBit); // sendPin Register low - DIRECT_MODE_INPUT(r1Reg, r1Bit); // r1eceivePin to input (pullups ar1e off) - DIRECT_MODE_OUTPUT(r1Reg, r1Bit); // r1eceivePin to OUTPUT - DIRECT_WRITE_LOW(r1Reg, r1Bit); // pin is now LOW AND OUTPUT - delayMicroseconds(10); - DIRECT_MODE_INPUT(r1Reg, r1Bit); // r1eceivePin to input (pullups ar1e off) - - DIRECT_MODE_INPUT(r2Reg, r2Bit); // r2eceivePin to input (pullups ar2e off) - DIRECT_MODE_OUTPUT(r2Reg, r2Bit); // r2eceivePin to OUTPUT - DIRECT_WRITE_LOW(r2Reg, r2Bit); // pin is now LOW AND OUTPUT - delayMicroseconds(10); - DIRECT_MODE_INPUT(r2Reg, r2Bit); // r2eceivePin to input (pullups ar2e off) - DIRECT_WRITE_HIGH(sReg, sBit); // sendPin High - - while ((total1 < CS_Timeout_Millis)) { // while total is positive value - pin1State = DIRECT_READ(r1Reg, r1Bit); - pin2State = DIRECT_READ(r2Reg, r2Bit); + + DIRECT_WRITE_LOW(sReg, sBit); // sendPin Register low + DIRECT_MODE_INPUT(r1Reg, r1Bit); // receivePin to input (pullups ar1e off) + DIRECT_MODE_OUTPUT(r1Reg, r1Bit); // receivePin to OUTPUT + DIRECT_WRITE_LOW(r1Reg, r1Bit); // pin is now LOW AND OUTPUT + delayMicroseconds(10); + DIRECT_MODE_INPUT(r1Reg, r1Bit); // receivePin to input (pullups ar1e off) + + DIRECT_MODE_INPUT(r2Reg, r2Bit); // receivePin to input (pullups ar2e off) + DIRECT_MODE_OUTPUT(r2Reg, r2Bit); // receivePin to OUTPUT + DIRECT_WRITE_LOW(r2Reg, r2Bit); // pin is now LOW AND OUTPUT + delayMicroseconds(10); + DIRECT_MODE_INPUT(r2Reg, r2Bit); // receivePin to input (pullups ar2e off) + DIRECT_WRITE_HIGH(sReg, sBit); // sendPin High + + while (total1 < CS_Timeout_Millis) { // while total is positive value + // Poll both pins at once + pin1State = DIRECT_READ(r1Reg, r1Bit); + pin2State = DIRECT_READ(r2Reg, r2Bit); total1 += !pin1State; - total2 += !pin2State; + total2 += !pin2State; + + // Break once both pins are high + if(pin1State && pin2State) break; + } - if(pin1State && pin2State) break; - } - //Serial.print("SenseOneCycle(1): "); - //Serial.println(total); - if (total1 > CS_Timeout_Millis) { - return -2; // total variable over timeout - } - // set receive pin HIGH briefly to charge up fully - because the while loop above will exit when pin is ~ 2.5V - DIRECT_WRITE_HIGH(r1Reg, r1Bit); - DIRECT_MODE_OUTPUT(r1Reg, r1Bit); // receivePin to OUTPUT - pin is now HIGH AND OUTPUT - DIRECT_MODE_OUTPUT(r2Reg, r2Bit); // receivePin to OUTPUT - pin is now HIGH AND OUTPUT - DIRECT_WRITE_HIGH(r1Reg, r1Bit); - DIRECT_WRITE_HIGH(r2Reg, r2Bit); - DIRECT_MODE_INPUT(r1Reg, r1Bit); // receivePin to INPUT (pullup is off) - DIRECT_MODE_INPUT(r2Reg, r2Bit); // receivePin to INPUT (pullup is off) - DIRECT_WRITE_LOW(sReg, sBit); // sendPin LOW - - while ( (total1 < CS_Timeout_Millis) ) { // while receive pin is HIGH AND total is less than timeout - pin1State = DIRECT_READ(r1Reg, r1Bit); - pin2State = DIRECT_READ(r2Reg, r2Bit); - - total1 += pin1State; - total2 += pin2State; - - if(!(pin1State || pin2State)) break; - } - //Serial.print("SenseOneCycle(2): "); - //Serial.println(total); - if (total1 >= CS_Timeout_Millis) { - return -2; // total variable over timeout - } else { - return 1; + return -2; // We timed out - should never happen with this implementation + } + + // set receive pin HIGH briefly to charge up fully - because the while loop above will exit when pin is ~ 2.5V + DIRECT_WRITE_HIGH(r1Reg, r1Bit); + DIRECT_MODE_OUTPUT(r1Reg, r1Bit); // receivePin to OUTPUT - pin is now HIGH AND OUTPUT + DIRECT_MODE_OUTPUT(r2Reg, r2Bit); // receivePin to OUTPUT - pin is now HIGH AND OUTPUT + DIRECT_WRITE_HIGH(r1Reg, r1Bit); + DIRECT_WRITE_HIGH(r2Reg, r2Bit); + DIRECT_MODE_INPUT(r1Reg, r1Bit); // receivePin to INPUT (pullup is off) + DIRECT_MODE_INPUT(r2Reg, r2Bit); // receivePin to INPUT (pullup is off) + DIRECT_WRITE_LOW(sReg, sBit); // sendPin LOW + + // Same loop as above, but measuring capacitor discharge time instead + while ( (total1 < CS_Timeout_Millis) ) { + pin1State = DIRECT_READ(r1Reg, r1Bit); + pin2State = DIRECT_READ(r2Reg, r2Bit); + + total1 += pin1State; + total2 += pin2State; + + if(!(pin1State || pin2State)) break; + } + + if (total1 >= CS_Timeout_Millis) { + return -2; + } else { + return 1; } } diff --git a/OpeNITHM.ino b/OpeNITHM.ino index baade89..9893d6e 100644 --- a/OpeNITHM.ino +++ b/OpeNITHM.ino @@ -32,6 +32,8 @@ void onKeyPress(int key, bool wasHeld) keyStates[key] = true; } +// Parse configuration command. For now, a serial terminal is required (like the monitor in Arduino IDE) +// Eventually I will make a config tool void parseCommand() { char input1 = Serial.read(); @@ -94,21 +96,27 @@ void setup() { FastLED.addLeds(leds, 16); + // Set LEDs blue for (CRGB& led : leds) { led = 0x0000FF; FastLED.show(); } + // Initialize and calibrate touch sensors touchboard = new Touchboard(onKeyPress); + // Set LEDs red for (CRGB& led : leds) { led = 0xFF0000; FastLED.show(); } + // Initialize air sensor - will automatically calibrate as it starts being read sensor = new AirSensor(500, 50); + + // Initialize relevant output method / USB or serial #ifdef USB output = new USBOutput(); #else @@ -117,6 +125,13 @@ void setup() { } void loop() { + // Process config commands + if (Serial.available()) + { + parseCommand(); + } + + // If currently paused through a config command, do not execute main loop if (!activated) return; // Scan touch keyboard and update lights @@ -125,12 +140,15 @@ void loop() { { if (lightIntensity[i] > 0.05f) lightIntensity[i] -= 0.05f; + + // If the key is currently being held, set its color to purple if (touchboard->update(i)) { leds[i].setRGB(128 + 127 * lightIntensity[i], 0, 128 + 127 * lightIntensity[i]); } else { + // If not, make it yellow and send the "key released" event if it was previously pressed leds[i].setRGB(128, 128, 0); if (keyStates[i]) { @@ -140,6 +158,7 @@ void loop() { } } + // Process air sensor hand position const float newPosition = sensor->getHandPosition(); if (newPosition != sensorPosition) { @@ -148,13 +167,9 @@ void loop() { } - // If the air sensor is calibrated, begin updating lights. The air sensor will automatically calibrate as it is being polled. + // If the air sensor is calibrated, update lights. The lights will stay red as long as the air sensor is not calibrated. if (sensor->isCalibrated()) FastLED.show(); - if (Serial.available()) - { - parseCommand(); - } } diff --git a/PinConfig.h b/PinConfig.h index 004580b..e6ac858 100644 --- a/PinConfig.h +++ b/PinConfig.h @@ -1,5 +1,8 @@ #pragma once +#define IR_SENSOR_MULTIPLEXED +#define CALIBRATION_SAMPLES 500 + // Multiplexer pin settings #define MUX_A 9 #define MUX_B 8 @@ -10,7 +13,16 @@ #define IR_B 4 #define IR_C 3 -#define SENSOR_IN A0 +#ifdef IR_SENSOR_MULTIPLEXED + #define SENSOR_IN A0 +#else + #define AIR_SENSOR_0_PIN 4 + #define AIR_SENSOR_1_PIN 5 + #define AIR_SENSOR_2_PIN 6 + #define AIR_SENSOR_3_PIN 7 + #define AIR_SENSOR_4_PIN 8 + #define AIR_SENSOR_5_PIN 9 +#endif // Capsense pin settings #define KEYBOARDPIN_1 10 diff --git a/SerialOutput.cpp b/SerialOutput.cpp index 111d398..a1606f4 100644 --- a/SerialOutput.cpp +++ b/SerialOutput.cpp @@ -4,6 +4,10 @@ #include "SerialOutput.h" +// Workaround for microcontrollers without USB functionality +// use a serial line to talk to a UC with USB, which will then act as a USB peripheral +// Only needed because I didn't receive my Atmega328p until late in the project + void SerialOutput::sendKeyEvent(int key, bool pressed, bool doublePressed) { builtPacket.data = 0; diff --git a/Touchboard.cpp b/Touchboard.cpp index 2d49217..efdd212 100644 --- a/Touchboard.cpp +++ b/Touchboard.cpp @@ -2,6 +2,7 @@ void Touchboard::scan() { + // For each key, set multiplexers and poll both capacitive sensors simultaneously for (int i = 0; i < 8; i++) { digitalWrite(MUX_A, bitRead(i, 0)); @@ -9,6 +10,8 @@ void Touchboard::scan() digitalWrite(MUX_C, bitRead(i, 2)); unsigned int* values = sensor.sense(3); + + // Store the values received from the sensor poll into their respective positions keys[i] = values[0]; keys[i + 8] = values[1]; } @@ -16,6 +19,7 @@ void Touchboard::scan() void Touchboard::calibrateKeys() { + // Reset calibration data for all keys for (int i = 0; i < 16; i++) { em_averages[i] = 0; @@ -24,12 +28,15 @@ void Touchboard::calibrateKeys() keys[i] = false; } - for (int i = 0; i < 100; i++) { + for (int i = 0; i < CALIBRATION_SAMPLES; i++) { + // Repeatedly scan all keys scan(); + // Store the lowest read value as our baseline for (int j = 0; j < 16; j++) { if (keys[j] > neutral_values[j]) neutral_values[j] = keys[j]; } } + // After calibration is complete, initialize the averages to the baseline values established previously for (int i = 0; i < 16; i++) { em_averages[i] = neutral_values[i]; } @@ -49,8 +56,8 @@ bool Touchboard::update(int key) key_states[key] = false; } else - //If we are outside of the deadzone: { + //If we are outside of the deadzone: if (read_value > em_averages[key] + threshold) { // If we just detected a touch, make the key touched, discard the previous moving average and trigger the callback.