Last active
April 18, 2023 23:49
-
-
Save Pomax/da2e626c092bfd815f18f4cde6378f5a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "Adafruit_seesaw.h" | |
#include <seesaw_neopixel.h> | |
#include <Joystick.h> | |
#include <Adafruit_GFX.h> | |
#include <Adafruit_SSD1306.h> | |
#define SS_SWITCH 24 // this is the pin on the encoder that connects to the momentary switch | |
#define SEESAW_BASE_ADDR 0x36 // I2C address, starts with 0x36 | |
#define SCREEN_WIDTH 128 // OLED display width, in pixels | |
#define SCREEN_HEIGHT 32 // OLED display height, in pixels | |
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) | |
#define SCREEN_ADDRESS 0x3C // <- See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 | |
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); | |
// create 6 encoders | |
#define ENCODER_COUNT 6 | |
uint8_t button_count = ENCODER_COUNT * 3; | |
Adafruit_seesaw encoders[ENCODER_COUNT]; | |
// Our trim increment values for the coarse and fine knobs. | |
uint8_t encoder_increment[] = { | |
5, // coarse pitch increment | |
5, // coarse aileron increment | |
5, // coarse rudder increment | |
1, // fine increments | |
1, | |
1 | |
}; | |
// We use this to set the "neutral" value. | |
uint16_t encoder_center[] = { | |
1000, // pitch center | |
1000, // aileron center | |
1000, // rudder center | |
1000, 1000, | |
1000 | |
}; | |
// Set the encoders output values to neutral on startup | |
int32_t encoder_positions[] = {0, 0, 0, 0, 0, 0}; | |
// List of encoders found connected via I2C | |
bool found_encoders[] = {false, false, false, false, false, false}; | |
// And a list of bools that indicate whether an encoder's switch is being pressed. | |
bool encoder_down[] = {false, false, false, false, false, false}; | |
// In addition to acting as rotary dial, we als dual-purpose them as | |
// button presses, so that rotating the dial also triggers [down,reset,up] | |
// buttons. | |
uint8_t down_buttons[] = {0, 3, 6, 0, 3}; | |
uint8_t reset_buttons[] = {1, 4, 7, 1, 4}; | |
uint8_t up_buttons[] = {2, 5, 8, 2, 5}; | |
// We set different button-press durations for coarse vs fine controls. | |
uint8_t buttons_durations[] = { | |
300, 25, 300, // pitch | |
300, 25, 300, // aileron | |
300, 25, 300, // rudder | |
50, 25, 50, // fine values | |
50, 25, 50, | |
50, 25, 50 | |
}; | |
// In order to measure those button presses, we need to | |
// make sure we record when button presses start: | |
long last_pressed[] = { | |
0, 0, 0, | |
0, 0, 0, | |
0, 0, 0, | |
0, 0, 0, | |
0, 0, 0, | |
0, 0, 0 | |
}; | |
// Next up, our USB joystick configuration, as per | |
// https://github.com/MHeironimus/ArduinoJoystickLibrary#joystick_ | |
Joystick_ joystick( | |
/* hidReportId */ 3, | |
/* joystickType */ 4, // this will register as normal joystick. | |
/* buttonCount */ 9, // three buttons for "up, reset, down" per encoder pair | |
/* hatSwitchCount */ 0, | |
/* includeXAxis */ false, | |
/* includeYAxis */ false, | |
/* includeZAxis */ false, | |
/* includeRxAxis */ true, // pitch trim axis | |
/* includeRyAxis */ true, // aileron trim axis | |
/* includeRzAxis */ true, // rudder trim axis | |
/* includeRudder */ false, | |
/* includeThrottle */ false, | |
/* includeAccelerator */ false, | |
/* includeBrake */ false, | |
/* includeSteering */ false | |
); | |
// Our "boot" function | |
void setup() { | |
Serial.begin(115200); | |
bindEncoders(); | |
setupJoystick(); | |
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally | |
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { | |
Serial.println(F("SSD1306 allocation failed")); | |
for (;;); // Don't proceed, loop forever | |
} | |
display.clearDisplay(); | |
display.setTextSize(1); | |
display.setTextColor(SSD1306_WHITE); | |
display.cp437(true); | |
display.setCursor(0, 0); | |
drawStringToScreen(" trim controller"); | |
display.display(); | |
} | |
// Find and bind our six encoders | |
void bindEncoders() { | |
for (uint8_t enc = 0; enc < ENCODER_COUNT; enc++) { | |
String s = enc == 0 ? "0" : enc == 1 ? "1" : "2"; | |
// See if we can find encoders on this address | |
if (! encoders[enc].begin(SEESAW_BASE_ADDR + enc)) { | |
Serial.print("Couldn't find encoder " + s); | |
} else { | |
Serial.print("Found encoder + pixel " + s); | |
uint32_t version = ((encoders[enc].getVersion() >> 16) & 0xFFFF); | |
if (version != 4991) { | |
Serial.print("Wrong firmware loaded? "); | |
Serial.println(version); | |
while (1) delay(10); | |
} | |
Serial.println("Found Product 4991"); | |
// use a pin for the built in encoder switch | |
encoders[enc].pinMode(SS_SWITCH, INPUT_PULLUP); | |
// get starting position | |
encoders[enc].setEncoderPosition(0); | |
Serial.println("Turning on interrupts"); | |
delay(10); | |
encoders[enc].setGPIOInterrupts((uint32_t)1 << SS_SWITCH, 1); | |
encoders[enc].enableEncoderInterrupt(); | |
found_encoders[enc] = true; | |
} | |
} | |
Serial.println("Encoders started"); | |
} | |
// Bind the encoders to USB joystick functions | |
void setupJoystick() { | |
joystick.begin(true); | |
joystick.setRxAxisRange(0, 2 * encoder_center[0]); | |
joystick.setRxAxis(encoder_center[0]); | |
joystick.setRyAxis(encoder_center[1]); | |
joystick.setRyAxisRange(0, 2 * encoder_center[1]); | |
joystick.setRzAxis(encoder_center[2]); | |
joystick.setRzAxisRange(0, 2 * encoder_center[2]); | |
} | |
// And finally, our "do this forever" code, set to run | |
// every 50ms. Of course, you're entirely free to drop | |
// that number a much smaller millisecond value. | |
void loop() { | |
checkButtonRelease(); | |
for (uint8_t enc = 0; enc < ENCODER_COUNT; enc++) { | |
if (!found_encoders[enc]) continue; | |
processPosition(enc, encoder_positions[enc], encoders[enc].getEncoderPosition()); | |
checkEncoderSwitch(enc); | |
} | |
drawValuesToScreen(); | |
yield(); | |
delay(50); | |
} | |
// Check whether we need to update an encoder's output value... | |
void processPosition(uint8_t enc, int32_t old_position, int32_t new_position) { | |
if (old_position == new_position) return; | |
encoder_positions[enc] = new_position; | |
uint16_t axis_value = 0; | |
uint8_t ctrl = enc % 3; | |
int8_t other = 3; | |
if (enc > ctrl) other = -3; | |
axis_value = encoder_center[enc] + encoder_increment[enc] * encoder_positions[enc] + encoder_increment[enc + other] * encoder_positions[enc + other]; | |
// update the "joystick" with the new value | |
if (ctrl == 0) { | |
joystick.setRxAxis(axis_value); | |
} | |
else if (ctrl == 1) { | |
joystick.setRyAxis(axis_value); | |
} | |
else if (ctrl == 2) { | |
joystick.setRzAxis(axis_value); | |
} | |
if (old_position < new_position) { | |
pressButton(up_buttons[enc]); | |
} else { | |
pressButton(down_buttons[enc]); | |
} | |
} | |
// And check whether an encoder is being pressed, triggering its switch | |
void checkEncoderSwitch(uint8_t enc) { | |
bool pressed = !encoders[enc].digitalRead(SS_SWITCH); | |
if (pressed && !encoder_down[enc]) { | |
encoder_down[enc] = true; | |
encoder_positions[enc] = 0; | |
encoders[enc].setEncoderPosition(0); | |
uint8_t ctrl = enc % 3; | |
int8_t other = 3; | |
if (enc > ctrl) { | |
other = -3; | |
} | |
encoder_positions[enc + other] = 0; | |
encoders[enc + other].setEncoderPosition(0); | |
pressButton(reset_buttons[enc]); | |
uint16_t center_value = encoder_center[enc]; | |
processPosition(enc, 9999, 0); | |
} | |
else if (!pressed && encoder_down[enc]) { | |
encoder_down[enc] = false; | |
} | |
} | |
// A little helper function that signals a | |
// button press on our USB joystick driver. | |
void pressButton(uint8_t button) { | |
joystick.pressButton(button); | |
last_pressed[button] = millis(); | |
} | |
// And a helper function to check whether we | |
// need to *let go* of our virtual button, too =) | |
void checkButtonRelease() { | |
long now = millis(); | |
for (uint8_t i = 0; i < button_count; i++) { | |
if (now - last_pressed[i] > buttons_durations[i]) { | |
joystick.releaseButton(i); | |
} | |
} | |
} | |
// And finally, a helper function to show our current trim values on the display. | |
void drawValuesToScreen() { | |
display.clearDisplay(); | |
// We get four lines of text, so we can use one line to say what this is... | |
display.setCursor(0, 0); | |
drawStringToScreen(" trim controller"); | |
// and then use the other three lines for our trim values: | |
for (int8_t i = 0; i < 3; i++) { | |
display.setCursor(0, 8 * (1 + i)); | |
String label = ""; | |
if (i == 0) label = "pitch: "; | |
if (i == 1) label = "yaw : "; | |
if (i == 2) label = "roll : "; | |
String dval = String(encoder_increment[i] * encoder_positions[i] + encoder_increment[i + 3] * encoder_positions[i + 3]); | |
while (dval.length() < 14) dval = " " + dval; | |
drawStringToScreen(label + dval); | |
} | |
display.display(); | |
} | |
// With a helper function to help the helper function actually draw text. | |
void drawStringToScreen(String str) { | |
int16_t slen = str.length() + 1; | |
char letters[slen]; | |
str.toCharArray(letters, slen); | |
for (int16_t i = 0; i < slen; i++) display.write(letters[i]); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment