Compare commits

38 Commits

Author SHA1 Message Date
Hykilpikonna fb8870038d [F] Fix random hue and optimize last frame detection 2023-04-28 23:00:28 -04:00
Hykilpikonna ca99722d5a [+] Better animation 2023-04-28 22:51:42 -04:00
Hykilpikonna 24a771151d [O] Optimize: Don't draw unchanged frames 2023-04-28 22:51:12 -04:00
Hykilpikonna 4dcb6dd2b1 [+] Animation queue 2023-04-28 19:12:46 -04:00
Hykilpikonna b4390eaf2c [O] Use constexpr instead of dynamic calculation at runtime 2023-04-28 19:12:36 -04:00
Hykilpikonna 22cae349e7 [+] Update thread 2023-04-28 18:22:26 -04:00
Hykilpikonna 2258b7db6e [+] Run lights 2023-04-28 17:53:34 -04:00
Hykilpikonna 1823b0f5d2 [F] Fix position calculation 2023-04-28 17:53:12 -04:00
Hykilpikonna 658b55394d [F] Fix compilation issue 2023-04-27 20:44:36 -04:00
Hykilpikonna ace84d0773 [+] Keyboard lights 2023-04-27 20:43:24 -04:00
Hykilpikonna 83d65430a7 [O] Optimize hit detection 2023-04-21 19:53:26 -04:00
Hykilpikonna 3e1a74b0b1 [O] Class refactor 2023-04-21 18:14:41 -04:00
Hykilpikonna 4de4f39269 [O] Optimize fps counter 2023-04-21 17:39:23 -04:00
Hykilpikonna 61eaabfa33 [M] Move constants 2023-04-21 17:39:04 -04:00
Hykilpikonna 14cd64dc92 [-] Remove unused code 2023-04-21 17:31:31 -04:00
Hykilpikonna 90c54f1a25 [+] Choose port prompt 2023-04-21 17:29:41 -04:00
Hykilpikonna 0f2ae62247 [+] Light strip 2023-04-21 17:29:21 -04:00
Azalea Gui 5103d75c51 [+] Panel v3 2023-04-15 05:14:16 -04:00
Azalea Gui c55272a376 [+] Panel v2 fixed 2023-04-14 18:54:53 -04:00
Azalea Gui c6b250bf01 [+] Mux v3 2023-04-14 18:47:37 -04:00
Hykilpikonna 5d315a5102 [+] Multithreading 2023-04-14 18:35:07 -04:00
Hykilpikonna 2e836f34e0 [+] Loop both 2023-04-12 19:40:52 -04:00
Hykilpikonna 469c3ccb90 [+] Panel demo 2023-04-12 19:38:56 -04:00
Hykilpikonna 1c2dfff9b4 [+] Add libraries 2023-04-12 19:38:21 -04:00
Hykilpikonna ede28ef420 [+] Pin config 2023-04-12 19:37:41 -04:00
Hykilpikonna 038dcfa989 [+] fps reporting 2023-04-11 23:39:56 -04:00
Hykilpikonna 41108a6311 [+] Initialize pins 2023-04-11 23:39:47 -04:00
Hykilpikonna c1160429f6 [+] ESP32-S3 Config 2023-04-11 23:39:20 -04:00
Hykilpikonna cc5c2ae6cb [O] (Experimental) switch to stm32 2023-04-10 18:50:21 -04:00
Hykilpikonna 23decb6c75 [+] Order pcb instructions 2023-03-25 21:48:01 -04:00
Hykilpikonna 94c8a4fa7a [+] Flash firmware guide 2023-03-25 21:20:41 -04:00
Hykilpikonna 8b94c8c2d0 [M] Split config 2023-03-25 21:15:05 -04:00
Hykilpikonna 3c0b3008cd [-] Replace ESP32 with "development board" 2023-03-25 21:10:16 -04:00
Hykilpikonna be0a2f0136 [O] Reformat table 2023-03-25 21:09:33 -04:00
Hykilpikonna 5a3e64df88 [+] Pin configuration 2023-03-25 21:08:06 -04:00
Hykilpikonna 9e387e222f [+] Add constants for pins 2023-03-25 19:49:40 -04:00
Hykilpikonna f465f3b0c2 [M] Move config section 2023-03-25 19:40:21 -04:00
Hykilpikonna 4d975caf40 [M] Split macros 2023-03-25 19:40:06 -04:00
21 changed files with 610 additions and 148 deletions
+45 -3
View File
@@ -20,14 +20,14 @@
Open-KeyPrec is an open-sourced, pressure-sensitive electric keyboard percussion (mallet) MIDI instrument. It has 61 keys, spanning 5 octaves of playing range from C2 to C7 like a standard Marimba. It also has a MIDI control panel with 16 buttons, 12 knobs (8 × 280° potentiometers and 4 × 360° rotary encoders).
The sensors are controlled by an ESP32 development board, and the output is converted into a standard MIDI device. You can use audio workstation softwares like GarageBand or Reaper (or open-source alternatives like LMMS, Zrythm, or Ardour) to simulate the sounds of all kinds of different instruments.
The sensors are controlled by a development board, and the output is converted into a standard MIDI device. You can use audio workstation softwares like GarageBand or Reaper (or open-source alternatives like LMMS, Zrythm, or Ardour) to simulate the sounds of all kinds of different instruments.
## Code
**Source code structure**
* `/src` - PC driver written in Rust
* `/firmware` - ESP32 module firmware written in C++
* `/firmware` - Development board firmware written in C++
* `/circuits` - Printed circuit boards
* `/models` - 3D-printable models
@@ -38,6 +38,22 @@ The sensors are controlled by an ESP32 development board, and the output is conv
* CLion: IDE for Rust & C++
* PlatformIO: Firmware development toolkit
**Pin Configuration**
| Pins | I/O | Description |
|------|-----|-------------------------------------------|
| 4 | O | Mux Select for C2-B6 |
| 5 | IA | Mux Input for C2-B6 |
| 1 | IA | Input for C7 |
| 8 | I | (Panel) Input for 4 rotary encoders |
| 3 | O | Mux Select for buttons and potentiometers |
| 2 | I | Mux Input for buttons |
| 1 | IA | Mux Input for potentiometers |
| 3 | O | RGB LED Control |
Total ADC channels required: 7
Total GPIO required: 23
## Build
### Materials Required
@@ -53,10 +69,36 @@ The sensors are controlled by an ESP32 development board, and the output is conv
### 1. Order PCBs
!!!!!!!!!! TODO !!!!!!!!!!
I ordered my PCBs from JLC, but any PCB service would work. They should accept the exported Gerber zip files in `circuits/Exports` folder.
I created two circuit boards for this keyboard, one is the multiplexer ([Mux v2.zip](circuits/Mux%20v2.zip)) which uses the CD74HC4067 1:16 multiplexer ic to convert the 61 separate analog inputs requirement into only 5 analog inputs and 4 digital switch outputs.
It also haso diodes and resistors for voltage protection. The other pcb ([OKP-Panel-SMT.zip](circuits/OKP-Panel-SMT.zip)) is for the MIDI control panel containing the buttons and knobs.
1. Go to jlcpcb.com (Or jlc.com if you read Chinese)
2. Upload the Gerber file
3. Set parameters: FR-4, 2 Layers, Color, leave default for other options
**PCB Assembly (SMT)**
If you're not comfortable soldering small SMD components, you can order PCB Assembly from JLC.
You can also select which parts you would like to SMT based on your soldering abilities.
For example, in the Mux pcb, I ordered SMT for only the 0603 Zener diodes and the CD74HC4067 ic, and hand-soldered everything else.
1. Order "PCB Assembly" (SMT) and click confirm
2. In the SMT menu, upload the BOM and CPL (PickAndPlace) files
3. After uploading, remove the parts you want to manually solder (they're pretty expensive, since someone in the factory still have to manually solder them for you)
4. Order manually-soldered parts somewhere else (e.g. szlcsc.com)
## Usage
### Flash Firmware
1. Install PlatformIO Core ([Guide](https://platformio.org/install/cli))
2. Open the `firmware` directory
3. In your preferred IDE, edit `src/config.h` with your pin configuration.
4. Edit `platformio.ini` to match your dev board.
5. Connect dev board, build and upload firmware using `platformio run --target upload`
### Setup MIDI Device
!!!!!!!!!! TODO !!!!!!!!!!
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17 -4
View File
@@ -13,6 +13,16 @@
;board = wemos_d1_uno32
;framework = arduino
[env:s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
lib_deps =
adafruit/Adafruit NeoPixel@^1.11.0
paulstoffregen/Encoder@^1.4.2
; ESP32-S2 and C3 both has a problem where the ADC jumps around a LOT
;[env:s2]
;platform = espressif32
;board = lolin_s2_mini
@@ -23,11 +33,14 @@
;board = dfrobot_beetle_esp32c3
;framework = arduino
[env:s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
; YD STM32F411CEU6
;[env:stm32f411ce]
;platform = ststm32
;board = blackpill_f411ce
;framework = arduino
;upload_protocol = dfu
; Arduino nano is good but too few pins
;[env:n328p]
;platform = atmelavr
;board = nanoatmega328
+81
View File
@@ -0,0 +1,81 @@
#ifndef FIRMWARE_CONFIG_H
#define FIRMWARE_CONFIG_H
#include "utils.h"
// ========================================
// Main Keyboard Pin Configuration
// ========================================
// LED indicator for pulling update
const int LED_REFRESH = 45;
// 4 1:16 Multiplexers: GPIO pins for each analog multiplexer that handles 12 sensors of an octave
// Notes are connected in order: Mux #1 (0-11), Mux #2 (12-23), Mux #3 (24-35), Mux #4 (36-47), Mux #5 (48-59)
const int NUM_MUX = 5;
const int PINS_PER_MUX = 12;
const int MUX_IN[NUM_MUX] = {4, 5, 6, 7, 8};
// Select pins for every multiplexer, each multiplexer has 4 select pins, all sel0 are connected to 14, etc.
const int NUM_MUX_SEL = 4;
const int MUX_SEL_OUT[NUM_MUX_SEL] = {14, 13, 12, 11};
// LED Light strip
const int LK_PIN = 2;
const int LK_LIGHTS_PER_METER = 60;
constexpr float LK_NUM_METERS = 2;
const int LK_KEY_SPACING_MM = 7;
const int LK_KEY_LEN_MM = 30;
const int LK_OFFSET_MM = 25; // Length in mm from the start of the strip to the first key
const float LK_LIGHTS_PER_MM = LK_LIGHTS_PER_METER / 1000.0;
constexpr int LK_NUM_LIGHTS = (int) (LK_LIGHTS_PER_METER * LK_NUM_METERS + 0.5);
// ========================================
// MIDI Panel Pin Configuration
// ========================================
// 3 1:8 Multiplexers for the midi panel
const int P_PINS_PER_MUX = 8;
const int P_KEY_MUX_IN = 36;
const int P_BUTTON_MUX_IN = 35; // Digital signal inputs for button multiplexers
const int P_KNOB_MUX_IN = 10; // Analog signal inputs for potentiometer
// Select pins for every multiplexer, each multiplexer has 3 select pins
const int P_NUM_MUX_SEL = 3;
const int P_MUX_SEL_OUT[P_NUM_MUX_SEL] = {37, 38, 39};
// Rotary encoder pins
const int P_NUM_ROTARY = 4;
const int P_ROTARY_A[P_NUM_ROTARY] = {18, 17, 16, 15};
const int P_ROTARY_B[P_NUM_ROTARY] = {19, 20, 21, 47};
// LED light strips
const int P_LED_KEY = 42;
const int P_LED_KNOB = 41;
const int P_LED_ROTARY = 40;
// ========================================
// Constants
// ========================================
typedef struct note
{
char name[4];
u16 midi;
} Note;
// 61 Notes from C2 to C7
const int NUM_NOTES = 61;
const int NUM_SHARP_NOTES = 25;
const int NUM_REGULAR_NOTES = NUM_NOTES - NUM_SHARP_NOTES;
const Note notes[] = {
{"C2", 36}, {"C#2", 37}, {"D2", 38}, {"D#2", 39}, {"E2", 40}, {"F2", 41}, {"F#2", 42}, {"G2", 43}, {"G#2", 44}, {"A2", 45}, {"A#2", 46}, {"B2", 47},
{"C3", 48}, {"C#3", 49}, {"D3", 50}, {"D#3", 51}, {"E3", 52}, {"F3", 53}, {"F#3", 54}, {"G3", 55}, {"G#3", 56}, {"A3", 57}, {"A#3", 58}, {"B3", 59},
{"C4", 60}, {"C#4", 61}, {"D4", 62}, {"D#4", 63}, {"E4", 64}, {"F4", 65}, {"F#4", 66}, {"G4", 67}, {"G#4", 68}, {"A4", 69}, {"A#4", 70}, {"B4", 71},
{"C5", 72}, {"C#5", 73}, {"D5", 74}, {"D#5", 75}, {"E5", 76}, {"F5", 77}, {"F#5", 78}, {"G5", 79}, {"G#5", 80}, {"A5", 81}, {"A#5", 82}, {"B5", 83},
{"C6", 84}, {"C#6", 85}, {"D6", 86}, {"D#6", 87}, {"E6", 88}, {"F6", 89}, {"F#6", 90}, {"G6", 91}, {"G#6", 92}, {"A6", 93}, {"A#6", 94}, {"B6", 95},
{"C7", 96}
};
#endif //FIRMWARE_CONFIG_H
+127
View File
@@ -0,0 +1,127 @@
//
// Created by Hykilpikonna on 4/21/23.
//
#include <Arduino.h>
#include "config.h"
#include "utils.h"
#include "Adafruit_NeoPixel.h"
#include <unordered_map>
#include <queue>
const int ANIM_QUEUE_SIZE = 100;
const int FRAME_DELAY_MS = 20;
using namespace std;
class KeyboardLights
{
private:
Adafruit_NeoPixel led_key;
unordered_map<int, int> key_to_light;
TaskHandle_t thread{};
u32 animation[ANIM_QUEUE_SIZE][LK_NUM_LIGHTS]{};
u32 last_frame[LK_NUM_LIGHTS]{};
int anim_index = 0;
static int wrap(int i) { return (i + LK_NUM_LIGHTS) % LK_NUM_LIGHTS; }
void update()
{
// Render this frame
auto frame = animation[anim_index];
for (int i = 0; i < LK_NUM_LIGHTS; i++)
{
if (frame[i] == 0 && last_frame[i] == 0) continue;
last_frame[i] = frame[i];
led_key.setPixelColor(i, frame[i]);
}
led_key.show();
// Clear the current frame
for (int i = 0; i < LK_NUM_LIGHTS; i++) frame[i] = 0;
anim_index = (anim_index + 1) % ANIM_QUEUE_SIZE;
}
[[noreturn]] static void loop(void *pv)
{
auto *lights = (KeyboardLights *) pv;
while (true)
{
lights->update();
vTaskDelay(FRAME_DELAY_MS / portTICK_PERIOD_MS);
}
}
public:
KeyboardLights() : led_key(LK_NUM_LIGHTS, LK_PIN, NEO_GRB + NEO_KHZ800)
{
// Initialize key to light map
int regi = 0;
for (int i = NUM_NOTES - 1; i >= 0; i--)
{
auto note = notes[i];
auto ki = (float) regi;
if (note.name[1] == '#') ki -= 0.5;
// Calculate the length from the start of the keyboard to the key
ki *= LK_KEY_SPACING_MM + LK_KEY_LEN_MM;
ki += LK_KEY_LEN_MM / 2.0; // Center of the key
// Calculate the index of the light at the same length position
ki *= LK_LIGHTS_PER_MM;
// Convert key index to mm, then to light index
key_to_light[i] = (int) round(ki);
// Increment the regular note index if it's not a sharp note
if (note.name[1] != '#') regi++;
}
}
void begin()
{
pinModeSafe(LK_PIN, OUTPUT);
led_key.begin();
xTaskCreate(loop, "loopLights", 4096, this, 1, &thread);
}
void hit(int key)
{
// 1. Calculate the starting index
int start = key_to_light[key];
Serial.printf("Key %d -> Light %d\n", key, start);
// 2. Start animation
// Fade out for one light
// for (int i = 0; i < 50; i++)
// {
// animation[(anim_index + i) % ANIM_QUEUE_SIZE][start] = Adafruit_NeoPixel::ColorHSV(0, 255, (50 - i) * 2);
// }
// Fade out from the center to both sides
int hue = random(0, 65535);
for (int i = 0; i < 25; i++)
{
// Pick a random hue
Serial.printf("Hue: %d\n", hue);
int v = (25 - i) * 2;
int s = 255;
auto frame = animation[(anim_index + i) % ANIM_QUEUE_SIZE];
frame[start] = Adafruit_NeoPixel::ColorHSV(hue, s, v);
// Sides
for (int j = 1; j <= min(i, 10); j++)
{
frame[(start + j) % LK_NUM_LIGHTS] = Adafruit_NeoPixel::ColorHSV(hue, s, MAX(v - j * 2, 0));
frame[(start - j + LK_NUM_LIGHTS) % LK_NUM_LIGHTS] = Adafruit_NeoPixel::ColorHSV(hue, s, MAX(v - j * 2, 0));
}
}
}
};
+100 -140
View File
@@ -1,79 +1,89 @@
#include <Arduino.h>
#include <chrono>
#include <cinttypes>
#include <driver/adc.h>
#include "config.h"
#include "Adafruit_NeoPixel.h"
#include "main.h"
#include "utils.h"
#include "panel.cpp"
#include "keyboard_lights.cpp"
#define u8 uint8_t
#define u16 uint16_t
#define u32 uint32_t
#define u64 uint64_t
#define timeMillis() std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count()
#define min(a, b) ((a) < (b) ? (a) : (b))
#define let auto
#define val const auto
u64 start_time = 0;
typedef struct note {
char name[4];
u16 midi;
} Note;
// 61 Notes from C2 to C7
val NUM_NOTES = 61;
const Note notes[] = {
{"C2", 36}, {"C#2", 37}, {"D2", 38}, {"D#2", 39}, {"E2", 40}, {"F2", 41}, {"F#2", 42}, {"G2", 43}, {"G#2", 44}, {"A2", 45}, {"A#2", 46}, {"B2", 47},
{"C3", 48}, {"C#3", 49}, {"D3", 50}, {"D#3", 51}, {"E3", 52}, {"F3", 53}, {"F#3", 54}, {"G3", 55}, {"G#3", 56}, {"A3", 57}, {"A#3", 58}, {"B3", 59},
{"C4", 60}, {"C#4", 61}, {"D4", 62}, {"D#4", 63}, {"E4", 64}, {"F4", 65}, {"F#4", 66}, {"G4", 67}, {"G#4", 68}, {"A4", 69}, {"A#4", 70}, {"B4", 71},
{"C5", 72}, {"C#5", 73}, {"D5", 74}, {"D#5", 75}, {"E5", 76}, {"F5", 77}, {"F#5", 78}, {"G5", 79}, {"G#5", 80}, {"A5", 81}, {"A#5", 82}, {"B5", 83},
{"C6", 84}, {"C#6", 85}, {"D6", 86}, {"D#6", 87}, {"E6", 88}, {"F6", 89}, {"F#6", 90}, {"G6", 91}, {"G#6", 92}, {"A6", 93}, {"A#6", 94}, {"B6", 95},
{"C7", 96}
};
// LED pins
val PIN_LED = 37;
let led_state = true;
// 5 Multiplexers: GPIO pins for each analog multiplexer that handles 12 sensors (1 octave)
// Notes are connected in order: Mux #1 (0-11), Mux #2 (12-23), Mux #3 (24-35), Mux #4 (36-47), Mux #5 (48-59)
val NUM_MUX = 5;
val PINS_PER_MUX = 12;
const adc1_channel_t mux_in[] = {ADC1_CHANNEL_1, ADC1_CHANNEL_2, ADC1_CHANNEL_3, ADC1_CHANNEL_4, ADC1_CHANNEL_5};
//const int mux_in[] = {A0, A1, A2, A3, A4};
// Select pins for every multiplexer, each multiplexer has 4 select pins, all sel0 are connected to 14, etc.
val NUM_MUX_SEL = 4;
const int mux_sel[] = {40, 38, 36, 34};
// Multisampling: Take multiple samples for each sensor and average them
val MULTISAMPLING = 1;
int lasts[NUM_NOTES]; // variable to store the value coming from the sensor
u32 lasts[NUM_NOTES]; // variable to store the value coming from the sensor
u64 last_hit_times[NUM_NOTES];
u32 bounce_delay = 50; // Minimum time between two hits
val max_sensor = 4096;
val max_threshold = 2000;
val active_threshold = 100; // Minimum value to be considered as a hit
let max_sensor = 4096;
let max_threshold = 3000;
let active_threshold = 400; // Minimum value to be considered as a hit
let led_refresh_on = false;
Panel panel;
KeyboardLights keyboardLights;
void setup()
{
// Turn on LED
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, led_state);
// Initialize pins
pinModeSafe(LED_REFRESH, OUTPUT);
for (int pin: MUX_IN) pinModeSafe(pin, INPUT);
for (int pin: MUX_SEL_OUT) pinModeSafe(pin, OUTPUT);
// Initialize pin and serial
for (int pin: mux_in) pinMode(pin, INPUT);
adc1_config_width(ADC_WIDTH_MAX);
for (val pin : mux_in) {
adcAttachPin(pin);
adc1_config_channel_atten(pin, ADC_ATTEN_DB_11);
// Initialize serial
Serial.begin(115200);
Serial.printf("Initialized\r\n");
panel.begin();
keyboardLights.begin();
}
u64 fps_last_update = 0;
u32 fps_updates = 0;
const u32 fps_interval_ms = 1000;
void countFps(u64 time)
{
// Report FPS every second
fps_updates++;
if (time - fps_last_update >= fps_interval_ms)
{
fps_last_update = time;
double fps = 1.0 * fps_updates / fps_interval_ms * 1000;
Serial.printf("FPS: %.2f\r\n", fps);
fps_updates = 0;
}
// for (val pin: mux_sel) pinMode(pin, OUTPUT);
Serial.begin(9600);
Serial.println("Initialized");
}
// start_time = timeMillis();
void readKeyboard()
{
u64 time = millis();
countFps(time);
// Toggle LED refresh indicator
digitalWrite(LED_REFRESH, led_refresh_on = !led_refresh_on);
// Loop through each multiplexer state
for (int i = 0; i < PINS_PER_MUX; i++)
{
// Set select pins
for (int j = 0; j < NUM_MUX_SEL; j++)
{
// i >> j is the jth bit of i
digitalWrite(MUX_SEL_OUT[j], (i >> j) & 1);
}
// Read input pins from the multiplexer
for (int j = 0; j < NUM_MUX; j++)
{
int note_id = j * PINS_PER_MUX + i;
if (note_id >= NUM_NOTES) break;
// Read the analog input
u32 v = analogRead(MUX_IN[j]);
if (v != lasts[note_id])
{
on_sensor_update(note_id, time, lasts[note_id], v);
}
lasts[note_id] = v;
}
}
}
/**
@@ -81,90 +91,40 @@ void setup()
*
* @param id Sensor index
*/
void on_sensor_update(int id, u64 time, int last, int current)
void on_sensor_update(int id, u64 time, u32 last, u32 current)
{
// If the last value is larger than the current value, check timeout
if (last > current && last > active_threshold)
// If the last hit is too close, ignore this hit
let last_hit_time = last_hit_times[id];
if (time - last_hit_time < bounce_delay) return;
// If the value is above the threshold
if (current > active_threshold)
{
u64 elapsed = time - last_hit_times[id];
if (elapsed < 150)
// If the last value is below the threshold, it's a new hit
if (last < active_threshold)
{
// If the last hit is too close, ignore this hit
return;
// Send MIDI message
// /hit <note> <velocity>
Serial.printf("/hit %d %d\r\n", notes[id].midi,
MIN((last - active_threshold) * 127 / (max_threshold - active_threshold), 127));
// Lights
keyboardLights.hit(id);
}
// The last value is a local maximum,
// and we read it as the hit strength of our note. Send midi command to the host.
// /hit <note> <velocity>
Serial.printf("/hit %d %d\r\n", notes[id].midi,
min((last - active_threshold) * 127 / (max_threshold - active_threshold), 127));
// Update last hit time
}
else if (last > active_threshold)
{
// Released
last_hit_times[id] = time;
}
}
/**
* Read sensor value with multisampling
*
* @param pin Sensor pin
* @return Sensor value
*/
int read_sensor(int pin)
{
let sum = 0;
for (let i = 0; i < MULTISAMPLING; i++) {
sum += analogRead(pin);
}
return (int) round(((float) sum) / ((float) MULTISAMPLING));
}
void loop()
{
// u64 time = timeMillis();
// u64 elapsed = time - start_time;
readKeyboard();
// Toggle LED
led_state = !led_state;
digitalWrite(PIN_LED, led_state);
delay(100);
// // Loop through each multiplexer input
for (val mux : mux_in)
{
val v = read_sensor(mux);
Serial.printf("%d ", v);
}
Serial.println();
// Serial.printf("%" PRIu64 "=============\r\n", elapsed);
// Loop through each multiplexer state
// for (int i = 0; i < PINS_PER_MUX; i++)
// for (int i = 0; i < LK_NUM_LIGHTS; i++)
// {
// // Set select pins
// for (int j = 0; j < NUM_MUX_SEL; j++)
// {
// // i >> j is the jth bit of i
// digitalWrite(mux_sel[j], (i >> j) & 1);
// }
//
// // Read four input pins from the multiplexer
// for (int j = 0; j < NUM_MUX; j++)
// {
// int note_id = j * PINS_PER_MUX + i;
// if (note_id >= NUM_NOTES) break;
//
// // Read the analog input
// int v = read_sensor(mux_in[j]);
// if (v != lasts[note_id])
// {
// // Serial prints are really slow, so don't use them in debug mode
// Serial.printf("%s %d\r\n", notes[note_id].name, v);
// on_sensor_update(note_id, time, lasts[note_id], v);
// }
// lasts[note_id] = v;
// }
// lk.setPixelColor(i, Adafruit_NeoPixel::ColorHSV(last_hue + i * hue_interval, 255, brightness));
// }
// Serial.printf("Loop %" PRIu64 "\r\n", elapsed);
}
}
+15
View File
@@ -0,0 +1,15 @@
//
// Created by Hykilpikonna on 4/13/23.
//
#ifndef FIRMWARE_MAIN_H
#define FIRMWARE_MAIN_H
/**
* Called when the sensor value changes
*
* @param id Sensor index
*/
void on_sensor_update(int id, u64 time, u32 last, u32 current);
#endif //FIRMWARE_MAIN_H
+184
View File
@@ -0,0 +1,184 @@
//
// Created by Hykilpikonna on 4/21/23.
//
#include <Arduino.h>
#include "config.h"
#include "Adafruit_NeoPixel.h"
#include "Encoder.h"
/**
* Class controlling the MIDI panel
*/
class Panel {
private:
Adafruit_NeoPixel led_key;
Adafruit_NeoPixel led_knob;
Adafruit_NeoPixel led_rotary;
u16 last_hue = 0;
u8 brightness = 40;
bool key_states[P_PINS_PER_MUX]{};
bool btn_states[P_PINS_PER_MUX]{};
u32 pot_states[P_PINS_PER_MUX]{};
Encoder *encoders[P_NUM_ROTARY]{};
int encoder_states[P_NUM_ROTARY]{};
TaskHandle_t panelThread{};
public:
Panel() :
led_key(4, P_LED_KEY, NEO_GRB + NEO_KHZ800),
led_knob(9, P_LED_KNOB, NEO_GRB + NEO_KHZ800),
led_rotary(9, P_LED_ROTARY, NEO_GRB + NEO_KHZ800)
{}
void begin()
{
for (int pin: P_MUX_SEL_OUT) pinModeSafe(pin, OUTPUT);
for (int pin: P_ROTARY_A) pinModeSafe(pin, INPUT);
for (int pin: P_ROTARY_B) pinModeSafe(pin, INPUT);
pinModeSafe(P_BUTTON_MUX_IN, INPUT);
pinModeSafe(P_KEY_MUX_IN, INPUT);
pinModeSafe(P_KNOB_MUX_IN, INPUT);
pinModeSafe(P_LED_KEY, OUTPUT);
pinModeSafe(P_LED_KNOB, OUTPUT);
pinModeSafe(P_LED_ROTARY, OUTPUT);
led_key.begin();
led_knob.begin();
led_rotary.begin();
// Initialize encoders
for (int i = 0; i < P_NUM_ROTARY; i++)
{
encoders[i] = new Encoder(P_ROTARY_A[i], P_ROTARY_B[i]);
}
xTaskCreate(loopPanel, "loopPanel", 4096, this, 1, &panelThread);
}
private:
void readPanel()
{
const auto hue_interval = 512;
last_hue += hue_interval;
// Read rotary encoders
for (int i = 0; i < P_NUM_ROTARY; ++i)
{
int state = encoders[i]->read();
if (encoder_states[i] != state)
{
encoder_states[i] = state;
Serial.printf("Rotary changed - id: %d, value: %d\r\n", i, state);
led_rotary.setPixelColor(i, Adafruit_NeoPixel::ColorHSV(last_hue, 255, brightness));
led_rotary.show();
}
}
// Read buttons
for (int i = 0; i < P_PINS_PER_MUX; ++i)
{
// Set select pins
for (int j = 0; j < P_NUM_MUX_SEL; ++j)
{
// i >> j is the jth bit of i
digitalWrite(P_MUX_SEL_OUT[j], (i >> j) & 1);
}
vTaskDelay(1);
// Read button
int key = !digitalRead(P_KEY_MUX_IN);
int btn = !digitalRead(P_BUTTON_MUX_IN);
// If the state is changed, call button callback
if (key_states[i] != key)
{
key_states[i] = key;
onKey(i, key);
}
if (btn_states[i] != btn)
{
btn_states[i] = btn;
onBtn(i, btn);
}
// Read potentiometer
int pot = (int) round(analogRead(P_KNOB_MUX_IN) / 16.0);
onPotRead(i, pot);
// If the state is changed, call potentiometer callback
if (ABS(pot_states[i] - pot) > 4)
{
pot_states[i] = pot;
onPotChange(i, pot);
}
}
delay(10);
led_key.show();
led_knob.show();
led_rotary.show();
}
void onKey(int id, bool state)
{
// Check if it's one of the larger keys (the first 4)
if (id < 4)
{
if (state)
{
// Set a random color for the key's LED
led_key.setPixelColor(id, Adafruit_NeoPixel::ColorHSV(random(0, 65535), 255, brightness));
led_key.show();
}
else
{
// Clear the key's LED
led_key.setPixelColor(id, 0);
led_key.show();
}
}
// Key 5 = clear
if (id == 4 && state)
{
led_key.clear();
led_key.show();
}
Serial.printf("Key changed - id: %d, state: %d\r\n", id, state);
}
void onBtn(int id, bool state)
{
Serial.printf("Button changed - id: %d, state: %d\r\n", id, state);
}
void onPotRead(int id, u8 value)
{
// Set LED
led_knob.setPixelColor(id, Adafruit_NeoPixel::ColorHSV(last_hue, 255, value));
led_knob.show();
}
void onPotChange(int id, u8 value)
{
// Serial.printf("Potentiometer changed - id: %d, value: %d\r\n", id, value);
}
[[noreturn]] static void loopPanel(void* pvParameters)
{
auto* panel = (Panel*) pvParameters;
while (true)
{
panel->readPanel();
}
}
};
+3
View File
@@ -0,0 +1,3 @@
#include "utils.h"
+15
View File
@@ -0,0 +1,15 @@
#ifndef FIRMWARE_UTILS_H
#define FIRMWARE_UTILS_H
#define u8 uint8_t
#define u16 uint16_t
#define u32 uint32_t
#define u64 uint64_t
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define ABS(a) ((a) < 0 ? -(a) : (a))
#define let auto
#define pinModeSafe(pin, mode) do { if (pin != -1) pinMode(pin, mode); } while (false)
#endif //FIRMWARE_UTILS_H
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+23 -1
View File
@@ -10,7 +10,29 @@ fn start() -> Result<()> {
let midi_out = MidiOutput::new("MIDI Output")?;
let mut conn_out = midi_out.create_virtual("Virtual MIDI Output").unwrap();
let serial_port = serialport::new("/dev/cu.usbmodem14501", 9600)
// List serial devices
let ports = serialport::available_ports()?;
// If there are no ports available, quit
if ports.is_empty() {
println!("No serial ports found.");
exit(0);
}
// Let the user choose a port
println!("Available serial ports:");
for (i, p) in ports.iter().enumerate() {
println!("{}: {}", i, p.port_name);
}
println!("Choose a port (range 0-{}): ", ports.len() - 1);
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let choice = input.trim().parse::<usize>().unwrap();
let port = &ports[choice];
// Open serial port
let serial_port = serialport::new(port.port_name.as_str(), 115200)
.timeout(std::time::Duration::from_millis(10))
.open()?;
let mut reader = BufReader::new(serial_port);