Initial commit of mostly working library.
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
docs
|
||||
.vscode
|
||||
@ -1,3 +1,10 @@
|
||||
# Sitka Instruments Gravity Firmware Abstraction
|
||||
|
||||
TODO
|
||||
This library helps make writing firmware easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library.
|
||||
|
||||
## Required Third-party Libraries
|
||||
|
||||
* [uClock](https://github.com/midilab/uClock) [MIT] - Handle clock tempo, external clock input, and internal clock timer handler.
|
||||
* [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation.
|
||||
* [Adafruit_GFX](https://github.com/adafruit/Adafruit-GFX-Library) [BSD] - Graphics helper library.
|
||||
* [Adafruit_SSD1306](https://github.com/adafruit/Adafruit_SSD1306) [BSD] - Library for interacting with the SSD1306 OLED display.
|
||||
|
||||
107
button.h
Normal file
107
button.h
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @file button.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief for interacting with trigger / gate inputs.
|
||||
* @version 0.1
|
||||
* @date 2025-04-20
|
||||
*
|
||||
* Provide methods to convey curent state (HIGH / LOW) and change in state (disengaged, engageing, engaged, disengaging).
|
||||
*
|
||||
* @copyright Copyright (c) 2025
|
||||
*
|
||||
*/
|
||||
#ifndef BUTTON_H
|
||||
#define BUTTON_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
class Button {
|
||||
protected:
|
||||
typedef void (*CallbackFunction)(void);
|
||||
CallbackFunction on_press;
|
||||
|
||||
public:
|
||||
// Enum constants for active change in button state.
|
||||
enum ButtonChange {
|
||||
CHANGE_UNCHANGED,
|
||||
CHANGE_PRESSED,
|
||||
CHANGE_RELEASED,
|
||||
};
|
||||
|
||||
Button() {}
|
||||
Button(int pin) { Init(pin); }
|
||||
~Button() {}
|
||||
|
||||
/**
|
||||
* Initializes a CV Input object.
|
||||
*
|
||||
* @param pin gpio pin for the cv output.
|
||||
*/
|
||||
void Init(uint8_t pin) {
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
pin_ = pin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a handler function for executing when button is pressed.
|
||||
*
|
||||
* @param f Callback function to attach push behavior to this button.
|
||||
*/
|
||||
void AttachPressHandler(CallbackFunction f) {
|
||||
on_press = f;
|
||||
}
|
||||
|
||||
// Execute the press callback.
|
||||
void OnPress() {
|
||||
if (on_press != NULL) {
|
||||
on_press();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the state of the cv input.
|
||||
*/
|
||||
void Process() {
|
||||
int read = digitalRead(pin_);
|
||||
|
||||
bool debounced = (millis() - last_press_) > DEBOUNCE_MS;
|
||||
bool pressed = read == 0 && old_read_ == 1 && debounced;
|
||||
bool released = read == 1 && old_read_ == 0 && debounced;
|
||||
// Update variables for next loop
|
||||
last_press_ = (pressed || released) ? millis() : last_press_;
|
||||
|
||||
// Determine current clock input state.
|
||||
change_ = CHANGE_UNCHANGED;
|
||||
if (pressed) {
|
||||
change_ = CHANGE_PRESSED;
|
||||
on_ = true;
|
||||
} else if (released) {
|
||||
change_ = CHANGE_RELEASED;
|
||||
on_ = false;
|
||||
}
|
||||
old_read_ = read;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state change for the button.
|
||||
*
|
||||
* @return ButtonChange
|
||||
*/
|
||||
inline ButtonChange Change() { return change_; }
|
||||
|
||||
/**
|
||||
* Current cv state represented as a bool.
|
||||
*
|
||||
* @return true if cv signal is high, false if cv signal is low
|
||||
*/
|
||||
inline bool On() { return on_; }
|
||||
|
||||
private:
|
||||
uint8_t pin_;
|
||||
uint8_t old_read_;
|
||||
unsigned long last_press_;
|
||||
ButtonChange change_ = CHANGE_UNCHANGED;
|
||||
bool on_;
|
||||
};
|
||||
|
||||
#endif
|
||||
92
clock.h
Normal file
92
clock.h
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @file clock.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Wrapper Class for clock timing functions.
|
||||
* @version 0.1
|
||||
* @date 2025-05-04
|
||||
*
|
||||
* @copyright Copyright (c) 2025
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CLOCK_H
|
||||
#define CLOCK_H
|
||||
|
||||
#include <uClock.h>
|
||||
|
||||
#include "peripherials.h"
|
||||
|
||||
const int DEFAULT_TEMPO = 120;
|
||||
|
||||
enum Source {
|
||||
SOURCE_INTERNAL,
|
||||
SOURCE_EXTERNAL_PPQN_24,
|
||||
SOURCE_EXTERNAL_PPQN_4,
|
||||
};
|
||||
|
||||
class Clock {
|
||||
public:
|
||||
void Init() {
|
||||
// Initialize the clock library
|
||||
uClock.init();
|
||||
uClock.setMode(uClock.INTERNAL_CLOCK);
|
||||
uClock.setPPQN(uClock.PPQN_96);
|
||||
uClock.setTempo(DEFAULT_TEMPO);
|
||||
uClock.start();
|
||||
}
|
||||
|
||||
// Handler for receiving clock trigger(PPQN_4 or PPQN_24).
|
||||
void AttachExtHandler(void (*callback)(void)) {
|
||||
attachInterrupt(digitalPinToInterrupt(EXT_PIN), callback, RISING);
|
||||
}
|
||||
|
||||
// Internal PPQN96 callback for all clock timer operations.
|
||||
void AttachIntHandler(void (*callback)(uint32_t)) {
|
||||
uClock.setOnPPQN(callback);
|
||||
}
|
||||
|
||||
// Set the source of the clock mode.
|
||||
void SetSource(Source source) {
|
||||
switch (source) {
|
||||
case SOURCE_INTERNAL:
|
||||
uClock.setMode(uClock.INTERNAL_CLOCK);
|
||||
break;
|
||||
case SOURCE_EXTERNAL_PPQN_24:
|
||||
uClock.setMode(uClock.EXTERNAL_CLOCK);
|
||||
case SOURCE_EXTERNAL_PPQN_4:
|
||||
uClock.setMode(uClock.EXTERNAL_CLOCK);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool ExternalSource() {
|
||||
return uClock.getMode() == uClock.EXTERNAL_CLOCK;
|
||||
}
|
||||
|
||||
bool InternalSource() {
|
||||
return uClock.getMode() == uClock.INTERNAL_CLOCK;
|
||||
}
|
||||
|
||||
int Tempo() {
|
||||
return uClock.getTempo();
|
||||
}
|
||||
|
||||
void SetTempo(int tempo) {
|
||||
return uClock.setTempo(tempo);
|
||||
}
|
||||
|
||||
void Tick() {
|
||||
uClock.clockMe();
|
||||
}
|
||||
|
||||
void Pause() {
|
||||
uClock.pause();
|
||||
}
|
||||
|
||||
bool IsPaused() {
|
||||
return uClock.state == uClock.PAUSED;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
94
digital_output.h
Normal file
94
digital_output.h
Normal file
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file digital_output.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Class for interacting with trigger / gate outputs.
|
||||
* @version 0.1
|
||||
* @date 2025-04-17
|
||||
*
|
||||
* @copyright Copyright (c) 2023
|
||||
*
|
||||
*/
|
||||
#ifndef DIGITAL_OUTPUT_H
|
||||
#define DIGITAL_OUTPUT_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const byte DEFAULT_TRIGGER_DURATION_MS = 5;
|
||||
|
||||
class DigitalOutput {
|
||||
public:
|
||||
/**
|
||||
* Initializes an CV Output paired object.
|
||||
*
|
||||
* @param cv_pin gpio pin for the cv output
|
||||
*/
|
||||
void Init(uint8_t cv_pin) {
|
||||
pinMode(cv_pin, OUTPUT); // Gate/Trigger Output
|
||||
cv_pin_ = cv_pin;
|
||||
trigger_duration_ = DEFAULT_TRIGGER_DURATION_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the trigger duration in miliseconds.
|
||||
*
|
||||
* @param duration_ms trigger duration in miliseconds
|
||||
*/
|
||||
void SetTriggerDuration(uint8_t duration_ms) {
|
||||
trigger_duration_ = duration_ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn the CV and LED on or off according to the input state.
|
||||
*
|
||||
* @param state Arduino digital HIGH or LOW values.
|
||||
*/
|
||||
inline void Update(uint8_t state) {
|
||||
if (state == HIGH) High(); // Rising
|
||||
if (state == LOW) Low(); // Falling
|
||||
}
|
||||
|
||||
// Sets the cv output HIGH to about 5v.
|
||||
inline void High() { update(HIGH); }
|
||||
|
||||
// Sets the cv output LOW to 0v.
|
||||
inline void Low() { update(LOW); }
|
||||
|
||||
/**
|
||||
* Begin a Trigger period for this output.
|
||||
*/
|
||||
inline void Trigger() {
|
||||
update(HIGH);
|
||||
last_triggered_ = millis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a bool representing the on/off state of the output.
|
||||
*/
|
||||
inline void Process() {
|
||||
// If trigger is HIGH and the trigger duration time has elapsed, set the output low.
|
||||
if (on_ && (millis() - last_triggered_) >= trigger_duration_) {
|
||||
update(LOW);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a bool representing the on/off state of the output.
|
||||
*
|
||||
* @return true if current cv state is high, false if current cv state is low
|
||||
*/
|
||||
inline bool On() { return on_; }
|
||||
|
||||
private:
|
||||
unsigned long last_triggered_;
|
||||
uint8_t trigger_duration_;
|
||||
uint8_t cv_pin_;
|
||||
uint8_t led_pin_;
|
||||
bool on_;
|
||||
|
||||
void update(uint8_t state) {
|
||||
digitalWrite(cv_pin_, state);
|
||||
on_ = state == HIGH;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
134
encoder_dir.h
Normal file
134
encoder_dir.h
Normal file
@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @file encoder_dir.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Class for interacting with encoders.
|
||||
* @version 0.1
|
||||
* @date 2025-04-19
|
||||
*
|
||||
* @copyright Copyright (c) 2025
|
||||
*
|
||||
*/
|
||||
#ifndef ENCODER_DIR_H
|
||||
#define ENCODER_DIR_H
|
||||
|
||||
#include <RotaryEncoder.h>
|
||||
|
||||
#include "button.h"
|
||||
#include "peripherials.h"
|
||||
|
||||
enum Direction {
|
||||
DIRECTION_UNCHANGED,
|
||||
DIRECTION_INCREMENT,
|
||||
DIRECTION_DECREMENT,
|
||||
};
|
||||
|
||||
class EncoderDir {
|
||||
protected:
|
||||
typedef void (*CallbackFunction)(void);
|
||||
typedef void (*RotateCallbackFunction)(Direction dir, int val);
|
||||
CallbackFunction on_press;
|
||||
RotateCallbackFunction on_press_rotate;
|
||||
RotateCallbackFunction on_rotate;
|
||||
int change;
|
||||
|
||||
public:
|
||||
EncoderDir() : encoder_(ENCODER_PIN1, ENCODER_PIN2, RotaryEncoder::LatchMode::FOUR3),
|
||||
button_(ENCODER_SW_PIN) {}
|
||||
~EncoderDir() {}
|
||||
|
||||
// Set to true if the encoder read direction should be reversed.
|
||||
void SetReverseDirection(bool reversed) {
|
||||
reversed_ = reversed;
|
||||
}
|
||||
void AttachPressHandler(CallbackFunction f) {
|
||||
button_.AttachPressHandler(f);
|
||||
}
|
||||
|
||||
void AttachRotateHandler(RotateCallbackFunction f) {
|
||||
on_rotate = f;
|
||||
}
|
||||
|
||||
void AttachPressRotateHandler(RotateCallbackFunction f) {
|
||||
on_press_rotate = f;
|
||||
}
|
||||
|
||||
void OnRotate() {
|
||||
if (on_rotate != NULL) {
|
||||
on_rotate(RotateDirection(), change);
|
||||
}
|
||||
}
|
||||
|
||||
void OnPressRotate() {
|
||||
if (on_press_rotate != NULL) {
|
||||
on_press_rotate(RotateDirection(), change);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse EncoderButton increment direction.
|
||||
Direction RotateDirection() {
|
||||
int dir = (int)(encoder_.getDirection());
|
||||
return rotate_(dir, reversed_);
|
||||
}
|
||||
|
||||
void Process() {
|
||||
button_.Process();
|
||||
int change = _rotate_change();
|
||||
|
||||
// Handle encoder position change and button press.
|
||||
if (button_.On() && change != 0) {
|
||||
OnPressRotate();
|
||||
rotated_while_held = true;
|
||||
} else if (button_.Change() == Button::CHANGE_RELEASED && !rotated_while_held) {
|
||||
button_.OnPress();
|
||||
}
|
||||
|
||||
if (button_.Change() == Button::CHANGE_RELEASED) {
|
||||
rotated_while_held = false;
|
||||
}
|
||||
|
||||
if (change != 0 && !button_.On()) {
|
||||
OnRotate();
|
||||
}
|
||||
}
|
||||
|
||||
// Read the encoder state and update the read position.
|
||||
void UpdateEncoder() {
|
||||
encoder_.tick();
|
||||
}
|
||||
|
||||
private:
|
||||
int previous_pos_;
|
||||
bool reversed_ = true;
|
||||
RotaryEncoder encoder_;
|
||||
Button button_;
|
||||
bool rotated_while_held;
|
||||
|
||||
// Return the number of ticks change since last polled.
|
||||
int _rotate_change() {
|
||||
int position = encoder_.getPosition();
|
||||
|
||||
// Validation (TODO: add debounce check).
|
||||
if (previous_pos_ == position) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Update state variables.
|
||||
change = position - previous_pos_;
|
||||
previous_pos_ = position;
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
inline Direction rotate_(int dir, bool reversed) {
|
||||
switch (dir) {
|
||||
case 1:
|
||||
return (reversed) ? DIRECTION_INCREMENT : DIRECTION_DECREMENT;
|
||||
case -1:
|
||||
return (reversed) ? DIRECTION_DECREMENT : DIRECTION_INCREMENT;
|
||||
default:
|
||||
return DIRECTION_UNCHANGED;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
97
gravity.cpp
Normal file
97
gravity.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @file gravity.cpp
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Library for building custom scripts for the Sitka Instruments Gravity module.
|
||||
* @version 0.1
|
||||
* @date 2025-04-19
|
||||
*
|
||||
* @copyright Copyright (c) 2025
|
||||
*
|
||||
*/
|
||||
|
||||
#include "gravity.h"
|
||||
|
||||
void Gravity::Init() {
|
||||
InitClock();
|
||||
InitInputs();
|
||||
InitOutputs();
|
||||
InitDisplay();
|
||||
}
|
||||
|
||||
void Gravity::InitClock() {
|
||||
clock.Init();
|
||||
}
|
||||
|
||||
void Gravity::InitInputs() {
|
||||
shift_button.Init(SHIFT_BTN_PIN);
|
||||
play_button.Init(PLAY_BTN_PIN);
|
||||
|
||||
// Pin Change Interrupts for Encoder.
|
||||
// Thanks to https://dronebotworkshop.com/interrupts/
|
||||
|
||||
// Enable both PCIE2 Bit3 (Port D), and PCIE1 Bit2 (Port C).
|
||||
PCICR |= B00000110;
|
||||
// Select PCINT23 Bit4 = 1 (Pin D4)
|
||||
PCMSK2 |= B00010000;
|
||||
// Select PCINT11 Bit3 (Pin D17/A3)
|
||||
PCMSK1 |= B00001000;
|
||||
}
|
||||
|
||||
void Gravity::InitOutputs() {
|
||||
// Initialize each of the outputs with it's GPIO pins and probability.
|
||||
outputs[0].Init(OUT_CH1);
|
||||
outputs[1].Init(OUT_CH2);
|
||||
outputs[2].Init(OUT_CH3);
|
||||
outputs[3].Init(OUT_CH4);
|
||||
outputs[4].Init(OUT_CH5);
|
||||
outputs[5].Init(OUT_CH6);
|
||||
}
|
||||
void Gravity::InitDisplay() {
|
||||
// OLED Display configuration.
|
||||
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
|
||||
delay(1000);
|
||||
display.setRotation(2); // rotates text on OLED 1=90 degrees, 2=180 degrees
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(WHITE);
|
||||
display.display();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Gravity::Process() {
|
||||
// Read peripherials for changes.
|
||||
shift_button.Process();
|
||||
play_button.Process();
|
||||
encoder.Process();
|
||||
|
||||
// Call button state change handlers.
|
||||
if (play_button.Change() == Button::CHANGE_PRESSED) {
|
||||
play_button.OnPress();
|
||||
}
|
||||
if (shift_button.Change() == Button::CHANGE_PRESSED) {
|
||||
shift_button.OnPress();
|
||||
}
|
||||
|
||||
// Update Output states.
|
||||
for (int i; i < OUTPUT_COUNT; i++) {
|
||||
outputs[i].Process();
|
||||
}
|
||||
}
|
||||
|
||||
void ReadEncoder() {
|
||||
gravity.encoder.UpdateEncoder();
|
||||
}
|
||||
|
||||
// Define Encoder pin ISR.
|
||||
// Pin Change Interrupt on Port C (D17/A3).
|
||||
ISR(PCINT2_vect) {
|
||||
ReadEncoder();
|
||||
};
|
||||
// Pin Change Interrupt on Port D (D4).
|
||||
ISR(PCINT1_vect) {
|
||||
ReadEncoder();
|
||||
};
|
||||
|
||||
// Singleton
|
||||
Gravity gravity;
|
||||
46
gravity.h
Normal file
46
gravity.h
Normal file
@ -0,0 +1,46 @@
|
||||
#ifndef GRAVITY_H
|
||||
#define GRAVITY_H
|
||||
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "button.h"
|
||||
#include "clock.h"
|
||||
#include "digital_output.h"
|
||||
#include "encoder_dir.h"
|
||||
#include "peripherials.h"
|
||||
|
||||
// Hardware abstraction wrapper for the Gravity module.
|
||||
class Gravity {
|
||||
public:
|
||||
// Constructor
|
||||
Gravity()
|
||||
: display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1) {}
|
||||
|
||||
// Deconstructor
|
||||
~Gravity() {}
|
||||
|
||||
// Initializes the Arduino, and Gravity hardware.
|
||||
void Init();
|
||||
|
||||
// Polling check for state change of inputs and outputs.
|
||||
void Process();
|
||||
|
||||
Adafruit_SSD1306 display; // OLED display object.
|
||||
Clock clock; // Clock source wrapper.
|
||||
DigitalOutput outputs[OUTPUT_COUNT]; // An array containing each Output object.
|
||||
EncoderDir encoder; // Rotary encoder with button instance
|
||||
Button shift_button;
|
||||
Button play_button;
|
||||
|
||||
private:
|
||||
void InitClock();
|
||||
void InitDisplay();
|
||||
void InitInputs();
|
||||
void InitOutputs();
|
||||
};
|
||||
|
||||
extern Gravity gravity;
|
||||
|
||||
#endif
|
||||
43
peripherials.h
Normal file
43
peripherials.h
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @file peripherials.h
|
||||
* @author Adam Wonak (https://github.com/awonak)
|
||||
* @brief Arduino pin definitions for the Sitka Instruments Gravity module.
|
||||
* @version 0.1
|
||||
* @date 2025-04-19
|
||||
*
|
||||
* @copyright Copyright (c) 2025
|
||||
*
|
||||
*/
|
||||
#ifndef PERIPHERIALS_H
|
||||
#define PERIPHERIALS_H
|
||||
|
||||
// OLED Display config
|
||||
#define OLED_ADDRESS 0x3C
|
||||
#define SCREEN_WIDTH 128
|
||||
#define SCREEN_HEIGHT 64
|
||||
|
||||
// Peripheral input pins
|
||||
#define ENCODER_PIN1 17 // A3
|
||||
#define ENCODER_PIN2 4
|
||||
#define ENCODER_SW_PIN 14
|
||||
|
||||
// Clock and CV Inputs
|
||||
#define EXT_PIN 2
|
||||
#define CV1_PIN A7
|
||||
#define CV2_PIN A6
|
||||
|
||||
// Button pins
|
||||
#define SHIFT_BTN_PIN 12
|
||||
#define PLAY_BTN_PIN 5
|
||||
|
||||
// Output Pins
|
||||
#define OUT_CH1 7
|
||||
#define OUT_CH2 8
|
||||
#define OUT_CH3 10
|
||||
#define OUT_CH4 6
|
||||
#define OUT_CH5 9
|
||||
#define OUT_CH6 11
|
||||
|
||||
const uint8_t OUTPUT_COUNT = 6;
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user